Trend/TimeSeries: Add "Show values" option (#108090)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Hasith De Alwis 2025-08-23 08:33:45 -04:00 committed by GitHub
parent 67a6866c7b
commit 9fb5790166
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 89 additions and 0 deletions

View File

@ -57,6 +57,7 @@
"type": "linear" "type": "linear"
}, },
"showPoints": "auto", "showPoints": "auto",
"showValues": true,
"spanNulls": false, "spanNulls": false,
"stacking": { "stacking": {
"group": "A", "group": "A",

View File

@ -635,6 +635,7 @@ export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig,
drawStyle?: GraphDrawStyle; drawStyle?: GraphDrawStyle;
gradientMode?: GraphGradientMode; gradientMode?: GraphGradientMode;
insertNulls?: (boolean | number); insertNulls?: (boolean | number);
showValues?: boolean;
thresholdsStyle?: GraphThresholdsStyleConfig; thresholdsStyle?: GraphThresholdsStyleConfig;
transform?: GraphTransform; transform?: GraphTransform;
} }

View File

@ -229,6 +229,7 @@ GraphFieldConfig: {
gradientMode?: GraphGradientMode gradientMode?: GraphGradientMode
thresholdsStyle?: GraphThresholdsStyleConfig thresholdsStyle?: GraphThresholdsStyleConfig
transform?: GraphTransform transform?: GraphTransform
showValues?: bool
insertNulls?: bool | number insertNulls?: bool | number
} @cuetsy(kind="interface") } @cuetsy(kind="interface")

View File

@ -49,6 +49,7 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo
dataFrameFieldIndex?: DataFrameFieldIndex; dataFrameFieldIndex?: DataFrameFieldIndex;
theme: GrafanaTheme2; theme: GrafanaTheme2;
value?: uPlot.Series.Value; value?: uPlot.Series.Value;
showValues?: boolean;
} }
export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {

View File

@ -59,6 +59,7 @@ for (let i = 0; i < BIN_INCRS.length; i++) {
BIN_INCRS[i] = 2 ** i; BIN_INCRS[i] = 2 ** i;
} }
import { DrawStyle } from '@grafana/ui';
import { import {
UPlotConfigBuilder, UPlotConfigBuilder,
UPlotConfigPrepFn, UPlotConfigPrepFn,
@ -74,6 +75,7 @@ const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line, drawStyle: GraphDrawStyle.Line,
showPoints: VisibilityMode.Auto, showPoints: VisibilityMode.Auto,
axisPlacement: AxisPlacement.Auto, axisPlacement: AxisPlacement.Auto,
showValues: false,
}; };
export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
@ -529,6 +531,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
softMax: customConfig.axisSoftMax, softMax: customConfig.axisSoftMax,
// The following properties are not used in the uPlot config, but are utilized as transport for legend config // The following properties are not used in the uPlot config, but are utilized as transport for legend config
dataFrameFieldIndex: field.state?.origin, dataFrameFieldIndex: field.state?.origin,
showValues: customConfig.showValues,
}); });
// Render thresholds in graph // Render thresholds in graph
@ -553,6 +556,79 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
builder.setStackingGroups(stackingGroups); 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 // hook up custom/composite renderers
renderers?.forEach((r) => { renderers?.forEach((r) => {
if (!indexByName) { if (!indexByName) {

View File

@ -41,6 +41,7 @@ export const defaultGraphConfig: GraphFieldConfig = {
axisGridShow: true, axisGridShow: true,
axisCenteredZero: false, axisCenteredZero: false,
axisBorderShow: false, axisBorderShow: false,
showValues: false,
}; };
export type NullEditorSettings = { isTime: boolean }; export type NullEditorSettings = { isTime: boolean };
@ -208,6 +209,13 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig, isTime = true): SetFi
}, },
showIf: (config) => config.drawStyle !== GraphDrawStyle.Points, 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({ .addSliderInput({
path: 'pointSize', path: 'pointSize',
name: t('timeseries.config.get-graph-field-config.name-point-size', 'Point size'), name: t('timeseries.config.get-graph-field-config.name-point-size', 'Point size'),

View File

@ -13059,6 +13059,7 @@
"name-point-size": "Point size", "name-point-size": "Point size",
"name-show-points": "Show points", "name-show-points": "Show points",
"name-show-thresholds": "Show thresholds", "name-show-thresholds": "Show thresholds",
"name-show-values": "Show values",
"name-style": "Style", "name-style": "Style",
"name-transform": "Transform", "name-transform": "Transform",
"transform-options": { "transform-options": {