Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 98 additions & 69 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions statchart/schemas/migrate/migrate.cue
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,16 @@ kind: "StatChart"
spec: {
calculation: *commonMigrate.#mapping.calc[#panel.options.reduceOptions.calcs[0]] | commonMigrate.#defaultCalc // only consider [0] here as Perses's GaugeChart doesn't support individual calcs

// metricLabel
#textMode: *#panel.options.textMode | null
if #textMode == "name" && (*#panel.targets[0].legendFormat | null) != null {
// /!\ best effort logic
// - if legendFormat contains a more complex expression than {{label}}, the result will be broken (but manually fixable afterwards still)
// - Perses's metricLabel is a single setting at panel level, hence the [0], so the result wont fit in case of multiple queries using different legendFormat
metricLabel: strings.Trim(#panel.targets[0].legendFormat, "{}")
}
// /!\ here too using [0] thus not perfect, even though the field getting remapped is unique to the whole panel in that case
if #textMode == "auto" && (*#panel.targets[0].format | null) == "table" && (*#panel.options.reduceOptions.fields | null) != null {
metricLabel: strings.Trim(#panel.options.reduceOptions.fields, "/^$")
// textMode - map directly from Grafana's BigValueTextMode enum
#grafanaTextMode: *#panel.options.textMode | "auto"
if #grafanaTextMode != null {
textMode: #grafanaTextMode
}
Comment on lines +40 to +43
Copy link
Contributor

@AntoineThebaud AntoineThebaud Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if is always mached in the current case as #grafanaTextMod falls back to "auto" (never null), so you should either change to

Suggested change
#grafanaTextMode: *#panel.options.textMode | "auto"
if #grafanaTextMode != null {
textMode: #grafanaTextMode
}
#grafanaTextMode: *#panel.options.textMode | null
if #grafanaTextMode != null {
textMode: #grafanaTextMode
}

or

Suggested change
#grafanaTextMode: *#panel.options.textMode | "auto"
if #grafanaTextMode != null {
textMode: #grafanaTextMode
}
textMode: *#panel.options.textMode | "auto"

since textMode is an optional attribute I guess the first suggestion makes more sense


// metricLabel - map from reduceOptions.fields for field selection
#fields: *#panel.options.reduceOptions.fields | null
if #fields != null && #fields != "" {
metricLabel: strings.Trim(#fields, "/^$")
}

