diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 7f42046639f..b4aea6280d7 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -71,7 +71,7 @@ "react-transition-group": "4.4.1", "slate": "0.47.8", "tinycolor2": "1.4.1", - "uplot": "1.4.6" + "uplot": "1.4.7" }, "devDependencies": { "@rollup/plugin-commonjs": "16.0.0", diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index 9da26df3227..a150c4e6c21 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -23,8 +23,9 @@ export enum GraphMode { export enum LineInterpolation { Linear = 'linear', - Staircase = 'staircase', // https://leeoniya.github.io/uPlot/demos/line-stepped.html - Smooth = 'smooth', // https://leeoniya.github.io/uPlot/demos/line-smoothing.html + Smooth = 'smooth', + StepBefore = 'stepBefore', + StepAfter = 'stepAfter', } export interface LineConfig { @@ -65,8 +66,9 @@ export const graphFieldOptions = { lineInterpolation: [ { label: 'Linear', value: LineInterpolation.Linear }, - { label: 'Staircase', value: LineInterpolation.Staircase }, { label: 'Smooth', value: LineInterpolation.Smooth }, + { label: 'Step Before', value: LineInterpolation.StepBefore }, + { label: 'Step After', value: LineInterpolation.StepAfter }, ] as Array>, points: [ diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts index 24e48258454..57425066ede 100755 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotSeriesBuilder.ts @@ -1,7 +1,7 @@ import tinycolor from 'tinycolor2'; import uPlot, { Series } from 'uplot'; 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'; export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig { @@ -31,20 +31,29 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder { } else { lineConfig.stroke = lineColor; 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; if (mode === GraphMode.Bars) { pathsBuilder = barsBuilder; } else if (mode === GraphMode.Line) { - if (lineInterpolation === LineInterpolation.Staircase) { - pathsBuilder = staircaseBuilder; + if (lineInterpolation === LineInterpolation.StepBefore) { + pathsBuilder = stepBeforeBuilder; + } else if (lineInterpolation === LineInterpolation.StepAfter) { + pathsBuilder = stepAfterBuilder; } else if (lineInterpolation === LineInterpolation.Smooth) { pathsBuilder = smoothBuilder; } } - return pathsBuilder(self, seriesIdx, idx0, idx1); + return pathsBuilder(self, seriesIdx, idx0, idx1, extendGap, buildClip); }; } diff --git a/packages/grafana-ui/src/components/uPlot/paths.ts b/packages/grafana-ui/src/components/uPlot/paths.ts index f0818b81306..3173dcbf86e 100644 --- a/packages/grafana-ui/src/components/uPlot/paths.ts +++ b/packages/grafana-ui/src/components/uPlot/paths.ts @@ -1,6 +1,13 @@ 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 xdata = u.data[0]; 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 xdata = u.data[0]; - const ydata = u.data[seriesIdx]; - const scaleX = u.series[0].scale as string; - const scaleY = series.scale as string; +/* +const enum StepSide { + Before, + After, +} +*/ - const stroke = new Path2D(); - stroke.moveTo(Math.round(u.valToPos(xdata[0], scaleX, true)), Math.round(u.valToPos(ydata[0]!, scaleY, true))); +export const stepBeforeBuilder = stepBuilderFactory(false); +export const stepAfterBuilder = stepBuilderFactory(true); - for (let i = idx0; i <= idx1 - 1; i++) { - let x0 = Math.round(u.valToPos(xdata[i], scaleX, true)); - let y0 = Math.round(u.valToPos(ydata[i]!, scaleY, true)); - let x1 = Math.round(u.valToPos(xdata[i + 1], scaleX, true)); - let y1 = Math.round(u.valToPos(ydata[i + 1]!, scaleY, true)); +// babel does not support inlined const enums, so this uses a boolean flag for perf +// possible workaround: https://github.com/dosentmatter/babel-plugin-const-enum +function stepBuilderFactory(after: boolean): Series.PathBuilder { + return ( + 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); - stroke.lineTo(x1, y0); + const stroke = new Path2D(); - if (i === idx1 - 1) { - stroke.lineTo(x1, y1); + // find first non-null dataPt + while (ydata[idx0] == null) { + idx0++; } - } - const fill = new Path2D(stroke); + // find last-null dataPt + while (ydata[idx1] == null) { + idx1--; + } - //@ts-ignore - let fillTo = series.fillTo(u, seriesIdx, series.min, series.max); + let gaps: Series.Gaps = []; + 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)); - let minX = Math.round(u.valToPos(u.scales[scaleX].min!, scaleX, true)); - let maxX = Math.round(u.valToPos(u.scales[scaleX].max!, scaleX, true)); + stroke.moveTo(firstXPos, prevYPos); - fill.lineTo(maxX, minY); - fill.lineTo(minX, minY); + for (let i = idx0 + 1; i <= idx1; i++) { + let yVal1 = ydata[i]; - return { - stroke, - fill, + let x1 = Math.round(u.valToPos(xdata[i], scaleX, true)); + + 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) /** @@ -217,26 +292,63 @@ function catmullRomFitting(xCoords: number[], yCoords: number[], alpha: number) 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 xdata = u.data[0]; const ydata = u.data[seriesIdx]; const scaleX = u.series[0].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 yCoords = []; for (let i = idx0; i <= idx1; i++) { - if (ydata[i] != null) { - xCoords.push(u.valToPos(xdata[i], scaleX, true)); + let yVal = ydata[i]; + 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)); } } - const stroke = catmullRomFitting(xCoords, yCoords, alpha); + const stroke = catmullRomFitting(xCoords, yCoords, 0.5); 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 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(minX, minY); + fill.lineTo(prevXPos, minY); + fill.lineTo(firstXPos, minY); + + let clip = !series.spanGaps ? buildClip(gaps) : null; return { stroke, fill, + clip, }; }; diff --git a/yarn.lock b/yarn.lock index 39fd6921070..92d510dcb28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25667,10 +25667,10 @@ update-notifier@^2.5.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" -uplot@1.4.6: - version "1.4.6" - resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.6.tgz#52b6812c06b8d3cb89d4a8d053f45415cb5d9a13" - integrity sha512-nw3LdjLFhAqKQF/Rv7QjZICVnjJemOQVj2L3b7889gHBrnC1LwltkzTcawDcbMe6pxo++0KVev9xSC0YCw+m5w== +uplot@1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.7.tgz#feab0cf48b569184bfefacf29308deb021bad765" + integrity sha512-zyVwJWuZ5/DOULPpJZb8XhVsSFgWXvitPg1acAwIjZEblUfKAwvfHXA6+Gz6JruCWCokNyC4f3ZGgMcqT0Bv0Q== upper-case@^1.1.1: version "1.1.3"