Commit 2d156a38 by Leon Sorokin Committed by GitHub

GraphNG: fix and optimize spanNulls (#29633)

* fix and optimize spanNulls

* AsZero implies spanNulls = true, to prevent null-scanning

* move spanNulls toggle below fillOpacity
parent b66bc4a7
...@@ -147,6 +147,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -147,6 +147,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
pointColor: seriesColor, pointColor: seriesColor,
fillOpacity: customConfig.fillOpacity, fillOpacity: customConfig.fillOpacity,
fillColor: seriesColor, fillColor: seriesColor,
spanNulls: customConfig.spanNulls || false,
}); });
if (hasLegend.current) { if (hasLegend.current) {
......
...@@ -43,7 +43,7 @@ export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?: ...@@ -43,7 +43,7 @@ export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?:
export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null { export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null {
const valuesFromFrames: AlignedData[] = []; const valuesFromFrames: AlignedData[] = [];
const sourceFields: Field[] = []; const sourceFields: Field[] = [];
let spanNulls = false; const skipGaps: boolean[][] = [];
// Default to timeseries config // Default to timeseries config
if (!fields) { if (!fields) {
...@@ -55,6 +55,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -55,6 +55,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
for (const frame of frames) { for (const frame of frames) {
const dims = mapDimesions(fields, frame, frames); const dims = mapDimesions(fields, frame, frames);
if (!(dims.x.length && dims.y.length)) { if (!(dims.x.length && dims.y.length)) {
continue; // both x and y matched something! continue; // both x and y matched something!
} }
...@@ -63,9 +64,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -63,9 +64,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
throw new Error('Only a single x field is supported'); throw new Error('Only a single x field is supported');
} }
let skipGapsFrame: boolean[] = [];
// Add the first X axis // Add the first X axis
if (!sourceFields.length) { if (!sourceFields.length) {
sourceFields.push(dims.x[0]); sourceFields.push(dims.x[0]);
skipGapsFrame.push(true);
} }
const alignedData: AlignedData = [ const alignedData: AlignedData = [
...@@ -75,21 +79,23 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -75,21 +79,23 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
// Add the Y values // Add the Y values
for (const field of dims.y) { for (const field of dims.y) {
let values = field.values.toArray(); let values = field.values.toArray();
let spanNulls = field.config.custom.spanNulls || false;
if (field.config.nullValueMode === NullValueMode.AsZero) { if (field.config.nullValueMode === NullValueMode.AsZero) {
values = values.map(v => (v === null ? 0 : v)); values = values.map(v => (v === null ? 0 : v));
}
alignedData.push(values);
if (field.config.custom.spanNulls) {
spanNulls = true; spanNulls = true;
} }
alignedData.push(values);
skipGapsFrame.push(spanNulls);
// This will cache an appropriate field name in the field state // This will cache an appropriate field name in the field state
getFieldDisplayName(field, frame, frames); getFieldDisplayName(field, frame, frames);
sourceFields.push(field); sourceFields.push(field);
} }
valuesFromFrames.push(alignedData); valuesFromFrames.push(alignedData);
skipGaps.push(skipGapsFrame);
} }
if (valuesFromFrames.length === 0) { if (valuesFromFrames.length === 0) {
...@@ -97,22 +103,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -97,22 +103,12 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
} }
// do the actual alignment (outerJoin on the first arrays) // 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) { if (alignedData!.length !== sourceFields.length) {
throw new Error('outerJoinValues lost a field?'); 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 // Replace the values from the outer-join field
return { return {
frame: { frame: {
...@@ -126,18 +122,20 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -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) { if (tables.length === 1) {
return { return {
data: tables[0], data: tables[0],
isGap: () => true, isGap: skipGaps ? (u: uPlot, seriesIdx: number, dataIdx: number) => !skipGaps[0][seriesIdx] : () => true,
}; };
} }
let xVals: Set<number> = new Set(); let xVals: Set<number> = new Set();
let xNulls: Array<Set<number>> = [new Set()]; let xNulls: Array<Set<number>> = [new Set()];
for (const t of tables) { for (let ti = 0; ti < tables.length; ti++) {
let t = tables[ti];
let xs = t[0]; let xs = t[0];
let len = xs.length; let len = xs.length;
let nulls: Set<number> = new Set(); let nulls: Set<number> = new Set();
...@@ -147,11 +145,13 @@ export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest { ...@@ -147,11 +145,13 @@ export function outerJoinValues(tables: AlignedData[]): AlignedDataWithGapTest {
} }
for (let j = 1; j < t.length; j++) { 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++) { for (let i = 0; i < len; i++) {
if (ys[i] == null) { if (ys[i] == null) {
nulls.add(xs[i]); nulls.add(xs[i]);
}
} }
} }
} }
......
...@@ -32,6 +32,7 @@ export interface LineConfig { ...@@ -32,6 +32,7 @@ export interface LineConfig {
lineColor?: string; lineColor?: string;
lineWidth?: number; lineWidth?: number;
lineInterpolation?: LineInterpolation; lineInterpolation?: LineInterpolation;
spanNulls?: boolean;
} }
export interface AreaConfig { export interface AreaConfig {
...@@ -55,7 +56,6 @@ export interface AxisConfig { ...@@ -55,7 +56,6 @@ export interface AxisConfig {
export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, AxisConfig { export interface GraphFieldConfig extends LineConfig, AreaConfig, PointsConfig, AxisConfig {
drawStyle?: DrawStyle; drawStyle?: DrawStyle;
spanNulls?: boolean;
} }
export const graphFieldOptions = { export const graphFieldOptions = {
......
...@@ -130,6 +130,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -130,6 +130,7 @@ describe('UPlotConfigBuilder', () => {
pointColor: '#00ff00', pointColor: '#00ff00',
lineColor: '#0000ff', lineColor: '#0000ff',
lineWidth: 1, lineWidth: 1,
spanNulls: false,
}); });
expect(builder.getConfig()).toMatchInlineSnapshot(` expect(builder.getConfig()).toMatchInlineSnapshot(`
...@@ -147,6 +148,7 @@ describe('UPlotConfigBuilder', () => { ...@@ -147,6 +148,7 @@ describe('UPlotConfigBuilder', () => {
"stroke": "#00ff00", "stroke": "#00ff00",
}, },
"scale": "scale-x", "scale": "scale-x",
"spanGaps": false,
"stroke": "#0000ff", "stroke": "#0000ff",
"width": 1, "width": 1,
}, },
......
...@@ -22,6 +22,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -22,6 +22,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
fillColor, fillColor,
fillOpacity, fillOpacity,
scaleKey, scaleKey,
spanNulls,
} = this.props; } = this.props;
let lineConfig: Partial<Series> = {}; let lineConfig: Partial<Series> = {};
...@@ -87,6 +88,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -87,6 +88,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
return { return {
scale: scaleKey, scale: scaleKey,
spanGaps: spanNulls,
...lineConfig, ...lineConfig,
...pointsConfig, ...pointsConfig,
...areaConfig, ...areaConfig,
......
...@@ -64,6 +64,18 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel) ...@@ -64,6 +64,18 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
showIf: c => c.drawStyle !== DrawStyle.Points, showIf: c => c.drawStyle !== DrawStyle.Points,
}) })
.addRadio({ .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', path: 'points',
name: 'Points', name: 'Points',
defaultValue: graphFieldOptions.points[0].value, defaultValue: graphFieldOptions.points[0].value,
...@@ -83,17 +95,6 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel) ...@@ -83,17 +95,6 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
showIf: c => c.points !== PointMode.Never, showIf: c => c.points !== PointMode.Never,
}) })
.addRadio({ .addRadio({
path: 'spanNulls',
name: 'Null values',
defaultValue: false,
settings: {
options: [
{ label: 'Gaps', value: false },
{ label: 'Connected', value: true },
],
},
})
.addRadio({
path: 'axisPlacement', path: 'axisPlacement',
name: 'Placement', name: 'Placement',
category: ['Axis'], category: ['Axis'],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment