Commit 77d6100b by Leon Sorokin Committed by GitHub

GraphNG: update uPlot v1.5.0 (#29763)

parent 7dd387ce
...@@ -71,7 +71,7 @@ ...@@ -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.7" "uplot": "1.5.0"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "16.0.0", "@rollup/plugin-commonjs": "16.0.0",
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
FieldMatcherID, FieldMatcherID,
} from '@grafana/data'; } from '@grafana/data';
import { AlignedFrameWithGapTest } from '../uPlot/types'; import { AlignedFrameWithGapTest } from '../uPlot/types';
import uPlot, { AlignedData, AlignedDataWithGapTest } from 'uplot'; import uPlot, { AlignedData } from 'uplot';
import { XYFieldMatchers } from './GraphNG'; import { XYFieldMatchers } from './GraphNG';
// the results ofter passing though data // the results ofter passing though data
...@@ -103,7 +103,7 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -103,7 +103,7 @@ 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, skipGaps); let { data: alignedData, isGap } = uPlot.join(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?');
...@@ -121,76 +121,3 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): ...@@ -121,76 +121,3 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
isGap, isGap,
}; };
} }
// 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: 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 (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();
for (let i = 0; i < len; i++) {
xVals.add(xs[i]);
}
for (let j = 1; j < t.length; 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]);
}
}
}
}
xNulls.push(nulls);
}
let data: AlignedData = [Array.from(xVals).sort((a, b) => a - b)];
let alignedLen = data[0].length;
let xIdxs = new Map();
for (let i = 0; i < alignedLen; i++) {
xIdxs.set(data[0][i], i);
}
for (const t of tables) {
let xs = t[0];
for (let j = 1; j < t.length; j++) {
let ys = t[j];
let yVals = Array(alignedLen).fill(null);
for (let i = 0; i < ys.length; i++) {
yVals[xIdxs.get(xs[i])] = ys[i];
}
data.push(yVals);
}
}
return {
data: data,
isGap(u: uPlot, seriesIdx: number, dataIdx: number) {
// u.data has to be AlignedDate
let xVal = u.data[0][dataIdx];
return xNulls[seriesIdx].has(xVal!);
},
};
}
...@@ -20,12 +20,19 @@ describe('UPlotConfigBuilder', () => { ...@@ -20,12 +20,19 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig()).toMatchInlineSnapshot(` expect(builder.getConfig()).toMatchInlineSnapshot(`
Object { Object {
"axes": Array [], "axes": Array [],
"cursor": Object {
"drag": Object {
"setScale": false,
},
},
"scales": Object { "scales": Object {
"scale-x": Object { "scale-x": Object {
"auto": false,
"range": [Function], "range": [Function],
"time": true, "time": true,
}, },
"scale-y": Object { "scale-y": Object {
"auto": true,
"range": [Function], "range": [Function],
"time": false, "time": false,
}, },
...@@ -95,6 +102,11 @@ describe('UPlotConfigBuilder', () => { ...@@ -95,6 +102,11 @@ describe('UPlotConfigBuilder', () => {
"values": Array [], "values": Array [],
}, },
], ],
"cursor": Object {
"drag": Object {
"setScale": false,
},
},
"scales": Object {}, "scales": Object {},
"series": Array [ "series": Array [
Object {}, Object {},
...@@ -138,6 +150,11 @@ describe('UPlotConfigBuilder', () => { ...@@ -138,6 +150,11 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getConfig()).toMatchInlineSnapshot(` expect(builder.getConfig()).toMatchInlineSnapshot(`
Object { Object {
"axes": Array [], "axes": Array [],
"cursor": Object {
"drag": Object {
"setScale": false,
},
},
"scales": Object {}, "scales": Object {},
"series": Array [ "series": Array [
Object {}, Object {},
......
...@@ -4,6 +4,7 @@ import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder'; ...@@ -4,6 +4,7 @@ import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder'; import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { AxisPlacement } from '../config'; import { AxisPlacement } from '../config';
import { Cursor } from 'uplot'; import { Cursor } from 'uplot';
import { defaultsDeep } from 'lodash';
export class UPlotConfigBuilder { export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = []; private series: UPlotSeriesBuilder[] = [];
...@@ -68,9 +69,12 @@ export class UPlotConfigBuilder { ...@@ -68,9 +69,12 @@ export class UPlotConfigBuilder {
config.scales = this.scales.reduce((acc, s) => { config.scales = this.scales.reduce((acc, s) => {
return { ...acc, ...s.getConfig() }; return { ...acc, ...s.getConfig() };
}, {}); }, {});
if (this.cursor) {
config.cursor = this.cursor; config.cursor = this.cursor || {};
}
// prevent client-side zoom from triggering at the end of a selection
defaultsDeep(config.cursor, { drag: { setScale: false } });
return config; return config;
} }
} }
...@@ -27,6 +27,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> { ...@@ -27,6 +27,7 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
return { return {
[scaleKey]: { [scaleKey]: {
time: isTime, time: isTime,
auto: !isTime,
range: range ?? this.range, range: range ?? this.range,
}, },
}; };
......
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import uPlot, { Series } from 'uplot'; import uPlot, { Series } from 'uplot';
import { DrawStyle, LineConfig, AreaConfig, PointsConfig, PointVisibility, LineInterpolation } from '../config'; import { DrawStyle, LineConfig, AreaConfig, PointsConfig, PointVisibility, LineInterpolation } from '../config';
import { barsBuilder, smoothBuilder, stepBeforeBuilder, stepAfterBuilder } from '../paths';
import { PlotConfigBuilder } from '../types'; import { PlotConfigBuilder } from '../types';
const pathBuilders = uPlot.paths;
const barWidthFactor = 0.6;
const barMaxWidth = Infinity;
const barsBuilder = pathBuilders.bars!({ size: [barWidthFactor, barMaxWidth] });
const linearBuilder = pathBuilders.linear!();
const smoothBuilder = pathBuilders.spline!();
const stepBeforeBuilder = pathBuilders.stepped!({ align: -1 });
const stepAfterBuilder = pathBuilders.stepped!({ align: 1 });
export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig { export interface SeriesProps extends LineConfig, AreaConfig, PointsConfig {
drawStyle: DrawStyle; drawStyle: DrawStyle;
scaleKey: string; scaleKey: string;
...@@ -32,15 +42,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -32,15 +42,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} else { } else {
lineConfig.stroke = lineColor; lineConfig.stroke = lineColor;
lineConfig.width = lineWidth; lineConfig.width = lineWidth;
lineConfig.paths = ( lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
self: uPlot, let pathsBuilder = linearBuilder;
seriesIdx: number,
idx0: number,
idx1: number,
extendGap: Series.ExtendGap,
buildClip: Series.BuildClip
) => {
let pathsBuilder = self.paths;
if (drawStyle === DrawStyle.Bars) { if (drawStyle === DrawStyle.Bars) {
pathsBuilder = barsBuilder; pathsBuilder = barsBuilder;
...@@ -54,7 +57,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -54,7 +57,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} }
} }
return pathsBuilder(self, seriesIdx, idx0, idx1, extendGap, buildClip); return pathsBuilder(self, seriesIdx, idx0, idx1);
}; };
} }
......
...@@ -22,10 +22,6 @@ describe('usePlotConfig', () => { ...@@ -22,10 +22,6 @@ describe('usePlotConfig', () => {
// "focus": Object { // "focus": Object {
// "alpha": 1, // "alpha": 1,
// }, // },
// "gutters": Object {
// "x": 8,
// "y": 8,
// },
// "height": 0, // "height": 0,
// "hooks": Object {}, // "hooks": Object {},
// "legend": Object { // "legend": Object {
...@@ -66,10 +62,6 @@ describe('usePlotConfig', () => { ...@@ -66,10 +62,6 @@ describe('usePlotConfig', () => {
// "focus": Object { // "focus": Object {
// "alpha": 1, // "alpha": 1,
// }, // },
// "gutters": Object {
// "x": 8,
// "y": 8,
// },
// "height": 0, // "height": 0,
// "hooks": Object {}, // "hooks": Object {},
// "legend": Object { // "legend": Object {
......
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { PlotPlugin } from './types'; import { PlotPlugin } from './types';
import { pluginLog } from './utils'; import { pluginLog } from './utils';
import uPlot, { Options } from 'uplot'; import uPlot, { Options, PaddingSide } from 'uplot';
import { getTimeZoneInfo, TimeZone } from '@grafana/data'; import { getTimeZoneInfo, TimeZone } from '@grafana/data';
import { usePlotPluginContext } from './context'; import { usePlotPluginContext } from './context';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
...@@ -85,7 +85,11 @@ export const usePlotPlugins = () => { ...@@ -85,7 +85,11 @@ export const usePlotPlugins = () => {
}; };
}; };
export const DEFAULT_PLOT_CONFIG = { const paddingSide: PaddingSide = (u, side, sidesWithAxes, cycleNum) => {
return sidesWithAxes[side] ? 0 : 8;
};
export const DEFAULT_PLOT_CONFIG: Partial<Options> = {
focus: { focus: {
alpha: 1, alpha: 1,
}, },
...@@ -97,10 +101,7 @@ export const DEFAULT_PLOT_CONFIG = { ...@@ -97,10 +101,7 @@ export const DEFAULT_PLOT_CONFIG = {
legend: { legend: {
show: false, show: false,
}, },
gutters: { padding: [paddingSide, paddingSide, paddingSide, paddingSide],
x: 8,
y: 8,
},
series: [], series: [],
hooks: {}, hooks: {},
}; };
......
import uPlot, { Series } from 'uplot';
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];
const scaleX = u.series[0].scale as string;
const scaleY = series.scale as string;
const gapFactor = 0.25;
let gap = (u.width * gapFactor) / (idx1 - idx0);
let maxWidth = Infinity;
//@ts-ignore
let fillTo = series.fillTo(u, seriesIdx, series.min, series.max);
let y0Pos = u.valToPos(fillTo, scaleY, true);
let colWid = u.bbox.width / (idx1 - idx0);
let strokeWidth = Math.round(series.width! * devicePixelRatio);
let barWid = Math.round(Math.min(maxWidth, colWid - gap) - strokeWidth);
let stroke = new Path2D();
for (let i = idx0; i <= idx1; i++) {
let yVal = ydata[i];
if (yVal == null) {
continue;
}
let xVal = u.scales.x.distr === 2 ? i : xdata[i];
// TODO: all xPos can be pre-computed once for all series in aligned set
let xPos = u.valToPos(xVal, scaleX, true);
let yPos = u.valToPos(yVal, scaleY, true);
let lft = Math.round(xPos - barWid / 2);
let btm = Math.round(Math.max(yPos, y0Pos));
let top = Math.round(Math.min(yPos, y0Pos));
let barHgt = btm - top;
stroke.rect(lft, top, barWid, barHgt);
}
let fill = series.fill != null ? new Path2D(stroke) : undefined;
return {
stroke,
fill,
};
};
/*
const enum StepSide {
Before,
After,
}
*/
export const stepBeforeBuilder = stepBuilderFactory(false);
export const stepAfterBuilder = stepBuilderFactory(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;
const stroke = new Path2D();
// 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 prevYPos = Math.round(u.valToPos(ydata[idx0]!, scaleY, true));
let firstXPos = Math.round(u.valToPos(xdata[idx0], scaleX, true));
let prevXPos = firstXPos;
stroke.moveTo(firstXPos, prevYPos);
for (let i = idx0 + 1; i <= idx1; i++) {
let yVal1 = ydata[i];
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)
/**
* Interpolates a Catmull-Rom Spline through a series of x/y points
* Converts the CR Spline to Cubic Beziers for use with SVG items
*
* If 'alpha' is 0.5 then the 'Centripetal' variant is used
* If 'alpha' is 1 then the 'Chordal' variant is used
*
*
* @param {Array} data - Array of points, each point in object literal holding x/y values
* @return {String} d - SVG string with cubic bezier curves representing the Catmull-Rom Spline
*/
function catmullRomFitting(xCoords: number[], yCoords: number[], alpha: number) {
const path = new Path2D();
const dataLen = xCoords.length;
let p0x,
p0y,
p1x,
p1y,
p2x,
p2y,
p3x,
p3y,
bp1x,
bp1y,
bp2x,
bp2y,
d1,
d2,
d3,
A,
B,
N,
M,
d3powA,
d2powA,
d3pow2A,
d2pow2A,
d1pow2A,
d1powA;
path.moveTo(Math.round(xCoords[0]), Math.round(yCoords[0]));
for (let i = 0; i < dataLen - 1; i++) {
let p0i = i === 0 ? 0 : i - 1;
p0x = xCoords[p0i];
p0y = yCoords[p0i];
p1x = xCoords[i];
p1y = yCoords[i];
p2x = xCoords[i + 1];
p2y = yCoords[i + 1];
if (i + 2 < dataLen) {
p3x = xCoords[i + 2];
p3y = yCoords[i + 2];
} else {
p3x = p2x;
p3y = p2y;
}
d1 = Math.sqrt(Math.pow(p0x - p1x, 2) + Math.pow(p0y - p1y, 2));
d2 = Math.sqrt(Math.pow(p1x - p2x, 2) + Math.pow(p1y - p2y, 2));
d3 = Math.sqrt(Math.pow(p2x - p3x, 2) + Math.pow(p2y - p3y, 2));
// Catmull-Rom to Cubic Bezier conversion matrix
// A = 2d1^2a + 3d1^a * d2^a + d3^2a
// B = 2d3^2a + 3d3^a * d2^a + d2^2a
// [ 0 1 0 0 ]
// [ -d2^2a /N A/N d1^2a /N 0 ]
// [ 0 d3^2a /M B/M -d2^2a /M ]
// [ 0 0 1 0 ]
d3powA = Math.pow(d3, alpha);
d3pow2A = Math.pow(d3, 2 * alpha);
d2powA = Math.pow(d2, alpha);
d2pow2A = Math.pow(d2, 2 * alpha);
d1powA = Math.pow(d1, alpha);
d1pow2A = Math.pow(d1, 2 * alpha);
A = 2 * d1pow2A + 3 * d1powA * d2powA + d2pow2A;
B = 2 * d3pow2A + 3 * d3powA * d2powA + d2pow2A;
N = 3 * d1powA * (d1powA + d2powA);
if (N > 0) {
N = 1 / N;
}
M = 3 * d3powA * (d3powA + d2powA);
if (M > 0) {
M = 1 / M;
}
bp1x = (-d2pow2A * p0x + A * p1x + d1pow2A * p2x) * N;
bp1y = (-d2pow2A * p0y + A * p1y + d1pow2A * p2y) * N;
bp2x = (d3pow2A * p1x + B * p2x - d2pow2A * p3x) * M;
bp2y = (d3pow2A * p1y + B * p2y - d2pow2A * p3y) * M;
if (bp1x === 0 && bp1y === 0) {
bp1x = p1x;
bp1y = p1y;
}
if (bp2x === 0 && bp2y === 0) {
bp2x = p2x;
bp2y = p2y;
}
path.bezierCurveTo(bp1x, bp1y, bp2x, bp2y, p2x, p2y);
}
return path;
}
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;
// 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++) {
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, 0.5);
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,
};
};
...@@ -67,6 +67,10 @@ export const SelectionPlugin: React.FC<SelectionPluginProps> = ({ onSelect, onDi ...@@ -67,6 +67,10 @@ export const SelectionPlugin: React.FC<SelectionPluginProps> = ({ onSelect, onDi
width: u.select.width, width: u.select.width,
}, },
}); });
// manually hide selected region (since cursor.drag.setScale = false)
/* @ts-ignore */
u.setSelect({ left: 0, width: 0 }, false);
}, },
}, },
}); });
......
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import { rangeUtil, RawTimeRange } from '@grafana/data';
import { Options } from 'uplot'; import { Options } from 'uplot';
import { PlotPlugin, PlotProps } from './types'; import { PlotPlugin, PlotProps } from './types';
...@@ -9,11 +8,6 @@ export const timeFormatToTemplate = (f: string) => { ...@@ -9,11 +8,6 @@ export const timeFormatToTemplate = (f: string) => {
return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, match => `{${match}}`); return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, match => `{${match}}`);
}; };
export function rangeToMinMax(timeRange: RawTimeRange): [number, number] {
const v = rangeUtil.convertRawToRange(timeRange);
return [v.from.valueOf() / 1000, v.to.valueOf() / 1000];
}
export const buildPlotConfig = (props: PlotProps, plugins: Record<string, PlotPlugin>): Options => { export const buildPlotConfig = (props: PlotProps, plugins: Record<string, PlotPlugin>): Options => {
return { return {
width: props.width, width: props.width,
......
...@@ -25703,10 +25703,10 @@ update-notifier@^2.5.0: ...@@ -25703,10 +25703,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.7: uplot@1.5.0:
version "1.4.7" version "1.5.0"
resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.4.7.tgz#feab0cf48b569184bfefacf29308deb021bad765" resolved "https://registry.yarnpkg.com/uplot/-/uplot-1.5.0.tgz#c2814cd7a073ed12b0a1d2fc985f3d9cd33bf0cd"
integrity sha512-zyVwJWuZ5/DOULPpJZb8XhVsSFgWXvitPg1acAwIjZEblUfKAwvfHXA6+Gz6JruCWCokNyC4f3ZGgMcqT0Bv0Q== integrity sha512-ghzlTe4rgoYiM98pMUJCplQhSmhyJV8Pv+obV3sHDoC+VBrZDc8fxXnUNGUVcKhUW2eqyeO5D0huardirwOmHw==
upper-case@^1.1.1: upper-case@^1.1.1:
version "1.1.3" version "1.1.3"
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