From c6e27e00b4f1320c06bf1bfb8e7a631c46cb1ba8 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 1 Nov 2023 21:59:55 -0700 Subject: [PATCH] Chore: Move internal GraphNG+Timeseries components into core (#77525) * move to core where possible * update imports * ignore import order for now * add graveyard files * update codeowners --- .betterer.results | 99 ++- .github/CODEOWNERS | 6 +- .../grafana-data/src/transformations/index.ts | 3 + .../nulls}/nullInsertThreshold.test.ts | 0 .../transformers/nulls/nullInsertThreshold.ts | 186 +++++ .../nulls}/nullToUndefThreshold.ts | 0 .../transformers/nulls}/nullToValue.test.ts | 0 .../transformers/nulls/nullToValue.ts | 27 + .../src/components/Sparkline/utils.ts | 3 +- packages/grafana-ui/src/components/index.ts | 13 +- .../src/components/uPlot/internal.ts | 40 ++ .../grafana-ui/src/components/uPlot/types.ts | 5 + .../src/components/uPlot/utils.test.ts | 2 +- .../grafana-ui/src/components/uPlot/utils.ts | 3 +- .../src/graveyard/GraphNG/GraphNG.tsx | 275 +++++++ .../GraphNG/__snapshots__/utils.test.ts.snap | 0 .../grafana-ui/src/graveyard/GraphNG/hooks.ts | 41 ++ .../GraphNG/nullInsertThreshold.test.ts | 334 +++++++++ .../GraphNG/nullInsertThreshold.ts | 1 + .../graveyard/GraphNG/nullToUndefThreshold.ts | 33 + .../src/graveyard/GraphNG/nullToValue.test.ts | 94 +++ .../GraphNG/nullToValue.ts | 1 + .../grafana-ui/src/graveyard/GraphNG/types.ts | 18 + .../GraphNG/utils.test.ts | 0 .../GraphNG/utils.ts | 3 +- packages/grafana-ui/src/graveyard/README.md | 1 + .../TimeSeries/TimeSeries.tsx | 6 +- .../TimeSeries/utils.test.ts | 0 .../TimeSeries/utils.ts | 6 +- .../app/core}/components/GraphNG/GraphNG.tsx | 20 +- .../GraphNG/__snapshots__/utils.test.ts.snap | 245 +++++++ .../app/core}/components/GraphNG/hooks.ts | 0 .../app/core}/components/GraphNG/types.ts | 3 +- .../app/core/components/GraphNG/utils.test.ts | 522 ++++++++++++++ public/app/core/components/GraphNG/utils.ts | 140 ++++ .../core/components/TimeSeries/TimeSeries.tsx | 63 ++ .../core/components/TimeSeries/utils.test.ts | 274 +++++++ .../app/core/components/TimeSeries/utils.ts | 669 ++++++++++++++++++ .../TimelineChart/TimelineChart.tsx | 13 +- .../core/components/TimelineChart/timeline.ts | 2 +- .../core/components/TimelineChart/utils.ts | 4 +- .../plugins/panel/barchart/BarChartPanel.tsx | 4 +- .../panel/candlestick/CandlestickPanel.tsx | 3 +- .../app/plugins/panel/graph/data_processor.ts | 2 +- .../panel/timeseries/TimeSeriesPanel.tsx | 3 +- .../timeseries/plugins/ExemplarsPlugin.tsx | 2 +- .../plugins/ThresholdControlsPlugin.tsx | 3 +- public/app/plugins/panel/timeseries/utils.ts | 6 +- public/app/plugins/panel/trend/TrendPanel.tsx | 13 +- public/app/plugins/panel/xychart/dims.ts | 2 +- 50 files changed, 3091 insertions(+), 102 deletions(-) rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullInsertThreshold.test.ts (100%) create mode 100644 packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullToUndefThreshold.ts (100%) rename packages/{grafana-ui/src/components/GraphNG => grafana-data/src/transformations/transformers/nulls}/nullToValue.test.ts (100%) create mode 100644 packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts create mode 100644 packages/grafana-ui/src/components/uPlot/internal.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx rename packages/grafana-ui/src/{components => graveyard}/GraphNG/__snapshots__/utils.test.ts.snap (100%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/hooks.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/nullInsertThreshold.ts (99%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/nullToValue.ts (96%) create mode 100644 packages/grafana-ui/src/graveyard/GraphNG/types.ts rename packages/grafana-ui/src/{components => graveyard}/GraphNG/utils.test.ts (100%) rename packages/grafana-ui/src/{components => graveyard}/GraphNG/utils.ts (98%) create mode 100644 packages/grafana-ui/src/graveyard/README.md rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/TimeSeries.tsx (87%) rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/utils.test.ts (100%) rename packages/grafana-ui/src/{components => graveyard}/TimeSeries/utils.ts (98%) rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/GraphNG.tsx (92%) create mode 100644 public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/hooks.ts (100%) rename {packages/grafana-ui/src => public/app/core}/components/GraphNG/types.ts (85%) create mode 100644 public/app/core/components/GraphNG/utils.test.ts create mode 100644 public/app/core/components/GraphNG/utils.ts create mode 100644 public/app/core/components/TimeSeries/TimeSeries.tsx create mode 100644 public/app/core/components/TimeSeries/utils.test.ts create mode 100644 public/app/core/components/TimeSeries/utils.ts diff --git a/.betterer.results b/.betterer.results index d9446d6b110..27b2fd82df3 100644 --- a/.betterer.results +++ b/.betterer.results @@ -256,6 +256,17 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], + "packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + ], + "packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] + ], "packages/grafana-data/src/transformations/transformers/reduce.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] @@ -802,34 +813,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Graph/utils.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], - "packages/grafana-ui/src/components/GraphNG/GraphNG.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Do not use any type assertions.", "5"], - [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Unexpected any. Specify a different type.", "8"], - [0, 0, 0, "Do not use any type assertions.", "9"], - [0, 0, 0, "Do not use any type assertions.", "10"], - [0, 0, 0, "Do not use any type assertions.", "11"] - ], - "packages/grafana-ui/src/components/GraphNG/hooks.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"] - ], - "packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], - "packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] - ], "packages/grafana-ui/src/components/InfoBox/InfoBox.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], @@ -981,11 +964,6 @@ exports[`better eslint`] = { "packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], - "packages/grafana-ui/src/components/TimeSeries/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx:5381": [ [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] @@ -1032,6 +1010,39 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "1"] ], + "packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"] + ], + "packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"] + ], + "packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "packages/grafana-ui/src/options/builder/axis.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], @@ -1177,6 +1188,23 @@ exports[`better eslint`] = { "public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"] ], + "public/app/core/components/GraphNG/GraphNG.tsx:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Unexpected any. Specify a different type.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"], + [0, 0, 0, "Do not use any type assertions.", "5"], + [0, 0, 0, "Do not use any type assertions.", "6"], + [0, 0, 0, "Do not use any type assertions.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Do not use any type assertions.", "9"], + [0, 0, 0, "Do not use any type assertions.", "10"], + [0, 0, 0, "Do not use any type assertions.", "11"] + ], + "public/app/core/components/GraphNG/hooks.ts:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/core/components/Layers/LayerDragDropList.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], @@ -1411,6 +1439,11 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "1"], [0, 0, 0, "Styles should be written using objects.", "2"] ], + "public/app/core/components/TimeSeries/utils.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] + ], "public/app/core/components/TraceToLogs/TagMappingInput.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 81b5d3e891b..fbbf20a345b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -312,9 +312,7 @@ /packages/grafana-ui/src/components/Table/ @grafana/grafana-bi-squad /packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad /packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/GraphNG/ @grafana/dataviz-squad /packages/grafana-ui/src/components/Graph/ @grafana/dataviz-squad -/packages/grafana-ui/src/components/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad /packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad /packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad @@ -322,6 +320,8 @@ /packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad /packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad +/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad +/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend /packages/grafana-data/src/transformations/ @grafana/grafana-bi-squad /packages/grafana-data/src/**/*logs* @grafana/observability-logs @@ -366,6 +366,8 @@ cypress.config.js @grafana/grafana-frontend-platform /public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform /public/app/core/components/Layers/ @grafana/dataviz-squad /public/app/core/components/TraceToLogs @grafana/observability-traces-and-profiling +/public/app/core/components/GraphNG/ @grafana/dataviz-squad +/public/app/core/components/TimeSeries/ @grafana/dataviz-squad /public/app/features/all.ts @grafana/grafana-frontend-platform /public/app/features/admin/ @grafana/grafana-authnz-team /public/app/features/auth-config/ @grafana/grafana-authnz-team diff --git a/packages/grafana-data/src/transformations/index.ts b/packages/grafana-data/src/transformations/index.ts index 1fd3229f897..4cdd4ea2f8b 100644 --- a/packages/grafana-data/src/transformations/index.ts +++ b/packages/grafana-data/src/transformations/index.ts @@ -20,3 +20,6 @@ export type { RenameByRegexTransformerOptions } from './transformers/renameByReg export { joinDataFrames as outerJoinDataFrames, isLikelyAscendingVector } from './transformers/joinDataFrames'; export * from './transformers/histogram'; export { ensureTimeField } from './transformers/convertFieldType'; + +// Required for Sparklines util to work in @grafana/data, but ideally kept internal +export { applyNullInsertThreshold } from './transformers/nulls/nullInsertThreshold'; diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.test.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.test.ts diff --git a/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts new file mode 100644 index 00000000000..94d8dd2e3c0 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts @@ -0,0 +1,186 @@ +import { DataFrame, FieldType } from '../../../types'; + +type InsertMode = (prev: number, next: number, threshold: number) => number; + +const INSERT_MODES = { + threshold: (prev: number, next: number, threshold: number) => prev + threshold, + midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2, + // previous time + 1ms to prevent StateTimeline from forward-interpolating prior state + plusone: (prev: number, next: number, threshold: number) => prev + 1, +}; + +interface NullInsertOptions { + frame: DataFrame; + refFieldName?: string | null; + refFieldPseudoMax?: number; + refFieldPseudoMin?: number; + insertMode?: InsertMode; +} + +function getRefField(frame: DataFrame, refFieldName?: string | null) { + return frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); +} + +/** @internal */ +export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { + if (opts.frame.length === 0) { + return opts.frame; + } + + let thorough = true; + let { frame, refFieldName, refFieldPseudoMax, refFieldPseudoMin, insertMode } = opts; + + if (!insertMode) { + insertMode = INSERT_MODES.threshold; + } + + const refField = getRefField(frame, refFieldName); + + if (refField == null) { + return frame; + } + + refField.state = { + ...refField.state, + nullThresholdApplied: true, + }; + + const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls || refField.config.interval || null); + + const uniqueThresholds = new Set(thresholds); + + uniqueThresholds.delete(null as any); + + if (uniqueThresholds.size === 0) { + return frame; + } + + if (uniqueThresholds.size === 1) { + const threshold = uniqueThresholds.values().next().value; + + if (threshold <= 0) { + return frame; + } + + const refValues = refField.values; + + const frameValues = frame.fields.map((field) => field.values); + + const filledFieldValues = nullInsertThreshold( + refValues, + frameValues, + threshold, + refFieldPseudoMin, + refFieldPseudoMax, + insertMode, + thorough + ); + + if (filledFieldValues === frameValues) { + return frame; + } + + return { + ...frame, + length: filledFieldValues[0].length, + fields: frame.fields.map((field, i) => ({ + ...field, + values: filledFieldValues[i], + })), + }; + } + + // TODO: unique threshold-per-field (via overrides) is unimplemented + // should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...) + // as a separate nullInsertThreshold() dataset, then re-join into single dataset via join() + return frame; +} + +function nullInsertThreshold( + refValues: number[], + frameValues: any[][], + threshold: number, + refFieldPseudoMin: number | null = null, + // will insert a trailing null when refFieldPseudoMax > last datapoint + threshold + refFieldPseudoMax: number | null = null, + getInsertValue: InsertMode, + // will insert the value at every missing interval + thorough: boolean +) { + const len = refValues.length; + const refValuesNew: number[] = []; + + // Continuously subtract the threshold from the first data point, filling in insert values accordingly + if (refFieldPseudoMin != null && refFieldPseudoMin < refValues[0]) { + let preFillCount = Math.ceil((refValues[0] - refFieldPseudoMin) / threshold); + // this will be 0 or 1 threshold increment left of visible range + let prevSlot = refValues[0] - preFillCount * threshold; + + while (prevSlot < refValues[0]) { + // (prevSlot - threshold) is used to simulate the previous 'real' data point, as getInsertValue expects + refValuesNew.push(getInsertValue(prevSlot - threshold, prevSlot, threshold)); + prevSlot += threshold; + } + } + + // Insert initial value + refValuesNew.push(refValues[0]); + + let prevValue: number = refValues[0]; + + // Fill nulls when a value is greater than the threshold value + for (let i = 1; i < len; i++) { + const curValue = refValues[i]; + + while (curValue - prevValue > threshold) { + refValuesNew.push(getInsertValue(prevValue, curValue, threshold)); + + prevValue += threshold; + + if (!thorough) { + break; + } + } + + refValuesNew.push(curValue); + + prevValue = curValue; + } + + // At the end of the sequence + if (refFieldPseudoMax != null && refFieldPseudoMax > prevValue) { + while (prevValue + threshold < refFieldPseudoMax) { + refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold)); + prevValue += threshold; + } + } + + const filledLen = refValuesNew.length; + + if (filledLen === len) { + return frameValues; + } + + const filledFieldValues: any[][] = []; + + for (let fieldValues of frameValues) { + let filledValues; + + if (fieldValues !== refValues) { + filledValues = Array(filledLen); + + for (let i = 0, j = 0; i < filledLen; i++) { + filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null; + } + } else { + filledValues = refValuesNew; + } + + filledFieldValues.push(filledValues); + } + + return filledFieldValues; +} diff --git a/packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/nullToValue.test.ts rename to packages/grafana-data/src/transformations/transformers/nulls/nullToValue.test.ts diff --git a/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts new file mode 100644 index 00000000000..4c3ba5a81e6 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/nulls/nullToValue.ts @@ -0,0 +1,27 @@ +import { DataFrame } from '../../../types'; + +export function nullToValue(frame: DataFrame) { + return { + ...frame, + fields: frame.fields.map((field) => { + const noValue = +field.config?.noValue!; + + if (!Number.isNaN(noValue)) { + const transformedVals = field.values.slice(); + + for (let i = 0; i < transformedVals.length; i++) { + if (transformedVals[i] === null) { + transformedVals[i] = noValue; + } + } + + return { + ...field, + values: transformedVals, + }; + } else { + return field; + } + }), + }; +} diff --git a/packages/grafana-ui/src/components/Sparkline/utils.ts b/packages/grafana-ui/src/components/Sparkline/utils.ts index c643d375a37..aa77fd64dc4 100644 --- a/packages/grafana-ui/src/components/Sparkline/utils.ts +++ b/packages/grafana-ui/src/components/Sparkline/utils.ts @@ -5,11 +5,10 @@ import { FieldType, isLikelyAscendingVector, sortDataFrame, + applyNullInsertThreshold, } from '@grafana/data'; import { GraphFieldConfig } from '@grafana/schema'; -import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold'; - /** @internal * Given a sparkline config returns a DataFrame ready to be turned into Plot data set **/ diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 0f5dd38ea02..78cd9b5321e 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -289,13 +289,14 @@ export { UPlotChart } from './uPlot/Plot'; export { PlotLegend } from './uPlot/PlotLegend'; export * from './uPlot/geometries'; export * from './uPlot/plugins'; -export { type PlotTooltipInterpolator, type PlotSelection } from './uPlot/types'; +export { type PlotTooltipInterpolator, type PlotSelection, FIXED_UNIT } from './uPlot/types'; export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder'; -export { GraphNG, type GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG'; -export { TimeSeries } from './TimeSeries/TimeSeries'; -export { useGraphNGContext } from './GraphNG/hooks'; -export { preparePlotFrame, buildScaleKey } from './GraphNG/utils'; -export { type GraphNGLegendEvent } from './GraphNG/types'; export * from './PanelChrome/types'; export { Label as BrowserLabel } from './BrowserLabel/Label'; export { PanelContainer } from './PanelContainer/PanelContainer'; + +export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG'; +export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries'; +export { useGraphNGContext } from '../graveyard/GraphNG/hooks'; +export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils'; +export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types'; diff --git a/packages/grafana-ui/src/components/uPlot/internal.ts b/packages/grafana-ui/src/components/uPlot/internal.ts new file mode 100644 index 00000000000..e7a9c6a14d1 --- /dev/null +++ b/packages/grafana-ui/src/components/uPlot/internal.ts @@ -0,0 +1,40 @@ +import { FieldConfig, FieldType } from '@grafana/data'; +import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema'; + +import { FIXED_UNIT } from './types'; + +/** + * @internal -- not a public API + */ +export function buildScaleKey(config: FieldConfig, fieldType: FieldType) { + const defaultPart = 'na'; + + const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${ + config.max !== undefined ? config.max : defaultPart + }`; + + const scaleSoftRange = `${config.custom?.axisSoftMin !== undefined ? config.custom.axisSoftMin : defaultPart}-${ + config.custom?.axisSoftMax !== undefined ? config.custom.axisSoftMax : defaultPart + }`; + + const scalePlacement = `${ + config.custom?.axisPlacement !== undefined ? config.custom?.axisPlacement : AxisPlacement.Auto + }`; + + const scaleUnit = config.unit ?? FIXED_UNIT; + + const scaleDistribution = config.custom?.scaleDistribution + ? getScaleDistributionPart(config.custom.scaleDistribution) + : ScaleDistribution.Linear; + + const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart; + + return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`; +} + +function getScaleDistributionPart(config: ScaleDistributionConfig) { + if (config.type === ScaleDistribution.Log) { + return `${config.type}${config.log}`; + } + return config.type; +} diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index 959787608d7..86f3c99c277 100644 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -3,6 +3,11 @@ import uPlot, { Options, AlignedData } from 'uplot'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; +/** + * @internal -- not a public API + */ +export const FIXED_UNIT = '__fixed'; + export type PlotConfig = Pick< Options, 'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding' diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 605c8649e63..fc6224ac596 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,7 +1,7 @@ import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data'; import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema'; -import { preparePlotFrame } from '../GraphNG/utils'; +import { preparePlotFrame } from '../../../../../public/app/core/components/GraphNG/utils'; import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils'; diff --git a/packages/grafana-ui/src/components/uPlot/utils.ts b/packages/grafana-ui/src/components/uPlot/utils.ts index b9312097b66..d5d1dd7356b 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.ts @@ -5,7 +5,8 @@ import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, Stacki import { attachDebugger } from '../../utils'; import { createLogger } from '../../utils/logger'; -import { buildScaleKey } from '../GraphNG/utils'; + +import { buildScaleKey } from './internal'; const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx new file mode 100644 index 00000000000..52dfadb27a6 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx @@ -0,0 +1,275 @@ +import React, { Component } from 'react'; +import { Subscription } from 'rxjs'; +import { throttleTime } from 'rxjs/operators'; +import uPlot, { AlignedData } from 'uplot'; + +import { + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + Field, + FieldMatcherID, + fieldMatchers, + FieldType, + LegacyGraphHoverEvent, + TimeRange, + TimeZone, +} from '@grafana/data'; +import { VizLegendOptions } from '@grafana/schema'; + +import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { VizLayout } from '../../components/VizLayout/VizLayout'; +import { UPlotChart } from '../../components/uPlot/Plot'; +import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder'; +import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder'; +import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils'; +import { Themeable2 } from '../../types'; + +import { GraphNGLegendEvent, XYFieldMatchers } from './types'; +import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; + +/** + * @deprecated + * @internal -- not a public API + */ +export type PropDiffFn = (prev: T, next: T) => boolean; + +/** @deprecated */ +export interface GraphNGProps extends Themeable2 { + frames: DataFrame[]; + structureRev?: number; // a number that will change when the frames[] structure changes + width: number; + height: number; + timeRange: TimeRange; + timeZone: TimeZone[] | TimeZone; + legend: VizLegendOptions; + fields?: XYFieldMatchers; // default will assume timeseries data + renderers?: Renderers; + tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps; + tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps; + onLegendClick?: (event: GraphNGLegendEvent) => void; + children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode; + prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder; + propsToDiff?: Array; + preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null; + renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null; + + /** + * needed for propsToDiff to re-init the plot & config + * this is a generic approach to plot re-init, without having to specify which panel-level options + * should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in + * similar to structureRev. then we can drop propsToDiff entirely. + */ + options?: Record; +} + +function sameProps(prevProps: any, nextProps: any, propsToDiff: Array = []) { + for (const propName of propsToDiff) { + if (typeof propName === 'function') { + if (!propName(prevProps, nextProps)) { + return false; + } + } else if (nextProps[propName] !== prevProps[propName]) { + return false; + } + } + + return true; +} + +/** + * @internal -- not a public API + * @deprecated + */ +export interface GraphNGState { + alignedFrame: DataFrame; + alignedData?: AlignedData; + config?: UPlotConfigBuilder; +} + +/** + * "Time as X" core component, expects ascending x + * @deprecated + */ +export class GraphNG extends Component { + static contextType = PanelContextRoot; + panelContext: PanelContext = {} as PanelContext; + private plotInstance: React.RefObject; + + private subscription = new Subscription(); + + constructor(props: GraphNGProps) { + super(props); + let state = this.prepState(props); + state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData; + this.state = state; + this.plotInstance = React.createRef(); + } + + getTimeRange = () => this.props.timeRange; + + prepState(props: GraphNGProps, withConfig = true) { + let state: GraphNGState = null as any; + + const { frames, fields, preparePlotFrame } = props; + + const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame; + + const alignedFrame = preparePlotFrameFn( + frames, + fields || { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])), + }, + props.timeRange + ); + pluginLog('GraphNG', false, 'data aligned', alignedFrame); + + if (alignedFrame) { + let config = this.state?.config; + + if (withConfig) { + config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config prepared', config); + } + + state = { + alignedFrame, + config, + }; + + pluginLog('GraphNG', false, 'data prepared', state.alignedData); + } + + return state; + } + + handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) { + const time = evt.payload?.point?.time; + const u = this.plotInstance.current; + if (u && time) { + // Try finding left position on time axis + const left = u.valToPos(time, 'x'); + let top; + if (left) { + // find midpoint between points at current idx + top = findMidPointYPosition(u, u.posToIdx(left)); + } + + if (!top || !left) { + return; + } + + u.setCursor({ + left, + top, + }); + } + } + + componentDidMount() { + this.panelContext = this.context as PanelContext; + const { eventBus } = this.panelContext; + + this.subscription.add( + eventBus + .getStream(DataHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => { + if (eventBus === evt.origin) { + return; + } + this.handleCursorUpdate(evt); + }, + }) + ); + + // Legacy events (from flot graph) + this.subscription.add( + eventBus + .getStream(LegacyGraphHoverEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: (evt) => this.handleCursorUpdate(evt), + }) + ); + + this.subscription.add( + eventBus + .getStream(DataHoverClearEvent) + .pipe(throttleTime(50)) + .subscribe({ + next: () => { + const u = this.plotInstance?.current; + + // @ts-ignore + if (u && !u.cursor._lock) { + u.setCursor({ + left: -10, + top: -10, + }); + } + }, + }) + ); + } + + componentDidUpdate(prevProps: GraphNGProps) { + const { frames, structureRev, timeZone, propsToDiff } = this.props; + + const propsChanged = !sameProps(prevProps, this.props, propsToDiff); + + if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) { + let newState = this.prepState(this.props, false); + + if (newState) { + const shouldReconfig = + this.state.config === undefined || + timeZone !== prevProps.timeZone || + structureRev !== prevProps.structureRev || + !structureRev || + propsChanged; + + if (shouldReconfig) { + newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange); + pluginLog('GraphNG', false, 'config recreated', newState.config); + } + + newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData; + + this.setState(newState); + } + } + } + + componentWillUnmount() { + this.subscription.unsubscribe(); + } + + render() { + const { width, height, children, renderLegend } = this.props; + const { config, alignedFrame, alignedData } = this.state; + + if (!config) { + return null; + } + + return ( + + {(vizWidth: number, vizHeight: number) => ( + ((this.plotInstance as React.MutableRefObject).current = u)} + > + {children ? children(config, alignedFrame) : null} + + )} + + ); + } +} diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap rename to packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap diff --git a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts new file mode 100644 index 00000000000..e4f1d46550a --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts @@ -0,0 +1,41 @@ +import React, { useCallback, useContext } from 'react'; + +import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data'; + +import { XYFieldMatchers } from './types'; + +/** @deprecated */ +interface GraphNGContextType { + mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex; + dimFields: XYFieldMatchers; + data: DataFrame; +} + +/** @deprecated */ +export const GraphNGContext = React.createContext({} as GraphNGContextType); + +/** @deprecated */ +export const useGraphNGContext = () => { + const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(GraphNGContext); + + const getXAxisField = useCallback(() => { + const xFieldMatcher = dimFields.x; + let xField: Field | null = null; + + for (let j = 0; j < data.fields.length; j++) { + if (xFieldMatcher(data.fields[j], data, [data])) { + xField = data.fields[j]; + break; + } + } + + return xField; + }, [data, dimFields]); + + return { + dimFields, + mapSeriesIndexToDataFrameFieldIndex, + getXAxisField, + alignedData: data, + }; +}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts new file mode 100644 index 00000000000..7a2ceb15e8d --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.test.ts @@ -0,0 +1,334 @@ +import { FieldType, createDataFrame } from '@grafana/data'; + +import { applyNullInsertThreshold } from './nullInsertThreshold'; + +function randInt(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1) + min); +} + +function genFrame() { + let fieldCount = 10; + let valueCount = 3000; + let step = 1000; + let skipProb = 0.5; + let skipSteps = [1, 5]; // min, max + + let allValues = Array(fieldCount); + + allValues[0] = Array(valueCount); + + for (let i = 0, curStep = Date.now(); i < valueCount; i++) { + curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1); + } + + for (let fi = 1; fi < fieldCount; fi++) { + let values = Array(valueCount); + + for (let i = 0; i < valueCount; i++) { + values[i] = Math.random() * 100; + } + + allValues[fi] = values; + } + + return { + length: valueCount, + fields: allValues.map((values, i) => { + return { + name: 'A-' + i, + type: i === 0 ? FieldType.time : FieldType.number, + config: { + interval: i === 0 ? step : null, + }, + values: values, + }; + }), + }; +} + +describe('nullInsertThreshold Transformer', () => { + test('should insert nulls at +threshold between adjacent > threshold: 1', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']); + }); + + test('should insert nulls at +threshold between adjacent > threshold: 2', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([5, 7, 9, 11]); + expect(result.fields[1].values).toEqual([4, 6, null, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', null, 'c']); + }); + + test('should insert nulls at +interval between adjacent > interval: 1', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']); + }); + + test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [4, 6, 13] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ + frame: df, + refFieldName: null, + refFieldPseudoMin: -0.5, + refFieldPseudoMax: 13, + }); + + expect(result.fields[0].values).toEqual([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(result.fields[1].values).toEqual([ + null, + null, + null, + null, + null, + 4, + null, + 6, + null, + null, + null, + null, + null, + null, + 8, + ]); + expect(result.fields[2].values).toEqual([ + null, + null, + null, + null, + null, + 'a', + null, + 'b', + null, + null, + null, + null, + null, + null, + 'c', + ]); + }); + + // this tests that intervals at 24hr but starting not at 12am UTC are not always snapped to 12am UTC + test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold 11PM UTC', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: { interval: 86400000 }, + values: [1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000], + }, + { name: 'One', type: FieldType.number, values: [0, 1, 2, 3, 4] }, + ], + }); + + const result = applyNullInsertThreshold({ + frame: df, + refFieldName: null, + refFieldPseudoMin: 1679320395828, + refFieldPseudoMax: 1679815217157, + }); + + expect(result.fields[0].values).toEqual([ + 1679266800000, 1679353200000, 1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000, + ]); + expect(result.fields[1].values).toEqual([null, null, 0, 1, 2, 3, 4]); + }); + + test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 13 }); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8, null, null]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c', null, null]); + + // should work for frames with 1 datapoint + const df2 = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] }, + { name: 'One', type: FieldType.number, values: [1] }, + { name: 'Two', type: FieldType.string, values: ['a'] }, + ], + }); + + // Max is 2.5 as opposed to the above 13 otherwise + // we get 12 nulls instead of the additional 1 + const result2 = applyNullInsertThreshold({ frame: df2, refFieldName: null, refFieldPseudoMax: 2.5 }); + + expect(result2.fields[0].values).toEqual([1, 2]); + expect(result2.fields[1].values).toEqual([1, null]); + expect(result2.fields[2].values).toEqual(['a', null]); + }); + + test('should not insert trailing null at end +interval when timeRange.to.valueOf() equals threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] }, + { name: 'One', type: FieldType.number, values: [1] }, + { name: 'Two', type: FieldType.string, values: ['a'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 2 }); + + expect(result.fields[0].values).toEqual([1]); + expect(result.fields[1].values).toEqual([1]); + expect(result.fields[2].values).toEqual(['a']); + }); + + // TODO: make this work + test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] }, + { name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result.fields[0].values).toEqual([5, 6, 7, 8, 11]); + expect(result.fields[1].values).toEqual([4, null, 6, null, 8]); + expect(result.fields[2].values).toEqual(['a', null, 'b', null, 'c']); + }); + + test('should noop on 0 datapoints', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [] }, + { name: 'Value', type: FieldType.number, values: [] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop on invalid threshold', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop on invalid interval', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop when no missing steps', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df }); + + expect(result).toBe(df); + }); + + test('should noop when refFieldName not found', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] }, + { name: 'Value', type: FieldType.number, values: [1, 1, 1] }, + ], + }); + + const result = applyNullInsertThreshold({ frame: df, refFieldName: 'Time2' }); + + expect(result).toBe(df); + }); + + // Leave this test skipped - it should be run manually + test.skip('perf stress test should be <= 10ms', () => { + // 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values) + let bigFrameA = genFrame(); + + // eslint-disable-next-line no-console + console.time('insertValues-10x3k'); + applyNullInsertThreshold({ frame: bigFrameA }); + // eslint-disable-next-line no-console + console.timeEnd('insertValues-10x3k'); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts similarity index 99% rename from packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts rename to packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts index 352f2f0660b..9361a6bab8d 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts @@ -19,6 +19,7 @@ interface NullInsertOptions { insertMode?: InsertMode; } +/** @deprecated */ export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame { if (opts.frame.length === 0) { return opts.frame; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts new file mode 100644 index 00000000000..bcc52e5e1b9 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts @@ -0,0 +1,33 @@ +/** + * mutates all nulls -> undefineds in the fieldValues array for value-less refValues ranges below maxThreshold + * refValues is typically a time array and maxThreshold is the allowable distance between in time + * @deprecated + */ +export function nullToUndefThreshold(refValues: number[], fieldValues: any[], maxThreshold: number): any[] { + let prevRef; + let nullIdx; + + for (let i = 0; i < fieldValues.length; i++) { + let fieldVal = fieldValues[i]; + + if (fieldVal == null) { + if (nullIdx == null && prevRef != null) { + nullIdx = i; + } + } else { + if (nullIdx != null) { + if (refValues[i] - (prevRef as number) < maxThreshold) { + while (nullIdx < i) { + fieldValues[nullIdx++] = undefined; + } + } + + nullIdx = null; + } + + prevRef = refValues[i]; + } + } + + return fieldValues; +} diff --git a/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts new file mode 100644 index 00000000000..e14f64e1710 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.test.ts @@ -0,0 +1,94 @@ +import { FieldType, createDataFrame } from '@grafana/data'; + +import { applyNullInsertThreshold } from './nullInsertThreshold'; +import { nullToValue } from './nullToValue'; + +describe('nullToValue Transformer', () => { + test('should change all nulls to configured zero value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [1, 3, 10] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 1 }, noValue: '0' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, 0, 6, 0, 0, 0, 0, 0, 0, 8]); + expect(result.fields[2].values).toEqual(['a', 0, 'b', 0, 0, 0, 0, 0, 0, 'c']); + }); + + test('should change all nulls to configured positive value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, values: [5, 7, 11] }, + { + name: 'One', + type: FieldType.number, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: [4, 6, 8], + }, + { + name: 'Two', + type: FieldType.string, + config: { custom: { insertNulls: 2 }, noValue: '1' }, + values: ['a', 'b', 'c'], + }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([5, 7, 9, 11]); + expect(result.fields[1].values).toEqual([4, 6, 1, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', 1, 'c']); + }); + + test('should change all nulls to configured negative value', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] }, + { name: 'One', type: FieldType.number, config: { noValue: '-1' }, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, config: { noValue: '-1' }, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df })); + + expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + expect(result.fields[1].values).toEqual([4, -1, 6, -1, -1, -1, -1, -1, -1, 8]); + expect(result.fields[2].values).toEqual(['a', -1, 'b', -1, -1, -1, -1, -1, -1, 'c']); + }); + + test('should have no effect without nulls', () => { + const df = createDataFrame({ + refId: 'A', + fields: [ + { name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] }, + { name: 'One', type: FieldType.number, values: [4, 6, 8] }, + { name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] }, + ], + }); + + const result = nullToValue(applyNullInsertThreshold({ frame: df, refFieldName: null })); + + expect(result.fields[0].values).toEqual([1, 2, 3]); + expect(result.fields[1].values).toEqual([4, 6, 8]); + expect(result.fields[2].values).toEqual(['a', 'b', 'c']); + }); +}); diff --git a/packages/grafana-ui/src/components/GraphNG/nullToValue.ts b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts similarity index 96% rename from packages/grafana-ui/src/components/GraphNG/nullToValue.ts rename to packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts index 62aea31b837..7ad8e3705aa 100644 --- a/packages/grafana-ui/src/components/GraphNG/nullToValue.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/nullToValue.ts @@ -1,5 +1,6 @@ import { DataFrame } from '@grafana/data'; +/** @deprecated */ export function nullToValue(frame: DataFrame) { return { ...frame, diff --git a/packages/grafana-ui/src/graveyard/GraphNG/types.ts b/packages/grafana-ui/src/graveyard/GraphNG/types.ts new file mode 100644 index 00000000000..91dd83fb5a8 --- /dev/null +++ b/packages/grafana-ui/src/graveyard/GraphNG/types.ts @@ -0,0 +1,18 @@ +import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data'; + +import { SeriesVisibilityChangeMode } from '../../components/PanelChrome'; + +/** + * Event being triggered when the user interact with the Graph legend. + * @deprecated + */ +export interface GraphNGLegendEvent { + fieldIndex: DataFrameFieldIndex; + mode: SeriesVisibilityChangeMode; +} + +/** @deprecated */ +export interface XYFieldMatchers { + x: FieldMatcher; // first match + y: FieldMatcher; +} diff --git a/packages/grafana-ui/src/components/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/utils.test.ts rename to packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.ts similarity index 98% rename from packages/grafana-ui/src/components/GraphNG/utils.ts rename to packages/grafana-ui/src/graveyard/GraphNG/utils.ts index 9922b85d9df..742031446ac 100644 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/graveyard/GraphNG/utils.ts @@ -7,7 +7,8 @@ import { ScaleDistributionConfig, } from '@grafana/schema'; -import { FIXED_UNIT } from './GraphNG'; +import { FIXED_UNIT } from '../../components/uPlot/types'; + import { applyNullInsertThreshold } from './nullInsertThreshold'; import { nullToUndefThreshold } from './nullToUndefThreshold'; import { XYFieldMatchers } from './types'; diff --git a/packages/grafana-ui/src/graveyard/README.md b/packages/grafana-ui/src/graveyard/README.md new file mode 100644 index 00000000000..2715ecec38a --- /dev/null +++ b/packages/grafana-ui/src/graveyard/README.md @@ -0,0 +1 @@ +Items in this folder are all deprecated and will be removed in the future diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx similarity index 87% rename from packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx rename to packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx index 019671e2b6a..9a748236d77 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx @@ -2,11 +2,11 @@ import React, { Component } from 'react'; import { DataFrame, TimeRange } from '@grafana/data'; +import { PanelContextRoot } from '../../components/PanelChrome/PanelContext'; +import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend'; +import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder'; import { withTheme2 } from '../../themes/ThemeContext'; import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; -import { PanelContextRoot } from '../PanelChrome/PanelContext'; -import { hasVisibleLegendSeries, PlotLegend } from '../uPlot/PlotLegend'; -import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { preparePlotConfigBuilder } from './utils'; diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.test.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts similarity index 100% rename from packages/grafana-ui/src/components/TimeSeries/utils.test.ts rename to packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts similarity index 98% rename from packages/grafana-ui/src/components/TimeSeries/utils.ts rename to packages/grafana-ui/src/graveyard/TimeSeries/utils.ts index 9d2d373b33d..ad029097ba5 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts @@ -61,10 +61,10 @@ for (let i = 0; i < BIN_INCRS.length; i++) { BIN_INCRS[i] = 2 ** i; } +import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder'; +import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills'; +import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils'; import { buildScaleKey } from '../GraphNG/utils'; -import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConfigBuilder'; -import { getScaleGradientFn } from '../uPlot/config/gradientFills'; -import { getStackingGroups, preparePlotData2 } from '../uPlot/utils'; const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/public/app/core/components/GraphNG/GraphNG.tsx similarity index 92% rename from packages/grafana-ui/src/components/GraphNG/GraphNG.tsx rename to public/app/core/components/GraphNG/GraphNG.tsx index d75ab59252b..9ebfa499c81 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/public/app/core/components/GraphNG/GraphNG.tsx @@ -16,24 +16,16 @@ import { TimeZone, } from '@grafana/data'; import { VizLegendOptions } from '@grafana/schema'; - -import { Themeable2 } from '../../types'; -import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; -import { VizLayout } from '../VizLayout/VizLayout'; -import { UPlotChart } from '../uPlot/Plot'; -import { AxisProps } from '../uPlot/config/UPlotAxisBuilder'; -import { Renderers, UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; -import { ScaleProps } from '../uPlot/config/UPlotScaleBuilder'; -import { findMidPointYPosition, pluginLog } from '../uPlot/utils'; +import { Themeable2, PanelContext, PanelContextRoot, VizLayout } from '@grafana/ui'; +import { UPlotChart } from '@grafana/ui/src/components/uPlot/Plot'; +import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; +import { Renderers, UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; +import { findMidPointYPosition, pluginLog } from '@grafana/ui/src/components/uPlot/utils'; import { GraphNGLegendEvent, XYFieldMatchers } from './types'; import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; -/** - * @internal -- not a public API - */ -export const FIXED_UNIT = '__fixed'; - /** * @internal -- not a public API */ diff --git a/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000000..09f70e81c44 --- /dev/null +++ b/public/app/core/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -0,0 +1,245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GraphNG utils preparePlotConfigBuilder 1`] = ` +{ + "axes": [ + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "x", + "show": true, + "side": 2, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": true, + "size": 4, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "timeZone": "utc", + "values": [Function], + }, + { + "filter": undefined, + "font": "12px "Inter", "Helvetica", "Arial", sans-serif", + "gap": 5, + "grid": { + "show": true, + "stroke": "rgba(240, 250, 255, 0.09)", + "width": 1, + }, + "incrs": undefined, + "labelGap": 0, + "rotate": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "side": 3, + "size": [Function], + "space": [Function], + "splits": undefined, + "stroke": "rgb(204, 204, 220)", + "ticks": { + "show": false, + "size": 4, + "stroke": "rgb(204, 204, 220)", + "width": 1, + }, + "timeZone": undefined, + "values": [Function], + }, + ], + "cursor": { + "dataIdx": [Function], + "drag": { + "setScale": false, + }, + "focus": { + "prox": 30, + }, + "points": { + "fill": [Function], + "size": [Function], + "stroke": [Function], + "width": [Function], + }, + "sync": { + "filters": { + "pub": [Function], + }, + "key": "__global_", + "scales": [ + "x", + "__fixed/na-na/na-na/auto/linear/na/number", + ], + }, + }, + "focus": { + "alpha": 1, + }, + "hooks": {}, + "legend": { + "show": false, + }, + "mode": 1, + "ms": 1, + "padding": [ + [Function], + [Function], + [Function], + [Function], + ], + "scales": { + "__fixed/na-na/na-na/auto/linear/na/number": { + "asinh": undefined, + "auto": true, + "dir": 1, + "distr": 1, + "log": undefined, + "ori": 1, + "range": [Function], + "time": undefined, + }, + "x": { + "auto": false, + "dir": 1, + "ori": 0, + "range": [Function], + "time": true, + }, + }, + "select": undefined, + "series": [ + { + "value": [Function], + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + { + "dash": [ + 1, + 2, + ], + "facets": undefined, + "fill": [Function], + "paths": [Function], + "points": { + "fill": "#ff0000", + "filter": [Function], + "show": true, + "size": undefined, + "stroke": "#ff0000", + }, + "pxAlign": undefined, + "scale": "__fixed/na-na/na-na/auto/linear/na/number", + "show": true, + "spanGaps": false, + "stroke": "#ff0000", + "value": [Function], + "width": 2, + }, + ], + "tzDate": [Function], +} +`; diff --git a/packages/grafana-ui/src/components/GraphNG/hooks.ts b/public/app/core/components/GraphNG/hooks.ts similarity index 100% rename from packages/grafana-ui/src/components/GraphNG/hooks.ts rename to public/app/core/components/GraphNG/hooks.ts diff --git a/packages/grafana-ui/src/components/GraphNG/types.ts b/public/app/core/components/GraphNG/types.ts similarity index 85% rename from packages/grafana-ui/src/components/GraphNG/types.ts rename to public/app/core/components/GraphNG/types.ts index 534cd9edf4d..e642fbd8aac 100644 --- a/packages/grafana-ui/src/components/GraphNG/types.ts +++ b/public/app/core/components/GraphNG/types.ts @@ -1,6 +1,5 @@ import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data'; - -import { SeriesVisibilityChangeMode } from '../PanelChrome'; +import { SeriesVisibilityChangeMode } from '@grafana/ui'; /** * Event being triggered when the user interact with the Graph legend. diff --git a/public/app/core/components/GraphNG/utils.test.ts b/public/app/core/components/GraphNG/utils.test.ts new file mode 100644 index 00000000000..9675cd7ca56 --- /dev/null +++ b/public/app/core/components/GraphNG/utils.test.ts @@ -0,0 +1,522 @@ +import { + createTheme, + DashboardCursorSync, + DataFrame, + DefaultTimeZone, + EventBusSrv, + FieldColorModeId, + FieldConfig, + FieldMatcherID, + fieldMatchers, + FieldType, + getDefaultTimeRange, + MutableDataFrame, +} from '@grafana/data'; +import { + BarAlignment, + GraphDrawStyle, + GraphFieldConfig, + GraphGradientMode, + LineInterpolation, + VisibilityMode, + StackingMode, +} from '@grafana/schema'; + +import { preparePlotConfigBuilder } from '../TimeSeries/utils'; + +import { preparePlotFrame } from './utils'; + +function mockDataFrame() { + const df1 = new MutableDataFrame({ + refId: 'A', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }], + }); + const df2 = new MutableDataFrame({ + refId: 'B', + fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }], + }); + + const f1Config: FieldConfig = { + displayName: 'Metric 1', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f2Config: FieldConfig = { + displayName: 'Metric 2', + color: { + mode: FieldColorModeId.Fixed, + }, + decimals: 2, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'A', + mode: StackingMode.Normal, + }, + }, + }; + + const f3Config: FieldConfig = { + displayName: 'Metric 3', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Line, + gradientMode: GraphGradientMode.Opacity, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + spanNulls: false, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f4Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.Normal, + }, + }, + }; + const f5Config: FieldConfig = { + displayName: 'Metric 4', + decimals: 2, + color: { + mode: FieldColorModeId.Fixed, + }, + custom: { + drawStyle: GraphDrawStyle.Bars, + gradientMode: GraphGradientMode.Hue, + lineColor: '#ff0000', + lineWidth: 2, + lineInterpolation: LineInterpolation.Linear, + lineStyle: { + fill: 'dash', + dash: [1, 2], + }, + barAlignment: BarAlignment.Before, + fillColor: '#ff0000', + fillOpacity: 0.1, + showPoints: VisibilityMode.Always, + stacking: { + group: 'B', + mode: StackingMode.None, + }, + }, + }; + + df1.addField({ + name: 'metric1', + type: FieldType.number, + config: f1Config, + }); + + df2.addField({ + name: 'metric2', + type: FieldType.number, + config: f2Config, + }); + df2.addField({ + name: 'metric3', + type: FieldType.number, + config: f3Config, + }); + df2.addField({ + name: 'metric4', + type: FieldType.number, + config: f4Config, + }); + df2.addField({ + name: 'metric5', + type: FieldType.number, + config: f5Config, + }); + + return preparePlotFrame([df1, df2], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); +} + +jest.mock('@grafana/data', () => ({ + ...jest.requireActual('@grafana/data'), + DefaultTimeZone: 'utc', +})); + +describe('GraphNG utils', () => { + test('preparePlotConfigBuilder', () => { + const frame = mockDataFrame(); + const result = preparePlotConfigBuilder({ + frame: frame!, + theme: createTheme(), + timeZones: [DefaultTimeZone], + getTimeRange: getDefaultTimeRange, + eventBus: new EventBusSrv(), + sync: () => DashboardCursorSync.Tooltip, + allFrames: [frame!], + }).getConfig(); + expect(result).toMatchSnapshot(); + }); + + test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => { + const df1: DataFrame = { + name: 'A', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [1, 1, 1, 1, 1], + }, + ], + }; + + const df2: DataFrame = { + name: 'B', + length: 5, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + }, + }, + values: [2, 2, 2, 2, 2], // bar series should be appended with nulls + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [3, 3, 3, 3, 3], // line series should be appended with undefineds + }, + ], + }; + + const df3: DataFrame = { + name: 'C', + length: 2, + fields: [ + { + name: 'time', + type: FieldType.time, + config: {}, + values: [1, 1.1], // should not trip up on smaller deltas of non-bars + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Line, + }, + }, + values: [4, 4], + }, + { + name: 'value', + type: FieldType.number, + config: { + custom: { + drawStyle: GraphDrawStyle.Bars, + hideFrom: { + viz: true, // should ignore hidden bar series + }, + }, + }, + values: [4, 4], + }, + ], + }; + + let aligndFrame = preparePlotFrame([df1, df2, df3], { + x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + y: fieldMatchers.get(FieldMatcherID.numeric).get({}), + }); + + expect(aligndFrame).toMatchInlineSnapshot(` + { + "fields": [ + { + "config": {}, + "name": "time", + "state": { + "nullThresholdApplied": true, + "origin": { + "fieldIndex": 0, + "frameIndex": 0, + }, + }, + "type": "time", + "values": [ + 1, + 1.1, + 2, + 4, + 6, + 30, + 40, + 50, + 90, + 100, + 101, + 102, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "A", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 0, + }, + }, + "type": "number", + "values": [ + 1, + undefined, + 1, + 1, + 1, + undefined, + undefined, + undefined, + undefined, + 1, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "spanNulls": -1, + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 2, + 2, + 2, + 2, + 2, + null, + null, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "B", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 1, + }, + }, + "type": "number", + "values": [ + undefined, + undefined, + undefined, + undefined, + undefined, + 3, + 3, + 3, + 3, + 3, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "line", + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 1, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + { + "config": { + "custom": { + "drawStyle": "bars", + "hideFrom": { + "viz": true, + }, + }, + }, + "labels": { + "name": "C", + }, + "name": "value", + "state": { + "origin": { + "fieldIndex": 2, + "frameIndex": 2, + }, + }, + "type": "number", + "values": [ + 4, + 4, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ], + }, + ], + "length": 12, + } + `); + }); +}); diff --git a/public/app/core/components/GraphNG/utils.ts b/public/app/core/components/GraphNG/utils.ts new file mode 100644 index 00000000000..030ea9722db --- /dev/null +++ b/public/app/core/components/GraphNG/utils.ts @@ -0,0 +1,140 @@ +import { DataFrame, Field, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToUndefThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullToUndefThreshold'; +import { GraphDrawStyle } from '@grafana/schema'; + +import { XYFieldMatchers } from './types'; + +function isVisibleBarField(f: Field) { + return ( + f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars && !f.config.custom?.hideFrom?.viz + ); +} + +export function getRefField(frame: DataFrame, refFieldName?: string | null) { + return frame.fields.find((field) => { + // note: getFieldDisplayName() would require full DF[] + return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time; + }); +} + +// will mutate the DataFrame's fields' values +function applySpanNullsThresholds(frame: DataFrame, refFieldName?: string | null) { + const refField = getRefField(frame, refFieldName); + + let refValues = refField?.values; + + for (let i = 0; i < frame.fields.length; i++) { + let field = frame.fields[i]; + + if (field === refField || isVisibleBarField(field)) { + continue; + } + + let spanNulls = field.config.custom?.spanNulls; + + if (typeof spanNulls === 'number') { + if (spanNulls !== -1 && refValues) { + field.values = nullToUndefThreshold(refValues, field.values, spanNulls); + } + } + } + + return frame; +} + +export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { + let xField: Field; + loop: for (let frame of frames) { + for (let field of frame.fields) { + if (dimFields.x(field, frame, frames)) { + xField = field; + break loop; + } + } + } + + // apply null insertions at interval + frames = frames.map((frame) => { + if (!xField?.state?.nullThresholdApplied) { + return applyNullInsertThreshold({ + frame, + refFieldName: xField.name, + refFieldPseudoMin: timeRange?.from.valueOf(), + refFieldPseudoMax: timeRange?.to.valueOf(), + }); + } else { + return frame; + } + }); + + let numBarSeries = 0; + + frames.forEach((frame) => { + frame.fields.forEach((f) => { + if (isVisibleBarField(f)) { + // prevent minesweeper-expansion of nulls (gaps) when joining bars + // since bar width is determined from the minimum distance between non-undefined values + // (this strategy will still retain any original pre-join nulls, though) + f.config.custom = { + ...f.config.custom, + spanNulls: -1, + }; + + numBarSeries++; + } + }); + }); + + // to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points + let minXDelta = Infinity; + + if (numBarSeries > 1) { + frames.forEach((frame) => { + if (!frame.fields.some(isVisibleBarField)) { + return; + } + + const xVals = xField.values; + + for (let i = 0; i < xVals.length; i++) { + if (i > 0) { + minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]); + } + } + }); + } + + let alignedFrame = outerJoinDataFrames({ + frames, + joinBy: dimFields.x, + keep: dimFields.y, + keepOriginIndices: true, + }); + + if (alignedFrame) { + alignedFrame = applySpanNullsThresholds(alignedFrame, xField!.name); + + // append 2 null vals at minXDelta to bar series + if (minXDelta !== Infinity) { + alignedFrame.fields.forEach((f, fi) => { + let vals = f.values; + + if (fi === 0) { + let lastVal = vals[vals.length - 1]; + vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta); + } else if (isVisibleBarField(f)) { + vals.push(null, null); + } else { + vals.push(undefined, undefined); + } + }); + + alignedFrame.length += 2; + } + + return alignedFrame; + } + + return null; +} diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx new file mode 100644 index 00000000000..4441bca98f3 --- /dev/null +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -0,0 +1,63 @@ +import React, { Component } from 'react'; + +import { DataFrame, TimeRange } from '@grafana/data'; +import { PanelContextRoot } from '@grafana/ui/src/components/PanelChrome/PanelContext'; +import { hasVisibleLegendSeries, PlotLegend } from '@grafana/ui/src/components/uPlot/PlotLegend'; +import { UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { withTheme2 } from '@grafana/ui/src/themes/ThemeContext'; + +import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG'; + +import { preparePlotConfigBuilder } from './utils'; + +const propsToDiff: Array = ['legend', 'options', 'theme']; + +type TimeSeriesProps = Omit; + +export class UnthemedTimeSeries extends Component { + static contextType = PanelContextRoot; + declare context: React.ContextType; + + prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { + const { eventBus, eventsScope, sync } = this.context; + const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props; + + return preparePlotConfigBuilder({ + frame: alignedFrame, + theme, + timeZones: Array.isArray(timeZone) ? timeZone : [timeZone], + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale, + tweakAxis, + eventsScope, + }); + }; + + renderLegend = (config: UPlotConfigBuilder) => { + const { legend, frames } = this.props; + + if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) { + return null; + } + + return ; + }; + + render() { + return ( + + ); + } +} + +export const TimeSeries = withTheme2(UnthemedTimeSeries); +TimeSeries.displayName = 'TimeSeries'; diff --git a/public/app/core/components/TimeSeries/utils.test.ts b/public/app/core/components/TimeSeries/utils.test.ts new file mode 100644 index 00000000000..583358c7c4c --- /dev/null +++ b/public/app/core/components/TimeSeries/utils.test.ts @@ -0,0 +1,274 @@ +import { EventBus, FieldType } from '@grafana/data'; +import { getTheme } from '@grafana/ui'; + +import { preparePlotConfigBuilder } from './utils'; + +describe('when fill below to option is used', () => { + let eventBus: EventBus; + // eslint-disable-next-line + let renderers: any[]; + // eslint-disable-next-line + let tests: any; + + beforeEach(() => { + eventBus = { + publish: jest.fn(), + getStream: jest.fn(), + subscribe: jest.fn(), + removeAllListeners: jest.fn(), + newScopedBus: jest.fn(), + }; + renderers = []; + + tests = [ + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [4, 5, 6], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + allFrames: [ + { + name: 'Test1', + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + { + name: 'Test2', + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'Time', + state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: { displayNameFromDS: 'Test2', min: 0, max: 100 }, + values: [1, 2, 3], + name: 'Value', + state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 2, + }, + ], + expectedResult: 1, + }, + { + alignedFrame: { + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 5, + }, + allFrames: [ + { + refId: 'A', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } }, + type: FieldType.time, + }, + { + config: { custom: { fillBelowTo: 'below_value1' } }, + values: [1, 2, 3], + name: 'value1', + state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } }, + type: FieldType.number, + }, + { + config: { custom: { fillBelowTo: 'below_value2' } }, + values: [4, 5, 6], + name: 'value2', + state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + { + refId: 'B', + fields: [ + { + config: {}, + values: [1667406900000, 1667407170000, 1667407185000], + name: 'time', + state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } }, + type: FieldType.time, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value1', + state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } }, + type: FieldType.number, + }, + { + config: {}, + values: [4, 5, 6], + name: 'below_value2', + state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } }, + type: FieldType.number, + }, + ], + length: 3, + }, + ], + expectedResult: 2, + }, + ]; + }); + + it('should verify if fill below to is set then builder bands are set', () => { + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is not set then builder bands are empty', () => { + tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined; + tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined; + tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined; + tests[0].expectedResult = 0; + tests[1].expectedResult = 0; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); + + it('should verify if fill below to is set and field name is overriden then builder bands are set', () => { + tests[0].alignedFrame.fields[2].config.displayName = 'newName'; + tests[0].alignedFrame.fields[2].state.displayName = 'newName'; + tests[0].allFrames[1].fields[1].config.displayName = 'newName'; + tests[0].allFrames[1].fields[1].state.displayName = 'newName'; + + tests[1].alignedFrame.fields[3].config.displayName = 'newName'; + tests[1].alignedFrame.fields[3].state.displayName = 'newName'; + tests[1].allFrames[1].fields[1].config.displayName = 'newName'; + tests[1].allFrames[1].fields[1].state.displayName = 'newName'; + + for (const test of tests) { + const builder = preparePlotConfigBuilder({ + frame: test.alignedFrame, + //@ts-ignore + theme: getTheme(), + timeZones: ['browser'], + getTimeRange: jest.fn(), + eventBus, + sync: jest.fn(), + allFrames: test.allFrames, + renderers, + }); + + //@ts-ignore + expect(builder.bands.length).toBe(test.expectedResult); + } + }); +}); diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts new file mode 100644 index 00000000000..36da06299da --- /dev/null +++ b/public/app/core/components/TimeSeries/utils.ts @@ -0,0 +1,669 @@ +import { isNumber } from 'lodash'; +import uPlot from 'uplot'; + +import { + DashboardCursorSync, + DataFrame, + DataHoverClearEvent, + DataHoverEvent, + DataHoverPayload, + FieldConfig, + FieldType, + formattedValueToString, + getFieldColorModeForField, + getFieldSeriesColor, + getFieldDisplayName, + getDisplayProcessor, + FieldColorModeId, + DecimalCount, +} from '@grafana/data'; +// eslint-disable-next-line import/order +import { + AxisPlacement, + GraphDrawStyle, + GraphFieldConfig, + GraphTresholdsStyleMode, + VisibilityMode, + ScaleDirection, + ScaleOrientation, + StackingMode, + GraphTransform, + AxisColorMode, + GraphGradientMode, +} from '@grafana/schema'; + +// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks +// see categories.ts is @grafana/data +const IEC_UNITS = new Set([ + 'bytes', + 'bits', + 'kbytes', + 'mbytes', + 'gbytes', + 'tbytes', + 'pbytes', + 'binBps', + 'binbps', + 'KiBs', + 'Kibits', + 'MiBs', + 'Mibits', + 'GiBs', + 'Gibits', + 'TiBs', + 'Tibits', + 'PiBs', + 'Pibits', +]); + +const BIN_INCRS = Array(53); + +for (let i = 0; i < BIN_INCRS.length; i++) { + BIN_INCRS[i] = 2 ** i; +} + +import { UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder'; +import { getScaleGradientFn } from '@grafana/ui/src/components/uPlot/config/gradientFills'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; +import { getStackingGroups, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils'; + +const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals)); + +const defaultConfig: GraphFieldConfig = { + drawStyle: GraphDrawStyle.Line, + showPoints: VisibilityMode.Auto, + axisPlacement: AxisPlacement.Auto, +}; + +export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ + sync?: () => DashboardCursorSync; +}> = ({ + frame, + theme, + timeZones, + getTimeRange, + eventBus, + sync, + allFrames, + renderers, + tweakScale = (opts) => opts, + tweakAxis = (opts) => opts, + eventsScope = '__global_', +}) => { + const builder = new UPlotConfigBuilder(timeZones[0]); + + let alignedFrame: DataFrame; + + builder.setPrepData((frames) => { + // cache alignedFrame + alignedFrame = frames[0]; + + return preparePlotData2(frames[0], builder.getStackingGroups()); + }); + + // X is the first field in the aligned frame + const xField = frame.fields[0]; + if (!xField) { + return builder; // empty frame with no options + } + + const xScaleKey = 'x'; + let xScaleUnit = '_x'; + let yScaleKey = ''; + + const xFieldAxisPlacement = + xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; + const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden; + + if (xField.type === FieldType.time) { + xScaleUnit = 'time'; + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + isTime: true, + range: () => { + const r = getTimeRange(); + return [r.from.valueOf(), r.to.valueOf()]; + }, + }); + + // filters first 2 ticks to make space for timezone labels + const filterTicks: uPlot.Axis.Filter | undefined = + timeZones.length > 1 + ? (u, splits) => { + return splits.map((v, i) => (i < 2 ? null : v)); + } + : undefined; + + for (let i = 0; i < timeZones.length; i++) { + const timeZone = timeZones[i]; + builder.addAxis({ + scaleKey: xScaleKey, + isTime: true, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + timeZone, + theme, + grid: { show: i === 0 && xField.config.custom?.axisGridShow }, + filter: filterTicks, + }); + } + + // render timezone labels + if (timeZones.length > 1) { + builder.addHook('drawAxes', (u: uPlot) => { + u.ctx.save(); + + u.ctx.fillStyle = theme.colors.text.primary; + u.ctx.textAlign = 'left'; + u.ctx.textBaseline = 'bottom'; + + let i = 0; + u.axes.forEach((a) => { + if (a.side === 2) { + //@ts-ignore + let cssBaseline: number = a._pos + a._size; + u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); + i++; + } + }); + + u.ctx.restore(); + }); + } + } else { + // Not time! + if (xField.config.unit) { + xScaleUnit = xField.config.unit; + } + + builder.addScale({ + scaleKey: xScaleKey, + orientation: ScaleOrientation.Horizontal, + direction: ScaleDirection.Right, + range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax], + }); + + builder.addAxis({ + scaleKey: xScaleKey, + placement: xFieldAxisPlacement, + show: xFieldAxisShow, + label: xField.config.custom?.axisLabel, + theme, + grid: { show: xField.config.custom?.axisGridShow }, + formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)), + }); + } + + let customRenderedFields = + renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? []; + + let indexByName: Map | undefined; + + for (let i = 1; i < frame.fields.length; i++) { + const field = frame.fields[i]; + + const config: FieldConfig = { + ...field.config, + custom: { + ...defaultConfig, + ...field.config.custom, + }, + }; + + const customConfig: GraphFieldConfig = config.custom!; + + if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) { + continue; + } + + let fmt = field.display ?? defaultFormatter; + if (field.config.custom?.stacking?.mode === StackingMode.Percent) { + fmt = getDisplayProcessor({ + field: { + ...field, + config: { + ...field.config, + unit: 'percentunit', + }, + }, + theme, + }); + } + const scaleKey = buildScaleKey(config, field.type); + const colorMode = getFieldColorModeForField(field); + const scaleColor = getFieldSeriesColor(field, theme); + const seriesColor = scaleColor.color; + + // The builder will manage unique scaleKeys and combine where appropriate + builder.addScale( + tweakScale( + { + scaleKey, + orientation: ScaleOrientation.Vertical, + direction: ScaleDirection.Up, + distribution: customConfig.scaleDistribution?.type, + log: customConfig.scaleDistribution?.log, + linearThreshold: customConfig.scaleDistribution?.linearThreshold, + min: field.config.min, + max: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + centeredZero: customConfig.axisCenteredZero, + range: + customConfig.stacking?.mode === StackingMode.Percent + ? (u: uPlot, dataMin: number, dataMax: number) => { + dataMin = dataMin < 0 ? -1 : 0; + dataMax = dataMax > 0 ? 1 : 0; + return [dataMin, dataMax]; + } + : field.type === FieldType.enum + ? (u: uPlot, dataMin: number, dataMax: number) => { + // this is the exhaustive enum (stable) + let len = field.config.type!.enum!.text!.length; + + return [-1, len]; + + // these are only values that are present + // return [dataMin - 1, dataMax + 1] + } + : undefined, + decimals: field.config.decimals, + }, + field + ) + ); + + if (!yScaleKey) { + yScaleKey = scaleKey; + } + + if (customConfig.axisPlacement !== AxisPlacement.Hidden) { + let axisColor: uPlot.Axis.Stroke | undefined; + + if (customConfig.axisColorMode === AxisColorMode.Series) { + if ( + colorMode.isByValue && + field.config.custom?.gradientMode === GraphGradientMode.Scheme && + colorMode.id === FieldColorModeId.Thresholds + ) { + axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds); + } else { + axisColor = seriesColor; + } + } + + const axisDisplayOptions = { + border: { + show: customConfig.axisBorderShow || false, + width: 1 / devicePixelRatio, + stroke: axisColor || theme.colors.text.primary, + }, + ticks: { + show: customConfig.axisBorderShow || false, + stroke: axisColor || theme.colors.text.primary, + }, + color: axisColor || theme.colors.text.primary, + }; + + let incrs: uPlot.Axis.Incrs | undefined; + + // TODO: these will be dynamic with frame updates, so need to accept getYTickLabels() + let values: uPlot.Axis.Values | undefined; + let splits: uPlot.Axis.Splits | undefined; + + if (IEC_UNITS.has(config.unit!)) { + incrs = BIN_INCRS; + } else if (field.type === FieldType.enum) { + let text = field.config.type!.enum!.text!; + splits = text.map((v: string, i: number) => i); + values = text; + } + + builder.addAxis( + tweakAxis( + { + scaleKey, + label: customConfig.axisLabel, + size: customConfig.axisWidth, + placement: customConfig.axisPlacement ?? AxisPlacement.Auto, + formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)), + theme, + grid: { show: customConfig.axisGridShow }, + decimals: field.config.decimals, + distr: customConfig.scaleDistribution?.type, + splits, + values, + incrs, + ...axisDisplayOptions, + }, + field + ) + ); + } + + const showPoints = + customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints; + + let pointsFilter: uPlot.Series.Points.Filter = () => null; + + if (customConfig.spanNulls !== true) { + pointsFilter = (u, seriesIdx, show, gaps) => { + let filtered = []; + + let series = u.series[seriesIdx]; + + if (!show && gaps && gaps.length) { + const [firstIdx, lastIdx] = series.idxs!; + const xData = u.data[0]; + const yData = u.data[seriesIdx]; + const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true)); + const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true)); + + if (gaps[0][0] === firstPos) { + filtered.push(firstIdx); + } + + // show single points between consecutive gaps that share end/start + for (let i = 0; i < gaps.length; i++) { + let thisGap = gaps[i]; + let nextGap = gaps[i + 1]; + + if (nextGap && thisGap[1] === nextGap[0]) { + // approx when data density is > 1pt/px, since gap start/end pixels are rounded + let approxIdx = u.posToIdx(thisGap[1], true); + + if (yData[approxIdx] == null) { + // scan left/right alternating to find closest index with non-null value + for (let j = 1; j < 100; j++) { + if (yData[approxIdx + j] != null) { + approxIdx += j; + break; + } + if (yData[approxIdx - j] != null) { + approxIdx -= j; + break; + } + } + } + + filtered.push(approxIdx); + } + } + + if (gaps[gaps.length - 1][1] === lastPos) { + filtered.push(lastIdx); + } + } + + return filtered.length ? filtered : null; + }; + } + + let { fillOpacity } = customConfig; + + let pathBuilder: uPlot.Series.PathBuilder | null = null; + let pointsBuilder: uPlot.Series.Points.Show | null = null; + + if (field.state?.origin) { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + + const originFrame = allFrames[field.state.origin.frameIndex]; + const originField = originFrame?.fields[field.state.origin.fieldIndex]; + + const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames); + + // disable default renderers + if (customRenderedFields.indexOf(dispName) >= 0) { + pathBuilder = () => null; + pointsBuilder = () => undefined; + } else if (customConfig.transform === GraphTransform.Constant) { + // patch some monkeys! + const defaultBuilder = uPlot.paths!.linear!(); + + pathBuilder = (u, seriesIdx) => { + //eslint-disable-next-line + const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types + + // the data we want the line renderer to pull is x at each plot edge with paired flat y values + + const r = getTimeRange(); + let xData = [r.from.valueOf(), r.to.valueOf()]; + let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null); + let yData = [firstY, firstY]; + let fauxData = _data.slice(); + fauxData[0] = xData; + fauxData[seriesIdx] = yData; + + //eslint-disable-next-line + return defaultBuilder( + { + ...u, + _data: fauxData, + } as any, + seriesIdx, + 0, + 1 + ); + }; + } + + if (customConfig.fillBelowTo) { + const fillBelowToField = frame.fields.find( + (f) => + customConfig.fillBelowTo === f.name || + customConfig.fillBelowTo === f.config?.displayNameFromDS || + customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames) + ); + + const fillBelowDispName = fillBelowToField + ? getFieldDisplayName(fillBelowToField, frame, allFrames) + : customConfig.fillBelowTo; + + const t = indexByName.get(dispName); + const b = indexByName.get(fillBelowDispName); + if (isNumber(b) && isNumber(t)) { + builder.addBand({ + series: [t, b], + fill: undefined, // using null will have the band use fill options from `t` + }); + + if (!fillOpacity) { + fillOpacity = 35; // default from flot + } + } else { + fillOpacity = 0; + } + } + } + + let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined; + + if (colorMode.id === FieldColorModeId.Thresholds) { + dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color; + } + + builder.addSeries({ + pathBuilder, + pointsBuilder, + scaleKey, + showPoints, + pointsFilter, + colorMode, + fillOpacity, + theme, + dynamicSeriesColor, + drawStyle: customConfig.drawStyle!, + lineColor: customConfig.lineColor ?? seriesColor, + lineWidth: customConfig.lineWidth, + lineInterpolation: customConfig.lineInterpolation, + lineStyle: customConfig.lineStyle, + barAlignment: customConfig.barAlignment, + barWidthFactor: customConfig.barWidthFactor, + barMaxWidth: customConfig.barMaxWidth, + pointSize: customConfig.pointSize, + spanNulls: customConfig.spanNulls || false, + show: !customConfig.hideFrom?.viz, + gradientMode: customConfig.gradientMode, + thresholds: config.thresholds, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + 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, + }); + + // Render thresholds in graph + if (customConfig.thresholdsStyle && config.thresholds) { + const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphTresholdsStyleMode.Off; + if (thresholdDisplay !== GraphTresholdsStyleMode.Off) { + builder.addThresholds({ + config: customConfig.thresholdsStyle, + thresholds: config.thresholds, + scaleKey, + theme, + hardMin: field.config.min, + hardMax: field.config.max, + softMin: customConfig.axisSoftMin, + softMax: customConfig.axisSoftMax, + }); + } + } + } + + let stackingGroups = getStackingGroups(frame); + + builder.setStackingGroups(stackingGroups); + + // hook up custom/composite renderers + renderers?.forEach((r) => { + if (!indexByName) { + indexByName = getNamesToFieldIndex(frame, allFrames); + } + let fieldIndices: Record = {}; + + for (let key in r.fieldMap) { + let dispName = r.fieldMap[key]; + fieldIndices[key] = indexByName.get(dispName)!; + } + + r.init(builder, fieldIndices); + }); + + builder.scaleKeys = [xScaleKey, yScaleKey]; + + // if hovered value is null, how far we may scan left/right to hover nearest non-null + const hoverProximityPx = 15; + + let cursor: Partial = { + // this scans left and right from cursor position to find nearest data index with value != null + // TODO: do we want to only scan past undefined values, but halt at explicit null values? + dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => { + let seriesData = self.data[seriesIdx]; + + if (seriesData[hoveredIdx] == null) { + let nonNullLft = null, + nonNullRgt = null, + i; + + i = hoveredIdx; + while (nonNullLft == null && i-- > 0) { + if (seriesData[i] != null) { + nonNullLft = i; + } + } + + i = hoveredIdx; + while (nonNullRgt == null && i++ < seriesData.length) { + if (seriesData[i] != null) { + nonNullRgt = i; + } + } + + let xVals = self.data[0]; + + let curPos = self.valToPos(cursorXVal, 'x'); + let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x'); + let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x'); + + let lftDelta = curPos - lftPos; + let rgtDelta = rgtPos - curPos; + + if (lftDelta <= rgtDelta) { + if (lftDelta <= hoverProximityPx) { + hoveredIdx = nonNullLft!; + } + } else { + if (rgtDelta <= hoverProximityPx) { + hoveredIdx = nonNullRgt!; + } + } + } + + return hoveredIdx; + }, + }; + + if (sync && sync() !== DashboardCursorSync.Off) { + const payload: DataHoverPayload = { + point: { + [xScaleKey]: null, + [yScaleKey]: null, + }, + data: frame, + }; + + const hoverEvent = new DataHoverEvent(payload); + cursor.sync = { + key: eventsScope, + filters: { + pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => { + if (sync && sync() === DashboardCursorSync.Off) { + return false; + } + + payload.rowIndex = dataIdx; + if (x < 0 && y < 0) { + payload.point[xScaleUnit] = null; + payload.point[yScaleKey] = null; + eventBus.publish(new DataHoverClearEvent()); + } else { + // convert the points + payload.point[xScaleUnit] = src.posToVal(x, xScaleKey); + payload.point[yScaleKey] = src.posToVal(y, yScaleKey); + payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip + eventBus.publish(hoverEvent); + hoverEvent.payload.down = undefined; + } + return true; + }, + }, + scales: [xScaleKey, yScaleKey], + // match: [() => true, (a, b) => a === b], + }; + } + + builder.setSync(); + builder.setCursor(cursor); + + return builder; +}; + +export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map { + const originNames = new Map(); + frame.fields.forEach((field, i) => { + const origin = field.state?.origin; + if (origin) { + const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex]; + if (origField) { + originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i); + } + } + }); + return originNames; +} diff --git a/public/app/core/components/TimelineChart/TimelineChart.tsx b/public/app/core/components/TimelineChart/TimelineChart.tsx index ac5e2e62769..e904530a82e 100644 --- a/public/app/core/components/TimelineChart/TimelineChart.tsx +++ b/public/app/core/components/TimelineChart/TimelineChart.tsx @@ -2,16 +2,9 @@ import React from 'react'; import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data'; import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema'; -import { - PanelContext, - PanelContextRoot, - GraphNG, - GraphNGProps, - UPlotConfigBuilder, - VizLayout, - VizLegend, - VizLegendItem, -} from '@grafana/ui'; +import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; + +import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG'; import { preparePlotConfigBuilder, TimelineMode } from './utils'; diff --git a/public/app/core/components/TimelineChart/timeline.ts b/public/app/core/components/TimelineChart/timeline.ts index 18ecfdbf33d..13fdd76fb4d 100644 --- a/public/app/core/components/TimelineChart/timeline.ts +++ b/public/app/core/components/TimelineChart/timeline.ts @@ -3,7 +3,7 @@ import uPlot, { Series } from 'uplot'; import { GrafanaTheme2, TimeRange } from '@grafana/data'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema'; -import { FIXED_UNIT } from '@grafana/ui/src/components/GraphNG/GraphNG'; +import { FIXED_UNIT } from '@grafana/ui'; import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute'; import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree'; import { FieldConfig as StateTimeLineFieldConfig } from 'app/plugins/panel/state-timeline/panelcfg.gen'; diff --git a/public/app/core/components/TimelineChart/utils.ts b/public/app/core/components/TimelineChart/utils.ts index 02048cca457..a372190209b 100644 --- a/public/app/core/components/TimelineChart/utils.ts +++ b/public/app/core/components/TimelineChart/utils.ts @@ -23,6 +23,8 @@ import { TimeRange, } from '@grafana/data'; import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { VizLegendOptions, AxisPlacement, @@ -40,8 +42,6 @@ import { UPlotConfigPrepFn, VizLegendItem, } from '@grafana/ui'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; -import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types'; import { preparePlotData2, getStackingGroups } from '@grafana/ui/src/components/uPlot/utils'; diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index c3ea72f4b21..27d7a366e17 100644 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -16,8 +16,6 @@ import { PanelDataErrorView } from '@grafana/runtime'; import { SortOrder } from '@grafana/schema'; import { GraphGradientMode, - GraphNG, - GraphNGProps, measureText, PlotLegend, Portal, @@ -31,9 +29,9 @@ import { VizLegend, VizTooltipContainer, } from '@grafana/ui'; -import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG'; import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG'; import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils'; import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView'; diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx index e0707a3820f..a0396c4a01b 100644 --- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx +++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx @@ -7,9 +7,10 @@ import uPlot from 'uplot'; import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; +import { TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui'; import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder'; import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin'; diff --git a/public/app/plugins/panel/graph/data_processor.ts b/public/app/plugins/panel/graph/data_processor.ts index 8f67e3abb5c..21afccf65d3 100644 --- a/public/app/plugins/panel/graph/data_processor.ts +++ b/public/app/plugins/panel/graph/data_processor.ts @@ -1,8 +1,8 @@ import { find } from 'lodash'; import { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; import { colors } from '@grafana/ui'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 752fc49ba3f..f9d57f04578 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -3,7 +3,8 @@ import React, { useMemo } from 'react'; import { PanelProps, DataFrameType } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; import { TooltipDisplayMode } from '@grafana/schema'; -import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { KeyboardPlugin, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { config } from 'app/core/config'; import { Options } from './panelcfg.gen'; diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 321dbfb1cec..754e052c7b9 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -9,7 +9,7 @@ import { TIME_SERIES_VALUE_FIELD_NAME, TimeZone, } from '@grafana/data'; -import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui'; +import { FIXED_UNIT, EventsCanvas, UPlotConfigBuilder } from '@grafana/ui'; import { ExemplarMarker } from './ExemplarMarker'; diff --git a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx index 3db8c039e8d..a286ce7612f 100644 --- a/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ThresholdControlsPlugin.tsx @@ -2,7 +2,8 @@ import React, { useState, useLayoutEffect, useMemo, useRef } from 'react'; import uPlot from 'uplot'; import { FieldConfigSource, ThresholdsConfig, getValueFormat, FieldType } from '@grafana/data'; -import { UPlotConfigBuilder, buildScaleKey } from '@grafana/ui'; +import { UPlotConfigBuilder } from '@grafana/ui'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; import { ThresholdDragHandle } from './ThresholdDragHandle'; diff --git a/public/app/plugins/panel/timeseries/utils.ts b/public/app/plugins/panel/timeseries/utils.ts index 27cb4845a6a..871cf62b599 100644 --- a/public/app/plugins/panel/timeseries/utils.ts +++ b/public/app/plugins/panel/timeseries/utils.ts @@ -13,10 +13,10 @@ import { TimeRange, } from '@grafana/data'; import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; +import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; +import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { GraphFieldConfig, LineInterpolation } from '@grafana/schema'; -import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold'; -import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue'; -import { buildScaleKey } from '@grafana/ui/src/components/GraphNG/utils'; +import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; type ScaleKey = string; diff --git a/public/app/plugins/panel/trend/TrendPanel.tsx b/public/app/plugins/panel/trend/TrendPanel.tsx index 371b0679a65..e5371dca263 100644 --- a/public/app/plugins/panel/trend/TrendPanel.tsx +++ b/public/app/plugins/panel/trend/TrendPanel.tsx @@ -3,15 +3,10 @@ import React, { useMemo } from 'react'; import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { config, PanelDataErrorView } from '@grafana/runtime'; -import { - KeyboardPlugin, - preparePlotFrame, - TimeSeries, - TooltipDisplayMode, - TooltipPlugin, - usePanelContext, -} from '@grafana/ui'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; +import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui'; +import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; +import { preparePlotFrame } from 'app/core/components/GraphNG/utils'; +import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; import { findFieldIndex } from 'app/features/dimensions'; import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils'; diff --git a/public/app/plugins/panel/xychart/dims.ts b/public/app/plugins/panel/xychart/dims.ts index 30320448330..9b2619b64ab 100644 --- a/public/app/plugins/panel/xychart/dims.ts +++ b/public/app/plugins/panel/xychart/dims.ts @@ -1,5 +1,5 @@ import { DataFrame, Field, FieldMatcher, FieldType, getFieldDisplayName } from '@grafana/data'; -import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types'; +import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { XYDimensionConfig } from './panelcfg.gen';