Scatter: support bubble and line charts with out-of-order data (alpha) (#39377) (#39809)

Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
(cherry picked from commit 4c8c2f6c96)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Grot (@grafanabot)
2021-09-29 19:42:50 +02:00
committed by GitHub
co-authored by Ryan McKinley
parent c6fb06721f
commit e69544b6ea
33 changed files with 1394 additions and 144 deletions
@@ -115,7 +115,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
state = {
alignedFrame,
alignedData: config!.prepData!(alignedFrame),
alignedData: config!.prepData!([alignedFrame]) as AlignedData,
config,
};
@@ -195,7 +195,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
if (shouldReconfig) {
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
newState.alignedData = newState.config.prepData!(newState.alignedFrame);
newState.alignedData = newState.config.prepData!([newState.alignedFrame]) as AlignedData;
pluginLog('GraphNG', false, 'config recreated', newState.config);
}
}
@@ -98,6 +98,7 @@ Object {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {
"__fixed": Object {
@@ -127,6 +128,7 @@ Object {
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -149,6 +151,7 @@ Object {
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -171,6 +174,7 @@ Object {
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -193,6 +197,7 @@ Object {
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -215,6 +220,7 @@ Object {
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -50,7 +50,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
const alignedDataFrame = preparePlotFrame(props.sparkline, props.config);
this.state = {
data: preparePlotData(alignedDataFrame),
data: preparePlotData([alignedDataFrame]),
alignedDataFrame,
configBuilder: this.prepareConfig(alignedDataFrame),
};
@@ -64,7 +64,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
return {
...state,
data: preparePlotData(frame),
data: preparePlotData([frame]),
alignedDataFrame: frame,
};
}
+3 -1
View File
@@ -246,12 +246,14 @@ export { LegacyForms, LegacyInputStatus };
// WIP, need renames and exports cleanup
export * from './uPlot/config';
export { UPlotConfigBuilder, UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder';
export { ScaleDistribution } from '@grafana/schema';
export { UPlotConfigBuilder } from './uPlot/config/UPlotConfigBuilder';
export { UPlotChart } from './uPlot/Plot';
export { PlotLegend } from './uPlot/PlotLegend';
export * from './uPlot/geometries';
export * from './uPlot/plugins';
export { PlotTooltipInterpolator, PlotSelection } from './uPlot/types';
export { UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder';
export { GraphNG, GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG';
export { TimeSeries } from './TimeSeries/TimeSeries';
export { useGraphNGContext } from './GraphNG/hooks';
@@ -55,7 +55,7 @@ const mockData = () => {
const config = new UPlotConfigBuilder();
config.addSeries({} as SeriesProps);
return { data, timeRange, config };
return { data: [data], timeRange, config };
};
describe('UPlotChart', () => {
@@ -104,7 +104,7 @@ describe('UPlotChart', () => {
expect(uPlot).toBeCalledTimes(1);
data.fields[1].values.set(0, 1);
data[0].fields[1].values.set(0, 1);
rerender(
<UPlotChart
@@ -1,5 +1,5 @@
import React, { createRef } from 'react';
import uPlot, { Options } from 'uplot';
import uPlot, { AlignedData, Options } from 'uplot';
import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils';
import { PlotProps } from './types';
@@ -72,7 +72,7 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
};
pluginLog('UPlot', false, 'Reinitializing plot', config);
const plot = new uPlot(config, this.props.data, this.plotContainer!.current!);
const plot = new uPlot(config, this.props.data as AlignedData, this.plotContainer!.current!);
if (plotRef) {
plotRef(plot);
@@ -100,12 +100,12 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
} else if (!sameConfig(prevProps, this.props)) {
this.reinitPlot();
} else if (!sameData(prevProps, this.props)) {
plot?.setData(this.props.data);
plot?.setData(this.props.data as AlignedData);
// this is a uPlot cache-busting hack for bar charts in case x axis labels changed
// since the x scale's "range" doesnt change, the axis size doesnt get recomputed, which is where the tick labels are regenerated & cached
// the more expensive, more proper/thorough way to do this is to force all axes to recalc: plot?.redraw(false, true);
if (plot && typeof this.props.data[0][0] === 'string') {
if (plot && typeof this.props.data[0]?.[0] === 'string') {
//@ts-ignore
plot.axes[0]._values = this.props.data[0];
}
@@ -18,7 +18,7 @@ export interface AxisProps {
formatValue?: (v: any) => string;
incrs?: Axis.Incrs;
splits?: Axis.Splits;
values?: any;
values?: Axis.Values;
isTime?: boolean;
timeZone?: TimeZone;
}
@@ -37,6 +37,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {},
"select": undefined,
@@ -87,6 +88,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {
"scale-x": Object {
@@ -166,6 +168,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {
"scale-y": Object {
@@ -218,6 +221,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {
"scale-y": Object {
@@ -271,6 +275,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {
"scale-y": Object {
@@ -387,6 +392,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {},
"select": undefined,
@@ -505,6 +511,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {},
"select": undefined,
@@ -513,6 +520,7 @@ describe('UPlotConfigBuilder', () => {
"value": [Function],
},
Object {
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -619,6 +627,7 @@ describe('UPlotConfigBuilder', () => {
},
},
"hooks": Object {},
"mode": 1,
"padding": undefined,
"scales": Object {},
"select": undefined,
@@ -627,6 +636,7 @@ describe('UPlotConfigBuilder', () => {
"value": [Function],
},
Object {
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -644,6 +654,7 @@ describe('UPlotConfigBuilder', () => {
"width": 1,
},
Object {
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -661,6 +672,7 @@ describe('UPlotConfigBuilder', () => {
"width": 1,
},
Object {
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": Object {
@@ -1,4 +1,4 @@
import uPlot, { Cursor, Band, Hooks, Select, AlignedData, Padding } from 'uplot';
import uPlot, { Cursor, Band, Hooks, Select, AlignedData, Padding, Series } from 'uplot';
import { merge } from 'lodash';
import {
DataFrame,
@@ -9,7 +9,7 @@ import {
TimeRange,
TimeZone,
} from '@grafana/data';
import { PlotConfig, PlotTooltipInterpolator } from '../types';
import { FacetedData, PlotConfig, PlotTooltipInterpolator } from '../types';
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
@@ -31,7 +31,7 @@ const cursorDefaults: Cursor = {
},
};
type PrepData = (frame: DataFrame) => AlignedData;
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = [];
@@ -45,7 +45,8 @@ export class UPlotConfigBuilder {
private hooks: Hooks.Arrays = {};
private tz: string | undefined = undefined;
private sync = false;
private frame: DataFrame | undefined = undefined;
private mode: uPlot.Mode = 1;
private frames: DataFrame[] | undefined = undefined;
// to prevent more than one threshold per scale
private thresholds: Record<string, UPlotThresholdOptions> = {};
// Custom handler for closest datapoint and series lookup
@@ -112,6 +113,10 @@ export class UPlotConfigBuilder {
this.cursor = merge({}, this.cursor, cursor);
}
setMode(mode: uPlot.Mode) {
this.mode = mode;
}
setSelect(select: Select) {
this.select = select;
}
@@ -151,9 +156,9 @@ export class UPlotConfigBuilder {
}
setPrepData(prepData: PrepData) {
this.prepData = (frame) => {
this.frame = frame;
return prepData(frame);
this.prepData = (frames) => {
this.frames = frames;
return prepData(frames);
};
}
@@ -171,10 +176,13 @@ export class UPlotConfigBuilder {
getConfig() {
const config: PlotConfig = {
mode: this.mode,
series: [
{
value: () => '',
},
this.mode === 2
? ((null as unknown) as Series)
: {
value: () => '',
},
],
};
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
@@ -193,19 +201,24 @@ export class UPlotConfigBuilder {
// interpolate for gradients/thresholds
if (typeof s !== 'string') {
let field = this.frame!.fields[seriesIdx];
let field = this.frames![0].fields[seriesIdx];
s = field.display!(field.values.get(u.cursor.idxs![seriesIdx]!)).color!;
}
return s + alphaHex;
};
config.cursor = merge({}, cursorDefaults, this.cursor, {
points: {
stroke: pointColorFn('80'),
fill: pointColorFn(),
config.cursor = merge(
{},
cursorDefaults,
{
points: {
stroke: pointColorFn('80'),
fill: pointColorFn(),
},
},
});
this.cursor
);
config.tzDate = this.tzDate;
config.padding = this.padding;
@@ -27,6 +27,8 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo
pxAlign?: boolean;
gradientMode?: GraphGradientMode;
facets?: uPlot.Series.Facet[];
/** Used when gradientMode is set to Scheme */
thresholds?: ThresholdsConfig;
colorMode?: FieldColorMode;
@@ -48,6 +50,7 @@ export interface SeriesProps extends LineConfig, BarConfig, FillConfig, PointsCo
export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
getConfig() {
const {
facets,
drawStyle,
pathBuilder,
pointsBuilder,
@@ -132,6 +135,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
return {
scale: scaleKey,
facets,
spanGaps: typeof spanNulls === 'number' ? false : spanNulls,
value: () => '',
pxAlign,
@@ -5,15 +5,19 @@ import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
export type PlotConfig = Pick<
Options,
'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding'
'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding'
>;
export interface PlotPluginProps {
id: string;
}
export type FacetValues = any[];
export type FacetSeries = FacetValues[];
export type FacetedData = [_: null, ...series: FacetSeries];
export interface PlotProps {
data: AlignedData;
data: AlignedData | FacetedData;
width: number;
height: number;
config: UPlotConfigBuilder;
@@ -27,7 +27,7 @@ describe('preparePlotData', () => {
});
it('creates array from DataFrame', () => {
expect(preparePlotData(df)).toMatchInlineSnapshot(`
expect(preparePlotData([df])).toMatchInlineSnapshot(`
Array [
Array [
9997,
@@ -75,7 +75,7 @@ describe('preparePlotData', () => {
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
expect(preparePlotData([df])).toMatchInlineSnapshot(`
Array [
Array [
9997,
@@ -122,7 +122,7 @@ describe('preparePlotData', () => {
},
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
expect(preparePlotData([df])).toMatchInlineSnapshot(`
Array [
Array [
9997,
@@ -185,7 +185,7 @@ describe('preparePlotData', () => {
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
expect(preparePlotData([df])).toMatchInlineSnapshot(`
Array [
Array [
9997,
@@ -260,7 +260,7 @@ describe('preparePlotData', () => {
],
});
expect(preparePlotData(df)).toMatchInlineSnapshot(`
expect(preparePlotData([df])).toMatchInlineSnapshot(`
Array [
Array [
9997,
@@ -39,7 +39,8 @@ interface StackMeta {
}
/** @internal */
export function preparePlotData(frame: DataFrame, onStackMeta?: (meta: StackMeta) => void): AlignedData {
export function preparePlotData(frames: DataFrame[], onStackMeta?: (meta: StackMeta) => void): AlignedData {
const frame = frames[0];
const result: any[] = [];
const stackingGroups: Map<string, number[]> = new Map();
let seriesIndex = 0;