From 2d156a385b8b1b9e0a7f85e5a1624e665fa94dab Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Sun, 6 Dec 2020 19:26:00 -0600 Subject: [PATCH] GraphNG: fix and optimize spanNulls (#29633) * fix and optimize spanNulls * AsZero implies spanNulls = true, to prevent null-scanning * move spanNulls toggle below fillOpacity --- .../src/components/GraphNG/GraphNG.tsx | 1 + .../src/components/GraphNG/utils.ts | 46 +++++++++---------- .../grafana-ui/src/components/uPlot/config.ts | 2 +- .../uPlot/config/UPlotConfigBuilder.test.ts | 2 + .../uPlot/config/UPlotSeriesBuilder.ts | 2 + public/app/plugins/panel/graph3/module.tsx | 23 +++++----- 6 files changed, 41 insertions(+), 35 deletions(-) diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 293e82a3c75..d07cb0efa4c 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -147,6 +147,7 @@ export const GraphNG: React.FC = ({ pointColor: seriesColor, fillOpacity: customConfig.fillOpacity, fillColor: seriesColor, + spanNulls: customConfig.spanNulls || false, }); if (hasLegend.current) { diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 5b1ade91f54..2d6376708f6 100755 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -43,7 +43,7 @@ export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?: export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null { const valuesFromFrames: AlignedData[] = []; const sourceFields: Field[] = []; - let spanNulls = false; + const skipGaps: boolean[][] = []; // Default to timeseries config if (!fields) { @@ -55,6 +55,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): for (const frame of frames) { const dims = mapDimesions(fields, frame, frames); + if (!(dims.x.length && dims.y.length)) { continue; // both x and y matched something! } @@ -63,9 +64,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): throw new Error('Only a single x field is supported'); } + let skipGapsFrame: boolean[] = []; + // Add the first X axis if (!sourceFields.length) { sourceFields.push(dims.x[0]); + skipGapsFrame.push(true); } const alignedData: AlignedData = [ @@ -75,21 +79,23 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): // Add the Y values for (const field of dims.y) { let values = field.values.toArray(); + let spanNulls = field.config.custom.spanNulls || false; + if (field.config.nullValueMode === NullValueMode.AsZero) { values = values.map(v => (v === null ? 0 : v)); - } - alignedData.push(values); - - if (field.config.custom.spanNulls) { spanNulls = true; } + alignedData.push(values); + skipGapsFrame.push(spanNulls); + // This will cache an appropriate field name in the field state getFieldDisplayName(field, frame, frames); sourceFields.push(field); } valuesFromFrames.push(alignedData); + skipGaps.push(skipGapsFrame); } if (valuesFromFrames.length === 0) { @@ -97,22 +103,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): } // do the actual alignment (outerJoin on the first arrays) - let { data: alignedData, isGap } = outerJoinValues(valuesFromFrames); + let { data: alignedData, isGap } = outerJoinValues(valuesFromFrames, skipGaps); if (alignedData!.length !== sourceFields.length) { throw new Error('outerJoinValues lost a field?'); } - // Wrap the gap function when span nulls exists - if (spanNulls) { - isGap = (u: uPlot, seriesIdx: number, dataIdx: number) => { - if (sourceFields[seriesIdx].config?.custom?.spanNulls) { - return false; - } - return isGap(u, seriesIdx, dataIdx); - }; - } - // Replace the values from the outer-join field return { frame: { @@ -126,18 +122,20 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): }; } -export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest { +// skipGaps is a tables-matched bool array indicating which series can skip storing indices of original nulls +export function outerJoinValues(tables: AlignedData[], skipGaps?: boolean[][]): AlignedDataWithGapTest { if (tables.length === 1) { return { data: tables[0], - isGap: () => true, + isGap: skipGaps ? (u: uPlot, seriesIdx: number, dataIdx: number) => !skipGaps[0][seriesIdx] : () => true, }; } let xVals: Set = new Set(); let xNulls: Array> = [new Set()]; - for (const t of tables) { + for (let ti = 0; ti < tables.length; ti++) { + let t = tables[ti]; let xs = t[0]; let len = xs.length; let nulls: Set = new Set(); @@ -147,11 +145,13 @@ export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest { } for (let j = 1; j < t.length; j++) { - let ys = t[j]; + if (skipGaps == null || !skipGaps[ti][j]) { + let ys = t[j]; - for (let i = 0; i < len; i++) { - if (ys[i] == null) { - nulls.add(xs[i]); + for (let i = 0; i < len; i++) { + if (ys[i] == null) { + nulls.add(xs[i]); + } } } } diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index d70c32b293f..a793a7ab472 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -32,6 +32,7 @@ export interface LineConfig { lineColor?: string; lineWidth?: number; lineInterpolation?: LineInterpolation; + spanNulls?: boolean; } export interface AreaConfig { @@ -55,7 +56,6 @@ export interface AxisConfig { export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, AxisConfig { drawStyle?: DrawStyle; - spanNulls?: boolean; } export const graphFieldOptions = { diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 51b9d738633..397e650ee8b 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -130,6 +130,7 @@ describe('UPlotConfigBuilder', () => { pointColor: '#00ff00', lineColor: '#0000ff', lineWidth: 1, + spanNulls: false, }); expect(builder.getConfig()).toMatchInlineSnapshot(` @@ -147,6 +148,7 @@ describe('UPlotConfigBuilder', () => { "stroke": "#00ff00", }, "scale": "scale-x", + "spanGaps": false, "stroke": "#0000ff", "width": 1, }, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index 79e5f73afaa..30922ea3007 100755 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -22,6 +22,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { fillColor, fillOpacity, scaleKey, + spanNulls, } = this.props; let lineConfig: Partial = {}; @@ -87,6 +88,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { return { scale: scaleKey, + spanGaps: spanNulls, ...lineConfig, ...pointsConfig, ...areaConfig, diff --git a/public/app/plugins/panel/graph3/module.tsx b/public/app/plugins/panel/graph3/module.tsx index ea677f3e377..87facfe39a1 100644 --- a/public/app/plugins/panel/graph3/module.tsx +++ b/public/app/plugins/panel/graph3/module.tsx @@ -63,6 +63,18 @@ export const plugin = new PanelPlugin(GraphPanel) }, showIf: c => c.drawStyle !== DrawStyle.Points, }) + .addRadio({ + path: 'spanNulls', + name: 'Null values', + defaultValue: false, + settings: { + options: [ + { label: 'Gaps', value: false }, + { label: 'Connected', value: true }, + ], + }, + showIf: c => c.drawStyle === DrawStyle.Line, + }) .addRadio({ path: 'points', name: 'Points', @@ -82,17 +94,6 @@ export const plugin = new PanelPlugin(GraphPanel) }, showIf: c => c.points !== PointMode.Never, }) - .addRadio({ - path: 'spanNulls', - name: 'Null values', - defaultValue: false, - settings: { - options: [ - { label: 'Gaps', value: false }, - { label: 'Connected', value: true }, - ], - }, - }) .addRadio({ path: 'axisPlacement', name: 'Placement',