// format
Expand Down
1 change: 1 addition & 0 deletions statchart/schemas/migrate/tests/basic/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"kind": "StatChart",
"spec": {
"calculation": "last-number",
"textMode": "auto",
"format": {
"decimalPlaces": 2,
"unit": "bytes/sec"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"kind": "StatChart",
"spec": {
"metricLabel": "version",
"textMode": "name",
"format": {
"unit": "decimal"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"kind": "StatChart",
"spec": {
"textMode": "auto",
"metricLabel": "version",
"format": {
"unit": "decimal"
Expand Down
1 change: 1 addition & 0 deletions statchart/schemas/migrate/tests/mappings/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"kind": "StatChart",
"spec": {
"calculation": "last",
"textMode": "auto",
"mappings": [
{
"kind": "Value",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"kind": "StatChart",
"spec": {
"calculation": "mean",
"textMode": "auto",
"format": {
"unit": "decimal"
},
Expand Down
1 change: 1 addition & 0 deletions statchart/schemas/stat.cue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
kind: "StatChart"
spec: close({
calculation: common.#calculation
textMode?: "auto" | "value" | "name" | "none" | "value_and_name"
metricLabel?: common.#metricLabel
format?: common.#format
thresholds?: common.#thresholds
Expand Down
3 changes: 2 additions & 1 deletion statchart/src/StatChartBase.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ describe('StatChart', () => {
};

const mockStatData: StatChartData = {
calculatedValue: 7.72931659687181,
numericValue: 7.72931659687181,
displayValue: 7.72931659687181,
color: '#1976d2',
seriesData: {
name: '(((count(count(node_cpu_seconds_total{job="example"}) by (cpu))',
Expand Down
40 changes: 28 additions & 12 deletions statchart/src/StatChartBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { EChart, FontSizeOption, GraphSeries, useChartsTheme } from '@perses-dev
import chroma from 'chroma-js';
import { useOptimalFontSize } from './utils/calculate-font-size';
import { formatStatChartValue } from './utils/format-stat-chart-value';
import { ColorMode } from './stat-chart-model';
import { ColorMode, TextMode } from './stat-chart-model';

use([EChartsLineChart, GridComponent, DatasetComponent, TitleComponent, TooltipComponent, CanvasRenderer]);

Expand All @@ -36,7 +36,9 @@ const BLACK_COLOR_CODE = '#000000';

export interface StatChartData {
color: string;
calculatedValue?: string | number | null;
numericValue?: number | null;
displayValue?: string | number | null;
displayName?: string;
seriesData?: GraphSeries;
}

Expand All @@ -46,9 +48,11 @@ export interface StatChartProps {
data: StatChartData;
format?: FormatOptions;
sparkline?: LineSeriesOption;
showSeriesName?: boolean;
valueFontSize?: FontSizeOption;
colorMode?: ColorMode;
textMode?: TextMode;
isMultiSeries?: boolean;
legendMode?: 'auto' | 'on' | 'off';
}

export const StatChartBase: FC<StatChartProps> = (props) => {
Expand All @@ -58,10 +62,12 @@ export const StatChartBase: FC<StatChartProps> = (props) => {
data,
data: { color },
sparkline,
showSeriesName,
format,
valueFontSize,
colorMode,
textMode = 'auto',
isMultiSeries = false,
legendMode = 'auto',
} = props;

const {
Expand All @@ -71,20 +77,30 @@ export const StatChartBase: FC<StatChartProps> = (props) => {
},
} = useTheme();
const chartsTheme = useChartsTheme();
const formattedValue = formatStatChartValue(data.calculatedValue, format);
const formattedValue = formatStatChartValue(data.displayValue, format);
const containerPadding = chartsTheme.container.padding.default;

// calculate series name font size and height
// Determine if top text should be shown
// Respect explicit legendMode choice, otherwise let textMode decide
const shouldShowTopText = (() => {
if (legendMode === 'off') return false;
if (legendMode === 'on') return true;

// legendMode='auto': let textMode decide
return textMode === 'value_and_name' || (textMode === 'auto' && isMultiSeries);
})();

// calculate top text font size and height
let seriesNameFontSize = useOptimalFontSize({
text: data?.seriesData?.name ?? '',
text: data.displayName ?? '',
fontWeight: SERIES_NAME_FONT_WEIGHT,
width,
height: height * 0.125, // assume series name will take 12.5% of available height
lineHeight: LINE_HEIGHT,
maxSize: SERIES_NAME_MAX_FONT_SIZE,
});

const seriesNameHeight = showSeriesName ? seriesNameFontSize * LINE_HEIGHT + containerPadding : 0;
const seriesNameHeight = shouldShowTopText ? seriesNameFontSize * LINE_HEIGHT + containerPadding : 0;

// calculate value font size and height
const availableWidth = width - containerPadding * 2;
Expand Down Expand Up @@ -199,7 +215,7 @@ export const StatChartBase: FC<StatChartProps> = (props) => {
}, [colorMode, containerPadding, optimalValueFontSize, formattedValue, color, paletteMode]);

const seriesName = useMemo((): ReactNode | null => {
if (!showSeriesName) return null;
if (!shouldShowTopText || !data.displayName) return null;

let textColor = '';

Expand All @@ -219,10 +235,10 @@ export const StatChartBase: FC<StatChartProps> = (props) => {

return (
<SeriesName padding={containerPadding} fontSize={seriesNameFontSize} color={textColor}>
{data.seriesData?.name}
{data.displayName}
</SeriesName>
);
}, [colorMode, showSeriesName, secondary, color, containerPadding, seriesNameFontSize, data?.seriesData?.name]);
}, [colorMode, shouldShowTopText, secondary, color, containerPadding, seriesNameFontSize, data.displayName]);

return (
<Box
Expand All @@ -237,7 +253,7 @@ export const StatChartBase: FC<StatChartProps> = (props) => {
}}
>
{seriesName}
{styledFormattedValue}
{data.displayValue !== undefined && textMode !== 'none' && styledFormattedValue}
{sparkline && (
<EChart
sx={{
Expand Down
32 changes: 32 additions & 0 deletions statchart/src/StatChartOptionsEditorSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
ShowLegendLabelItem,
StatChartOptions,
StatChartOptionsEditorProps,
TEXT_MODE_LABELS,
TextModeLabelItem,
} from './stat-chart-model';

const DEFAULT_FORMAT: FormatOptions = { unit: 'percent-decimal' };
Expand Down Expand Up @@ -80,6 +82,17 @@ export function StatChartOptionsEditorSettings(props: StatChartOptionsEditorProp
[onChange, value]
);

const handleTextModeChange = useCallback(
(_: unknown, newTextMode: TextModeLabelItem): void => {
onChange(
produce(value, (draft: StatChartOptions) => {
draft.textMode = newTextMode.id;
})
);
},
[onChange, value]
);

const handleUnitChange: FormatControlsProps['onChange'] = (newFormat) => {
onChange(
produce(value, (draft: StatChartOptions) => {
Expand Down Expand Up @@ -164,6 +177,24 @@ export function StatChartOptionsEditorSettings(props: StatChartOptionsEditorProp
);
}, [value.colorMode, handleColorModeChange]);

const selectTextMode = useMemo((): ReactElement => {
return (
<OptionsEditorControl
label="Text mode"
control={
<SettingsAutocomplete
onChange={handleTextModeChange}
options={TEXT_MODE_LABELS}
disableClearable
value={
TEXT_MODE_LABELS.find((i) => i.id === value.textMode) ?? TEXT_MODE_LABELS.find((i) => i.id === 'auto')!
}
/>
}
/>
);
}, [value.textMode, handleTextModeChange]);

return (
<OptionsEditorGrid>
<OptionsEditorColumn>
Expand All @@ -175,6 +206,7 @@ export function StatChartOptionsEditorSettings(props: StatChartOptionsEditorProp
/>
<FormatControls value={format} onChange={handleUnitChange} />
<CalculationSelector value={value.calculation} onChange={handleCalculationChange} />
{selectTextMode}
<MetricLabelInput value={value.metricLabel} onChange={handleMetricLabelChange} />
<FontSizeSelector value={value.valueFontSize} onChange={handleFontSizeChange} />
{selectColorMode}
Expand Down
Loading