diff --git a/packages/grafana-data/src/field/fieldDisplay.test.ts b/packages/grafana-data/src/field/fieldDisplay.test.ts
index 718c0e54430..5ec3ed7ba4f 100644
--- a/packages/grafana-data/src/field/fieldDisplay.test.ts
+++ b/packages/grafana-data/src/field/fieldDisplay.test.ts
@@ -3,11 +3,18 @@ import { merge } from 'lodash';
import { toDataFrame } from '../dataframe/processDataFrame';
import { createTheme } from '../themes/createTheme';
import { ReducerID } from '../transformations/fieldReducer';
+import { FieldType } from '../types/dataFrame';
import { FieldConfigPropertyItem } from '../types/fieldOverrides';
import { MappingType, SpecialValueMatch, ValueMapping } from '../types/valueMapping';
import { getDisplayProcessor } from './displayProcessor';
-import { fixCellTemplateExpressions, getFieldDisplayValues, GetFieldDisplayValuesOptions } from './fieldDisplay';
+import {
+ FieldSparkline,
+ fixCellTemplateExpressions,
+ getFieldDisplayValues,
+ GetFieldDisplayValuesOptions,
+ getSparklineHighlight,
+} from './fieldDisplay';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
describe('FieldDisplay', () => {
@@ -556,3 +563,71 @@ describe('fixCellTemplateExpressions', () => {
);
});
});
+
+describe('getSparklineHighlight', () => {
+ const sparkline: FieldSparkline = {
+ y: { name: 'A', type: FieldType.number, values: [null, 2, 3, 4, 10, 8, 8, 8, 9, null], config: {} },
+ };
+
+ it.each([
+ {
+ calc: ReducerID.last,
+ expected: {
+ type: 'point',
+ xIdx: 9,
+ },
+ },
+ {
+ calc: ReducerID.max,
+ expected: {
+ type: 'point',
+ xIdx: 4,
+ },
+ },
+ {
+ calc: ReducerID.min,
+ expected: {
+ type: 'point',
+ xIdx: 1,
+ },
+ },
+ {
+ calc: ReducerID.first,
+ expected: {
+ type: 'point',
+ xIdx: 0,
+ },
+ },
+ {
+ calc: ReducerID.firstNotNull,
+ expected: {
+ type: 'point',
+ xIdx: 1,
+ },
+ },
+ {
+ calc: ReducerID.lastNotNull,
+ expected: {
+ type: 'point',
+ xIdx: 8,
+ },
+ },
+ {
+ calc: ReducerID.mean,
+ expected: {
+ type: 'line',
+ y: 6.5,
+ },
+ },
+ {
+ calc: ReducerID.median,
+ expected: {
+ type: 'line',
+ y: 8,
+ },
+ },
+ ])('it calculates the correct highlight for the $calc', ({ calc, expected }) => {
+ const result = getSparklineHighlight(sparkline, calc);
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts
index 3d82f571926..3496f419395 100644
--- a/packages/grafana-data/src/field/fieldDisplay.ts
+++ b/packages/grafana-data/src/field/fieldDisplay.ts
@@ -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;
}
}
diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx
index 2d6c45a14bf..4a52d5241d5 100644
--- a/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx
+++ b/packages/grafana-ui/src/components/RadialGauge/RadialSparkline.tsx
@@ -67,7 +67,7 @@ export const RadialSparkline = memo(
return (
-
+
);
}
diff --git a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
index c18b235e757..d1fb4f3b0e0 100644
--- a/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
+++ b/packages/grafana-ui/src/components/Sparkline/Sparkline.tsx
@@ -14,18 +14,18 @@ export interface SparklineProps extends Themeable2 {
height: number;
config?: FieldConfig;
sparkline: FieldSparkline;
+ showHighlights?: boolean;
}
-const SparklineFn: React.FC = memo((props) => {
- const { sparkline, config: fieldConfig, theme, width, height } = props;
-
- const { frame: alignedDataFrame, warning } = prepareSeries(sparkline, fieldConfig);
+export const SparklineFn: React.FC = 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 ;
});
diff --git a/packages/grafana-ui/src/components/Sparkline/utils.test.ts b/packages/grafana-ui/src/components/Sparkline/utils.test.ts
index ca49f6da512..0ec65515e0c 100644
--- a/packages/grafana-ui/src/components/Sparkline/utils.test.ts
+++ b/packages/grafana-ui/src/components/Sparkline/utils.test.ts
@@ -1,6 +1,6 @@
-import { Field, FieldSparkline, FieldType } from '@grafana/data';
+import { createTheme, Field, FieldSparkline, FieldType, toDataFrame } from '@grafana/data';
-import { getYRange, preparePlotFrame } from './utils';
+import { getYRange, prepareConfig, preparePlotFrame } from './utils';
describe('Prepare Sparkline plot frame', () => {
it('should return sorted array if x-axis numeric', () => {
@@ -201,3 +201,134 @@ describe('Get y range', () => {
expect(actual[0]).toBeLessThan(actual[1]!);
});
});
+
+describe('prepareConfig', () => {
+ it('should not throw an error if there are multiple values', () => {
+ const sparkline: FieldSparkline = {
+ x: {
+ name: 'x',
+ values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000],
+ type: FieldType.time,
+ config: {},
+ },
+ y: {
+ name: 'y',
+ values: [1, 2, 3, 4, 5],
+ type: FieldType.number,
+ config: {},
+ },
+ };
+
+ const dataFrame = toDataFrame({
+ fields: [sparkline.x, sparkline.y],
+ });
+
+ const config = prepareConfig(sparkline, dataFrame, createTheme());
+ expect(config.series.length).toBe(1);
+ });
+
+ it('should not throw an error if there is a single value', () => {
+ const sparkline: FieldSparkline = {
+ x: {
+ name: 'x',
+ values: [1679839200000],
+ type: FieldType.time,
+ config: {},
+ },
+ y: {
+ name: 'y',
+ values: [1],
+ type: FieldType.number,
+ config: {},
+ },
+ };
+
+ const dataFrame = toDataFrame({
+ fields: [sparkline.x, sparkline.y],
+ });
+
+ const config = prepareConfig(sparkline, dataFrame, createTheme());
+ expect(config.series.length).toBe(1);
+ });
+
+ it('should not throw an error if there are no values', () => {
+ const sparkline: FieldSparkline = {
+ x: {
+ name: 'x',
+ values: [],
+ type: FieldType.time,
+ config: {},
+ },
+ y: {
+ name: 'y',
+ values: [],
+ type: FieldType.number,
+ config: {},
+ },
+ };
+
+ const dataFrame = toDataFrame({
+ fields: [sparkline.x, sparkline.y],
+ });
+
+ const config = prepareConfig(sparkline, dataFrame, createTheme());
+ expect(config.series.length).toBe(1);
+ });
+
+ it('should set up highlight series if showHighlights is true and highlightIdx exists', () => {
+ const sparkline: FieldSparkline = {
+ x: {
+ name: 'x',
+ values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000],
+ type: FieldType.time,
+ config: {},
+ },
+ y: {
+ name: 'y',
+ values: [1, 2, 3, 4, 5],
+ type: FieldType.number,
+ config: {},
+ },
+ highlightIndex: 2,
+ };
+
+ const dataFrame = toDataFrame({
+ fields: [sparkline.x, sparkline.y],
+ });
+
+ const config = prepareConfig(sparkline, dataFrame, createTheme(), true);
+ expect(config.series.length).toBe(1);
+ expect(config.series[0].getConfig().points).toEqual(
+ expect.objectContaining({
+ show: true,
+ filter: [2],
+ })
+ );
+ });
+
+ it('should not set up highlight series if showHighlights is false even if highlightIdx exists', () => {
+ const sparkline: FieldSparkline = {
+ x: {
+ name: 'x',
+ values: [1679839200000, 1680444000000, 1681048800000, 1681653600000, 1682258400000],
+ type: FieldType.time,
+ config: {},
+ },
+ y: {
+ name: 'y',
+ values: [1, 2, 3, 4, 5],
+ type: FieldType.number,
+ config: {},
+ },
+ highlightIndex: 2,
+ };
+
+ const dataFrame = toDataFrame({
+ fields: [sparkline.x, sparkline.y],
+ });
+
+ const config = prepareConfig(sparkline, dataFrame, createTheme(), false);
+ expect(config.series.length).toBe(1);
+ expect(config.series[0].getConfig().points?.show).not.toBe(true);
+ });
+});
diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts
index be24eb6c4e8..c1402c4da2d 100644
--- a/packages/grafana-ui/src/components/Sparkline/utils.ts
+++ b/packages/grafana-ui/src/components/Sparkline/utils.ts
@@ -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
+ _theme: GrafanaTheme2,
+ fieldConfig?: FieldConfig,
+ _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,