From 9fb5790166b3394058f776f5c150b79f1d5be3c5 Mon Sep 17 00:00:00 2001 From: Hasith De Alwis <83139626+HasithDeAlwis@users.noreply.github.com> Date: Sat, 23 Aug 2025 08:33:45 -0400 Subject: [PATCH] Trend/TimeSeries: Add "Show values" option (#108090) Co-authored-by: Leon Sorokin --- .../panel-trend/trend_example.json | 1 + .../grafana-schema/src/common/common.gen.ts | 1 + .../grafana-schema/src/common/mudball.cue | 1 + .../uPlot/config/UPlotSeriesBuilder.ts | 1 + .../app/core/components/TimeSeries/utils.ts | 76 +++++++++++++++++++ public/app/plugins/panel/timeseries/config.ts | 8 ++ public/locales/en-US/grafana.json | 1 + 7 files changed, 89 insertions(+) diff --git a/devenv/dev-dashboards/panel-trend/trend_example.json b/devenv/dev-dashboards/panel-trend/trend_example.json index 9961b07fc92..2f16489573d 100644 --- a/devenv/dev-dashboards/panel-trend/trend_example.json +++ b/devenv/dev-dashboards/panel-trend/trend_example.json @@ -57,6 +57,7 @@ "type": "linear" }, "showPoints": "auto", + "showValues": true, "spanNulls": false, "stacking": { "group": "A", diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index 1931a6ad967..53467e1f0b2 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -635,6 +635,7 @@ export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, drawStyle?: GraphDrawStyle; gradientMode?: GraphGradientMode; insertNulls?: (boolean | number); + showValues?: boolean; thresholdsStyle?: GraphThresholdsStyleConfig; transform?: GraphTransform; } diff --git a/packages/grafana-schema/src/common/mudball.cue b/packages/grafana-schema/src/common/mudball.cue index ed58696267e..24c371d8715 100644 --- a/packages/grafana-schema/src/common/mudball.cue +++ b/packages/grafana-schema/src/common/mudball.cue @@ -229,6 +229,7 @@ GraphFieldConfig: { gradientMode?: GraphGradientMode thresholdsStyle?: GraphThresholdsStyleConfig transform?: GraphTransform + showValues?: bool insertNulls?: bool | number } @cuetsy(kind="interface") diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index 62e651f919b..0889b3a9b4b 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -49,6 +49,7 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo dataFrameFieldIndex?: DataFrameFieldIndex; theme: GrafanaTheme2; value?: uPlot.Series.Value; + showValues?: boolean; } export class UPlotSeriesBuilder extends PlotConfigBuilder { diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 407bd92ed66..f87801d5955 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -59,6 +59,7 @@ for (let i = 0; i < BIN_INCRS.length; i++) { BIN_INCRS[i] = 2 ** i; } +import { DrawStyle } from '@grafana/ui'; import { UPlotConfigBuilder, UPlotConfigPrepFn, @@ -74,6 +75,7 @@ const defaultConfig: GraphFieldConfig = { drawStyle: GraphDrawStyle.Line, showPoints: VisibilityMode.Auto, axisPlacement: AxisPlacement.Auto, + showValues: false, }; export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ @@ -529,6 +531,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ softMax: customConfig.axisSoftMax, // The following properties are not used in the uPlot config, but are utilized as transport for legend config dataFrameFieldIndex: field.state?.origin, + showValues: customConfig.showValues, }); // Render thresholds in graph @@ -553,6 +556,79 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ builder.setStackingGroups(stackingGroups); + const mightShowValues = frame.fields.some((field, i) => { + if (i === 0) { + return false; + } + + const customConfig = field.config.custom ?? {}; + + return ( + customConfig.showValues && + (customConfig.drawStyle === GraphDrawStyle.Points || customConfig.showPoints !== VisibilityMode.Never) + ); + }); + + if (mightShowValues) { + // since bars style doesnt show points in Auto mode, we can't piggyback on series.points.show() + // so we make a simple density-based callback to use here + const barsShowValues = (u: uPlot) => { + let width = u.bbox.width / uPlot.pxRatio; + let count = u.data[0].length; + + // render values when each has at least 30px of width available + return width / count >= 30; + }; + + builder.addHook('draw', (u: uPlot) => { + const baseFontSize = 12; + const font = `${baseFontSize * uPlot.pxRatio}px ${theme.typography.fontFamily}`; + + const { ctx } = u; + + ctx.save(); + ctx.fillStyle = theme.colors.text.primary; + ctx.font = font; + ctx.textAlign = 'center'; + + for (let seriesIdx = 1; seriesIdx < u.data.length; seriesIdx++) { + const series = u.series[seriesIdx]; + const field = frame.fields[seriesIdx]; + + if ( + field.config.custom?.showValues && + // @ts-ignore points.show() is always callable on the instance (but may be boolean when passed to uPlot as init option) + (series.points?.show?.(u, seriesIdx) || + (field.config.custom?.drawStyle === DrawStyle.Bars && barsShowValues(u))) + ) { + const xData = u.data[0]; + const yData = u.data[seriesIdx]; + const yScale = series.scale!; + + for (let dataIdx = 0; dataIdx < yData.length; dataIdx++) { + const yVal = yData[dataIdx]; + + if (yVal != null) { + const text = formattedValueToString(field.display!(yVal)); + + const isNegative = yVal < 0; + const textOffset = isNegative ? 15 : -5; + ctx.textBaseline = isNegative ? 'top' : 'bottom'; + + const xVal = xData[dataIdx]; + const x = u.valToPos(xVal, 'x', true); + const y = u.valToPos(yVal, yScale, true); + + ctx.fillText(text, x, y + textOffset); + } + } + } + } + + ctx.restore(); + }); + } + // hook up custom/composite renderers renderers?.forEach((r) => { if (!indexByName) { diff --git a/public/app/plugins/panel/timeseries/config.ts b/public/app/plugins/panel/timeseries/config.ts index 1ad68d0584c..e88fc5b94a2 100644 --- a/public/app/plugins/panel/timeseries/config.ts +++ b/public/app/plugins/panel/timeseries/config.ts @@ -41,6 +41,7 @@ export const defaultGraphConfig: GraphFieldConfig = { axisGridShow: true, axisCenteredZero: false, axisBorderShow: false, + showValues: false, }; export type NullEditorSettings = { isTime: boolean }; @@ -208,6 +209,13 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig, isTime = true): SetFi }, showIf: (config) => config.drawStyle !== GraphDrawStyle.Points, }) + .addBooleanSwitch({ + path: 'showValues', + name: t('timeseries.config.get-graph-field-config.name-show-values', 'Show values'), + category: categoryStyles, + defaultValue: false, + showIf: (config) => config.showPoints !== VisibilityMode.Never || config.drawStyle === GraphDrawStyle.Points, + }) .addSliderInput({ path: 'pointSize', name: t('timeseries.config.get-graph-field-config.name-point-size', 'Point size'), diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index f4290d1df1d..c088579bcb7 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13059,6 +13059,7 @@ "name-point-size": "Point size", "name-show-points": "Show points", "name-show-thresholds": "Show thresholds", + "name-show-values": "Show values", "name-style": "Style", "name-transform": "Transform", "transform-options": {