mirror of
https://github.com/grafana/grafana.git
synced 2025-01-07 22:53:56 -06:00
GraphNG: fix and optimize spanNulls (#29633)
* fix and optimize spanNulls * AsZero implies spanNulls = true, to prevent null-scanning * move spanNulls toggle below fillOpacity
This commit is contained in:
parent
b66bc4a7b1
commit
2d156a385b
@ -147,6 +147,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
pointColor: seriesColor,
|
||||
fillOpacity: customConfig.fillOpacity,
|
||||
fillColor: seriesColor,
|
||||
spanNulls: customConfig.spanNulls || false,
|
||||
});
|
||||
|
||||
if (hasLegend.current) {
|
||||
|
@ -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<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 len = xs.length;
|
||||
let nulls: Set<number> = 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -22,6 +22,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
fillColor,
|
||||
fillOpacity,
|
||||
scaleKey,
|
||||
spanNulls,
|
||||
} = this.props;
|
||||
|
||||
let lineConfig: Partial<Series> = {};
|
||||
@ -87,6 +88,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
||||
|
||||
return {
|
||||
scale: scaleKey,
|
||||
spanGaps: spanNulls,
|
||||
...lineConfig,
|
||||
...pointsConfig,
|
||||
...areaConfig,
|
||||
|
@ -63,6 +63,18 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(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<Options, GraphFieldConfig>(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',
|
||||
|
Loading…
Reference in New Issue
Block a user