Merge branch 'fastfrwrd/112977-sparkline-annotations' into fastfrwrd/gauge-bug-bash

This commit is contained in:
Paul Marbach
2025-12-18 16:05:50 -05:00
4 changed files with 129 additions and 76 deletions
+81 -56
View File
@@ -3,7 +3,7 @@ import { isEmpty } from 'lodash';
import { DataFrameView } from '../dataframe/DataFrameView';
import { getTimeField } from '../dataframe/processDataFrame';
import { GrafanaTheme2 } from '../themes/types';
import { reduceField, ReducerID } from '../transformations/fieldReducer';
import { isReducerID, reduceField, ReducerID } from '../transformations/fieldReducer';
import { getFieldMatcher } from '../transformations/matchers';
import { FieldMatcherID } from '../transformations/matchers/ids';
import { ScopedVars } from '../types/ScopedVars';
@@ -43,6 +43,7 @@ export interface FieldSparkline {
x?: Field; // if this does not exist, use the index
timeRange?: TimeRange; // Optionally force an absolute time
highlightIndex?: number;
highlightLine?: number;
}
export interface FieldDisplay {
@@ -72,6 +73,76 @@ export interface GetFieldDisplayValuesOptions {
export const DEFAULT_FIELD_DISPLAY_VALUES_LIMIT = 25;
interface SparklineHighlightPoint {
type: 'point';
xIdx: number;
}
interface SparklineHighlightLine {
type: 'line';
y: number;
}
export function getSparklineHighlight(
sparkline: FieldSparkline,
calc: ReducerID
): SparklineHighlightPoint | SparklineHighlightLine | void {
switch (calc) {
case ReducerID.last:
return { type: 'point', xIdx: sparkline.y.values.length - 1 };
case ReducerID.first:
return { type: 'point', xIdx: 0 };
case ReducerID.lastNotNull: {
for (let k = sparkline.y.values.length - 1; k >= 0; k--) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v)) {
return { type: 'point', xIdx: k };
}
}
return;
}
case ReducerID.firstNotNull: {
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v)) {
return { type: 'point', xIdx: k };
}
}
return;
}
case ReducerID.min: {
let minIdx = -1;
let prevMin = Infinity;
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) {
prevMin = v;
minIdx = k;
}
}
return minIdx >= 0 ? { type: 'point', xIdx: minIdx } : undefined;
}
case ReducerID.max: {
let maxIdx = -1;
let prevMax = -Infinity;
for (let k = 0; k < sparkline.y.values.length; k++) {
const v = sparkline.y.values[k];
if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) {
prevMax = v;
maxIdx = k;
}
}
return maxIdx >= 0 ? { type: 'point', xIdx: maxIdx } : undefined;
}
case ReducerID.mean:
return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.mean] }).mean };
case ReducerID.median:
return { type: 'line', y: reduceField({ field: sparkline.y, reducers: [ReducerID.median] }).median };
default:
return;
}
}
export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): FieldDisplay[] => {
const { replaceVariables, reduceOptions, timeZone, theme } = options;
const calcs = reduceOptions.calcs.length ? reduceOptions.calcs : [ReducerID.last];
@@ -190,62 +261,16 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
y: dataFrame.fields[i],
x: timeField,
};
let highlightIdx: number | undefined = (() => {
switch (calc) {
case ReducerID.last:
return sparkline.y.values.length - 1;
case ReducerID.first:
return 0;
// TODO: #112977 enable more reducers for highlight index
// case ReducerID.lastNotNull: {
// for (let k = sparkline.y.values.length - 1; k >= 0; k--) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v)) {
// return k;
// }
// }
// return;
// }
// case ReducerID.firstNotNull: {
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v)) {
// return k;
// }
// }
// return;
// }
// case ReducerID.min: {
// let minIdx = -1;
// let prevMin = Infinity;
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v) && v < prevMin) {
// prevMin = v;
// minIdx = k;
// }
// }
// return minIdx >= 0 ? minIdx : undefined;
// }
// case ReducerID.max: {
// let maxIdx = -1;
// let prevMax = -Infinity;
// for (let k = 0; k < sparkline.y.values.length; k++) {
// const v = sparkline.y.values[k];
// if (v !== null && v !== undefined && !Number.isNaN(v) && v > prevMax) {
// prevMax = v;
// maxIdx = k;
// }
// }
// return maxIdx >= 0 ? maxIdx : undefined;
// }
default:
return;
if (isReducerID(calc)) {
const sparklineHighlight = getSparklineHighlight(sparkline, calc);
switch (sparklineHighlight?.type) {
case 'point':
sparkline.highlightIndex = sparklineHighlight.xIdx;
break;
case 'line':
sparkline.highlightLine = sparklineHighlight.y;
break;
}
})();
if (typeof highlightIdx === 'number') {
sparkline.highlightIndex = highlightIdx;
}
}
@@ -67,7 +67,7 @@ export const RadialSparkline = memo(
return (
<div style={{ position: 'absolute', top: topPos }}>
<Sparkline height={height} width={width} sparkline={sparkline} theme={theme} config={config} />
<Sparkline height={height} width={width} sparkline={sparkline} theme={theme} config={config} showHighlights />
</div>
);
}
@@ -14,18 +14,18 @@ export interface SparklineProps extends Themeable2 {
height: number;
config?: FieldConfig<GraphFieldConfig>;
sparkline: FieldSparkline;
showHighlights?: boolean;
}
const SparklineFn: React.FC<SparklineProps> = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig);
export const SparklineFn: React.FC<SparklineProps> = memo((props) => {
const { sparkline, config: fieldConfig, theme, width, height, showHighlights } = props;
const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, theme, fieldConfig, showHighlights);
if (warning) {
return null;
}
const data = preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame));
const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme);
const configBuilder = prepareConfig(sparkline, alignedDataFrame, theme, showHighlights);
return <UPlotChart data={data} config={configBuilder} width={width} height={height} />;
});
@@ -2,6 +2,7 @@ import { Range } from 'uplot';
import {
applyNullInsertThreshold,
// colorManipulator,
DataFrame,
FieldConfig,
FieldSparkline,
@@ -22,6 +23,7 @@ import {
VisibilityMode,
ScaleDirection,
ScaleOrientation,
// FieldColorModeId,
} from '@grafana/schema';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
@@ -112,8 +114,7 @@ export function getYRange(alignedFrame: DataFrame): Range.MinMax {
return [roundedMin, roundedMax];
}
// TODO: #112977 enable highlight index
// const HIGHLIGHT_IDX_POINT_SIZE = 6;
const HIGHLIGHT_IDX_POINT_SIZE = 6;
const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
@@ -124,7 +125,9 @@ const defaultConfig: GraphFieldConfig = {
export const prepareSeries = (
sparkline: FieldSparkline,
fieldConfig?: FieldConfig<GraphFieldConfig>
_theme: GrafanaTheme2,
fieldConfig?: FieldConfig<GraphFieldConfig>,
_showHighlights?: boolean
): { frame: DataFrame; warning?: string } => {
const frame = nullToValue(preparePlotFrame(sparkline, fieldConfig));
if (frame.fields.some((f) => f.values.length <= 1)) {
@@ -136,16 +139,41 @@ export const prepareSeries = (
frame,
};
}
// TODO:rgb(24, 24, 24) will address this.
// if (showHighlights && typeof sparkline.highlightLine === 'number') {
// const highlightY = sparkline.highlightLine;
// const colorMode = getFieldColorModeForField(sparkline.y);
// const seriesColor = colorMode.getCalculator(sparkline.y, theme)(highlightY, 0);
// frame.fields.push({
// name: 'highlightLine',
// type: FieldType.number,
// values: new Array(frame.length).fill(highlightY),
// config: {
// color: {
// mode: FieldColorModeId.Fixed,
// fixedColor: colorManipulator.lighten(seriesColor, 0.5),
// },
// custom: {
// lineStyle: {
// fill: 'dash',
// dash: [5, 2],
// },
// },
// },
// state: {},
// });
// }
return { frame };
};
export const prepareConfig = (
sparkline: FieldSparkline,
dataFrame: DataFrame,
theme: GrafanaTheme2
theme: GrafanaTheme2,
showHighlights?: boolean
): UPlotConfigBuilder => {
const builder = new UPlotConfigBuilder();
// const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2;
const rangePad = HIGHLIGHT_IDX_POINT_SIZE / 2;
builder.setCursor({
show: false,
@@ -206,13 +234,14 @@ export const prepareConfig = (
const colorMode = getFieldColorModeForField(field);
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
// TODO: #112977 enable highlight index and adjust padding accordingly
// const hasHighlightIndex = typeof sparkline.highlightIndex === 'number';
// if (hasHighlightIndex) {
// builder.setPadding([rangePad, rangePad, rangePad, rangePad]);
// }
const hasHighlightIndex = showHighlights && typeof sparkline.highlightIndex === 'number';
if (hasHighlightIndex) {
builder.setPadding([rangePad, rangePad, rangePad, rangePad]);
}
const pointsMode =
customConfig.drawStyle === GraphDrawStyle.Points // || hasHighlightIndex
customConfig.drawStyle === GraphDrawStyle.Points || hasHighlightIndex
? VisibilityMode.Always
: customConfig.showPoints;
@@ -227,9 +256,8 @@ export const prepareConfig = (
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
showPoints: pointsMode,
// TODO: #112977 enable highlight index
pointSize: /* hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : */ customConfig.pointSize,
// pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined,
pointSize: hasHighlightIndex ? HIGHLIGHT_IDX_POINT_SIZE : customConfig.pointSize,
pointsFilter: hasHighlightIndex ? [sparkline.highlightIndex!] : undefined,
fillOpacity: customConfig.fillOpacity,
fillColor: customConfig.fillColor,
lineStyle: customConfig.lineStyle,