mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: add gaps/nulls support to staircase & smooth interpolation modes (#29593)
This commit is contained in:
parent
f9c8d5ab49
commit
7236a44a4f
@ -71,7 +71,7 @@
|
|||||||
"react-transition-group": "4.4.1",
|
"react-transition-group": "4.4.1",
|
||||||
"slate": "0.47.8",
|
"slate": "0.47.8",
|
||||||
"tinycolor2": "1.4.1",
|
"tinycolor2": "1.4.1",
|
||||||
"uplot": "1.4.6"
|
"uplot": "1.4.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-commonjs": "16.0.0",
|
"@rollup/plugin-commonjs": "16.0.0",
|
||||||
|
@ -23,8 +23,9 @@ export enum GraphMode {
|
|||||||
|
|
||||||
export enum LineInterpolation {
|
export enum LineInterpolation {
|
||||||
Linear = 'linear',
|
Linear = 'linear',
|
||||||
Staircase = 'staircase', // https://leeoniya.github.io/uPlot/demos/line-stepped.html
|
Smooth = 'smooth',
|
||||||
Smooth = 'smooth', // https://leeoniya.github.io/uPlot/demos/line-smoothing.html
|
StepBefore = 'stepBefore',
|
||||||
|
StepAfter = 'stepAfter',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LineConfig {
|
export interface LineConfig {
|
||||||
@ -65,8 +66,9 @@ export const graphFieldOptions = {
|
|||||||
|
|
||||||
lineInterpolation: [
|
lineInterpolation: [
|
||||||
{ label: 'Linear', value: LineInterpolation.Linear },
|
{ label: 'Linear', value: LineInterpolation.Linear },
|
||||||
{ label: 'Staircase', value: LineInterpolation.Staircase },
|
|
||||||
{ label: 'Smooth', value: LineInterpolation.Smooth },
|
{ label: 'Smooth', value: LineInterpolation.Smooth },
|
||||||
|
{ label: 'Step Before', value: LineInterpolation.StepBefore },
|
||||||
|
{ label: 'Step After', value: LineInterpolation.StepAfter },
|
||||||
] as Array<SelectableValue<LineInterpolation>>,
|
] as Array<SelectableValue<LineInterpolation>>,
|
||||||
|
|
||||||
points: [
|
points: [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import uPlot, { Series } from 'uplot';
|
import uPlot, { Series } from 'uplot';
|
||||||
import { GraphMode, LineConfig, AreaConfig, PointsConfig, PointMode, LineInterpolation } from '../config';
|
import { GraphMode, LineConfig, AreaConfig, PointsConfig, PointMode, LineInterpolation } from '../config';
|
||||||
import { barsBuilder, smoothBuilder, staircaseBuilder } from '../paths';
|
import { barsBuilder, smoothBuilder, stepBeforeBuilder, stepAfterBuilder } from '../paths';
|
||||||
import { PlotConfigBuilder } from '../types';
|
import { PlotConfigBuilder } from '../types';
|
||||||
|
|
||||||
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
|
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
|
||||||
@ -31,20 +31,29 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
|
|||||||
} else {
|
} else {
|
||||||
lineConfig.stroke = lineColor;
|
lineConfig.stroke = lineColor;
|
||||||
lineConfig.width = lineWidth;
|
lineConfig.width = lineWidth;
|
||||||
lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
|
lineConfig.paths = (
|
||||||
|
self: uPlot,
|
||||||
|
seriesIdx: number,
|
||||||
|
idx0: number,
|
||||||
|
idx1: number,
|
||||||
|
extendGap: Series.ExtendGap,
|
||||||
|
buildClip: Series.BuildClip
|
||||||
|
) => {
|
||||||
let pathsBuilder = self.paths;
|
let pathsBuilder = self.paths;
|
||||||
|
|
||||||
if (mode === GraphMode.Bars) {
|
if (mode === GraphMode.Bars) {
|
||||||
pathsBuilder = barsBuilder;
|
pathsBuilder = barsBuilder;
|
||||||
} else if (mode === GraphMode.Line) {
|
} else if (mode === GraphMode.Line) {
|
||||||
if (lineInterpolation === LineInterpolation.Staircase) {
|
if (lineInterpolation === LineInterpolation.StepBefore) {
|
||||||
pathsBuilder = staircaseBuilder;
|
pathsBuilder = stepBeforeBuilder;
|
||||||
|
} else if (lineInterpolation === LineInterpolation.StepAfter) {
|
||||||
|
pathsBuilder = stepAfterBuilder;
|
||||||
} else if (lineInterpolation === LineInterpolation.Smooth) {
|
} else if (lineInterpolation === LineInterpolation.Smooth) {
|
||||||
pathsBuilder = smoothBuilder;
|
pathsBuilder = smoothBuilder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return pathsBuilder(self, seriesIdx, idx0, idx1);
|
return pathsBuilder(self, seriesIdx, idx0, idx1, extendGap, buildClip);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import uPlot, { Series } from 'uplot';
|
import uPlot, { Series } from 'uplot';
|
||||||
|
|
||||||
export const barsBuilder: Series.PathBuilder = (u: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
|
export const barsBuilder: Series.PathBuilder = (
|
||||||
|
u: uPlot,
|
||||||
|
seriesIdx: number,
|
||||||
|
idx0: number,
|
||||||
|
idx1: number,
|
||||||
|
extendGap: Series.ExtendGap,
|
||||||
|
buildClip: Series.BuildClip
|
||||||
|
) => {
|
||||||
const series = u.series[seriesIdx];
|
const series = u.series[seriesIdx];
|
||||||
const xdata = u.data[0];
|
const xdata = u.data[0];
|
||||||
const ydata = u.data[seriesIdx];
|
const ydata = u.data[seriesIdx];
|
||||||
@ -53,47 +60,115 @@ export const barsBuilder: Series.PathBuilder = (u: uPlot, seriesIdx: number, idx
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const staircaseBuilder: Series.PathBuilder = (u: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
|
/*
|
||||||
const series = u.series[seriesIdx];
|
const enum StepSide {
|
||||||
const xdata = u.data[0];
|
Before,
|
||||||
const ydata = u.data[seriesIdx];
|
After,
|
||||||
const scaleX = u.series[0].scale as string;
|
}
|
||||||
const scaleY = series.scale as string;
|
*/
|
||||||
|
|
||||||
const stroke = new Path2D();
|
export const stepBeforeBuilder = stepBuilderFactory(false);
|
||||||
stroke.moveTo(Math.round(u.valToPos(xdata[0], scaleX, true)), Math.round(u.valToPos(ydata[0]!, scaleY, true)));
|
export const stepAfterBuilder = stepBuilderFactory(true);
|
||||||
|
|
||||||
for (let i = idx0; i <= idx1 - 1; i++) {
|
// babel does not support inlined const enums, so this uses a boolean flag for perf
|
||||||
let x0 = Math.round(u.valToPos(xdata[i], scaleX, true));
|
// possible workaround: https://github.com/dosentmatter/babel-plugin-const-enum
|
||||||
let y0 = Math.round(u.valToPos(ydata[i]!, scaleY, true));
|
function stepBuilderFactory(after: boolean): Series.PathBuilder {
|
||||||
let x1 = Math.round(u.valToPos(xdata[i + 1], scaleX, true));
|
return (
|
||||||
let y1 = Math.round(u.valToPos(ydata[i + 1]!, scaleY, true));
|
u: uPlot,
|
||||||
|
seriesIdx: number,
|
||||||
|
idx0: number,
|
||||||
|
idx1: number,
|
||||||
|
extendGap: Series.ExtendGap,
|
||||||
|
buildClip: Series.BuildClip
|
||||||
|
) => {
|
||||||
|
const series = u.series[seriesIdx];
|
||||||
|
const xdata = u.data[0];
|
||||||
|
const ydata = u.data[seriesIdx];
|
||||||
|
const scaleX = u.series[0].scale as string;
|
||||||
|
const scaleY = series.scale as string;
|
||||||
|
const halfStroke = series.width! / 2;
|
||||||
|
|
||||||
stroke.lineTo(x0, y0);
|
const stroke = new Path2D();
|
||||||
stroke.lineTo(x1, y0);
|
|
||||||
|
|
||||||
if (i === idx1 - 1) {
|
// find first non-null dataPt
|
||||||
stroke.lineTo(x1, y1);
|
while (ydata[idx0] == null) {
|
||||||
|
idx0++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const fill = new Path2D(stroke);
|
// find last-null dataPt
|
||||||
|
while (ydata[idx1] == null) {
|
||||||
|
idx1--;
|
||||||
|
}
|
||||||
|
|
||||||
//@ts-ignore
|
let gaps: Series.Gaps = [];
|
||||||
let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
|
let inGap = false;
|
||||||
|
let prevYPos = Math.round(u.valToPos(ydata[idx0]!, scaleY, true));
|
||||||
|
let firstXPos = Math.round(u.valToPos(xdata[idx0], scaleX, true));
|
||||||
|
let prevXPos = firstXPos;
|
||||||
|
|
||||||
let minY = Math.round(u.valToPos(fillTo, scaleY, true));
|
stroke.moveTo(firstXPos, prevYPos);
|
||||||
let minX = Math.round(u.valToPos(u.scales[scaleX].min!, scaleX, true));
|
|
||||||
let maxX = Math.round(u.valToPos(u.scales[scaleX].max!, scaleX, true));
|
|
||||||
|
|
||||||
fill.lineTo(maxX, minY);
|
for (let i = idx0 + 1; i <= idx1; i++) {
|
||||||
fill.lineTo(minX, minY);
|
let yVal1 = ydata[i];
|
||||||
|
|
||||||
return {
|
let x1 = Math.round(u.valToPos(xdata[i], scaleX, true));
|
||||||
stroke,
|
|
||||||
fill,
|
if (yVal1 == null) {
|
||||||
|
//@ts-ignore
|
||||||
|
if (series.isGap(u, seriesIdx, i)) {
|
||||||
|
extendGap(gaps, prevXPos, x1);
|
||||||
|
inGap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let y1 = Math.round(u.valToPos(yVal1, scaleY, true));
|
||||||
|
|
||||||
|
if (inGap) {
|
||||||
|
extendGap(gaps, prevXPos, x1);
|
||||||
|
|
||||||
|
// don't clip vertical extenders
|
||||||
|
if (prevYPos !== y1) {
|
||||||
|
let lastGap = gaps[gaps.length - 1];
|
||||||
|
lastGap[0] += halfStroke;
|
||||||
|
lastGap[1] -= halfStroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
inGap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (after) {
|
||||||
|
stroke.lineTo(x1, prevYPos);
|
||||||
|
} else {
|
||||||
|
stroke.lineTo(prevXPos, y1);
|
||||||
|
}
|
||||||
|
|
||||||
|
stroke.lineTo(x1, y1);
|
||||||
|
|
||||||
|
prevYPos = y1;
|
||||||
|
prevXPos = x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fill = new Path2D(stroke);
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
|
||||||
|
|
||||||
|
let minY = Math.round(u.valToPos(fillTo, scaleY, true));
|
||||||
|
|
||||||
|
fill.lineTo(prevXPos, minY);
|
||||||
|
fill.lineTo(firstXPos, minY);
|
||||||
|
|
||||||
|
let clip = !series.spanGaps ? buildClip(gaps) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
stroke,
|
||||||
|
fill,
|
||||||
|
clip,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
// adapted from https://gist.github.com/nicholaswmin/c2661eb11cad5671d816 (MIT)
|
// adapted from https://gist.github.com/nicholaswmin/c2661eb11cad5671d816 (MIT)
|
||||||
/**
|
/**
|
||||||
@ -217,26 +292,63 @@ function catmullRomFitting(xCoords: number[], yCoords: number[], alpha: number)
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const smoothBuilder: Series.PathBuilder = (u: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
|
export const smoothBuilder: Series.PathBuilder = (
|
||||||
|
u: uPlot,
|
||||||
|
seriesIdx: number,
|
||||||
|
idx0: number,
|
||||||
|
idx1: number,
|
||||||
|
extendGap: Series.ExtendGap,
|
||||||
|
buildClip: Series.BuildClip
|
||||||
|
) => {
|
||||||
const series = u.series[seriesIdx];
|
const series = u.series[seriesIdx];
|
||||||
const xdata = u.data[0];
|
const xdata = u.data[0];
|
||||||
const ydata = u.data[seriesIdx];
|
const ydata = u.data[seriesIdx];
|
||||||
const scaleX = u.series[0].scale as string;
|
const scaleX = u.series[0].scale as string;
|
||||||
const scaleY = series.scale as string;
|
const scaleY = series.scale as string;
|
||||||
|
|
||||||
const alpha = 0.5;
|
// find first non-null dataPt
|
||||||
|
while (ydata[idx0] == null) {
|
||||||
|
idx0++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find last-null dataPt
|
||||||
|
while (ydata[idx1] == null) {
|
||||||
|
idx1--;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gaps: Series.Gaps = [];
|
||||||
|
let inGap = false;
|
||||||
|
let firstXPos = Math.round(u.valToPos(xdata[idx0], scaleX, true));
|
||||||
|
let prevXPos = firstXPos;
|
||||||
|
|
||||||
let xCoords = [];
|
let xCoords = [];
|
||||||
let yCoords = [];
|
let yCoords = [];
|
||||||
|
|
||||||
for (let i = idx0; i <= idx1; i++) {
|
for (let i = idx0; i <= idx1; i++) {
|
||||||
if (ydata[i] != null) {
|
let yVal = ydata[i];
|
||||||
xCoords.push(u.valToPos(xdata[i], scaleX, true));
|
let xVal = xdata[i];
|
||||||
|
let xPos = u.valToPos(xVal, scaleX, true);
|
||||||
|
|
||||||
|
if (yVal == null) {
|
||||||
|
//@ts-ignore
|
||||||
|
if (series.isGap(u, seriesIdx, i)) {
|
||||||
|
extendGap(gaps, prevXPos + 1, xPos);
|
||||||
|
inGap = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (inGap) {
|
||||||
|
extendGap(gaps, prevXPos + 1, xPos + 1);
|
||||||
|
inGap = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
xCoords.push((prevXPos = xPos));
|
||||||
yCoords.push(u.valToPos(ydata[i]!, scaleY, true));
|
yCoords.push(u.valToPos(ydata[i]!, scaleY, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stroke = catmullRomFitting(xCoords, yCoords, alpha);
|
const stroke = catmullRomFitting(xCoords, yCoords, 0.5);
|
||||||
|
|
||||||
const fill = new Path2D(stroke);
|
const fill = new Path2D(stroke);
|
||||||
|
|
||||||
@ -244,14 +356,15 @@ export const smoothBuilder: Series.PathBuilder = (u: uPlot, seriesIdx: number, i
|
|||||||
let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
|
let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
|
||||||
|
|
||||||
let minY = Math.round(u.valToPos(fillTo, scaleY, true));
|
let minY = Math.round(u.valToPos(fillTo, scaleY, true));
|
||||||
let minX = Math.round(u.valToPos(u.scales[scaleX].min!, scaleX, true));
|
|
||||||
let maxX = Math.round(u.valToPos(u.scales[scaleX].max!, scaleX, true));
|
|
||||||
|
|
||||||
fill.lineTo(maxX, minY);
|
fill.lineTo(prevXPos, minY);
|
||||||
fill.lineTo(minX, minY);
|
fill.lineTo(firstXPos, minY);
|
||||||
|
|
||||||
|
let clip = !series.spanGaps ? buildClip(gaps) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stroke,
|
stroke,
|
||||||
fill,
|
fill,
|
||||||
|
clip,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -25667,10 +25667,10 @@ update-notifier@^2.5.0:
|
|||||||
semver-diff "^2.0.0"
|
semver-diff "^2.0.0"
|
||||||
xdg-basedir "^3.0.0"
|
xdg-basedir "^3.0.0"
|
||||||
|
|
||||||
uplot@1.4.6:
|
uplot@1.4.7:
|
||||||
version "1.4.6"
|
version "1.4.7"
|
||||||
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.6.tgz#52b6812c06b8d3cb89d4a8d053f45415cb5d9a13"
|
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.7.tgz#feab0cf48b569184bfefacf29308deb021bad765"
|
||||||
integrity sha512-nw3LdjLFhAqKQF/Rv7QjZICVnjJemOQVj2L3b7889gHBrnC1LwltkzTcawDcbMe6pxo++0KVev9xSC0YCw+m5w==
|
integrity sha512-zyVwJWuZ5/DOULPpJZb8XhVsSFgWXvitPg1acAwIjZEblUfKAwvfHXA6+Gz6JruCWCokNyC4f3ZGgMcqT0Bv0Q==
|
||||||
|
|
||||||
upper-case@^1.1.1:
|
upper-case@^1.1.1:
|
||||||
version "1.1.3"
|
version "1.1.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user