mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graph NG: fix toggling queries and extract Graph component from graph3 panel (#28290)
* Fix issue when data and config is not in sync * Extract GraphNG component from graph panel and add some tests coverage * Update packages/grafana-ui/src/components/uPlot/hooks.test.ts * Update packages/grafana-ui/src/components/uPlot/hooks.test.ts * Update packages/grafana-ui/src/components/uPlot/hooks.test.ts * Fix grid color and annotations refresh
This commit is contained in:
parent
448114f649
commit
f989e37132
209
packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx
Normal file
209
packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { GraphNG } from './GraphNG';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
ArrayVector,
|
||||||
|
DataTransformerID,
|
||||||
|
dateTime,
|
||||||
|
FieldConfig,
|
||||||
|
FieldType,
|
||||||
|
MutableDataFrame,
|
||||||
|
standardTransformers,
|
||||||
|
standardTransformersRegistry,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { Canvas, GraphCustomFieldConfig } from '..';
|
||||||
|
|
||||||
|
const mockData = () => {
|
||||||
|
const data = new MutableDataFrame();
|
||||||
|
|
||||||
|
data.addField({
|
||||||
|
type: FieldType.time,
|
||||||
|
name: 'Time',
|
||||||
|
values: new ArrayVector([1602630000000, 1602633600000, 1602637200000]),
|
||||||
|
config: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
data.addField({
|
||||||
|
type: FieldType.number,
|
||||||
|
name: 'Value',
|
||||||
|
values: new ArrayVector([10, 20, 5]),
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
line: { show: true },
|
||||||
|
},
|
||||||
|
} as FieldConfig<GraphCustomFieldConfig>,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timeRange = {
|
||||||
|
from: dateTime(1602673200000),
|
||||||
|
to: dateTime(1602680400000),
|
||||||
|
raw: { from: '1602673200000', to: '1602680400000' },
|
||||||
|
};
|
||||||
|
return { data, timeRange };
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('GraphNG', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
standardTransformersRegistry.setInit(() => [
|
||||||
|
{
|
||||||
|
id: DataTransformerID.seriesToColumns,
|
||||||
|
editor: () => null,
|
||||||
|
transformation: standardTransformers.seriesToColumnsTransformer,
|
||||||
|
name: 'outer join',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when rendered without Canvas as child', () => {
|
||||||
|
const { data, timeRange } = mockData();
|
||||||
|
expect(() => {
|
||||||
|
render(<GraphNG data={[data]} timeRange={timeRange} timeZone={'browser'} width={100} height={100} />);
|
||||||
|
}).toThrow('Missing Canvas component as a child of the plot.');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data update', () => {
|
||||||
|
it('does not re-initialise uPlot when there are no field config changes', () => {
|
||||||
|
const { data, timeRange } = mockData();
|
||||||
|
const onDataUpdateSpy = jest.fn();
|
||||||
|
const onPlotInitSpy = jest.fn();
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onDataUpdate={onDataUpdateSpy}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
data.fields[1].values.set(0, 1);
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onDataUpdate={onDataUpdateSpy}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onPlotInitSpy).toBeCalledTimes(1);
|
||||||
|
expect(onDataUpdateSpy).toHaveBeenLastCalledWith([
|
||||||
|
[1602630000, 1602633600, 1602637200],
|
||||||
|
[1, 20, 5],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('config update', () => {
|
||||||
|
it('should skip plot intialization for width and height equal 0', () => {
|
||||||
|
const { data, timeRange } = mockData();
|
||||||
|
const onPlotInitSpy = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={0}
|
||||||
|
height={0}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onPlotInitSpy).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reinitializes plot when number of series change', () => {
|
||||||
|
const { data, timeRange } = mockData();
|
||||||
|
const onPlotInitSpy = jest.fn();
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
data.addField({
|
||||||
|
name: 'Value1',
|
||||||
|
type: FieldType.number,
|
||||||
|
values: new ArrayVector([1, 2, 3]),
|
||||||
|
config: {
|
||||||
|
custom: {
|
||||||
|
line: { show: true },
|
||||||
|
},
|
||||||
|
} as FieldConfig<GraphCustomFieldConfig>,
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onPlotInitSpy).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reinitializes plot when series field config changes', () => {
|
||||||
|
const { data, timeRange } = mockData();
|
||||||
|
const onPlotInitSpy = jest.fn();
|
||||||
|
|
||||||
|
const { rerender } = render(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
expect(onPlotInitSpy).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
data.fields[1].config.custom.line.width = 5;
|
||||||
|
|
||||||
|
rerender(
|
||||||
|
<GraphNG
|
||||||
|
data={[data]}
|
||||||
|
timeRange={timeRange}
|
||||||
|
timeZone={'browser'}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
onPlotInit={onPlotInitSpy}
|
||||||
|
>
|
||||||
|
<Canvas />
|
||||||
|
</GraphNG>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onPlotInitSpy).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
189
packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
Normal file
189
packages/grafana-ui/src/components/GraphNG/GraphNG.tsx
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
DataFrame,
|
||||||
|
FieldConfig,
|
||||||
|
FieldType,
|
||||||
|
formattedValueToString,
|
||||||
|
getFieldColorModeForField,
|
||||||
|
getTimeField,
|
||||||
|
systemDateFormats,
|
||||||
|
} from '@grafana/data';
|
||||||
|
import { timeFormatToTemplate } from '../uPlot/utils';
|
||||||
|
import { alignAndSortDataFramesByFieldName } from './utils';
|
||||||
|
import { Area, Axis, Line, Point, Scale, SeriesGeometry } from '../uPlot/geometries';
|
||||||
|
import { UPlotChart } from '../uPlot/Plot';
|
||||||
|
import { GraphCustomFieldConfig, PlotProps } from '../uPlot/types';
|
||||||
|
import { useTheme } from '../../themes';
|
||||||
|
|
||||||
|
const timeStampsConfig = [
|
||||||
|
[3600 * 24 * 365, '{YYYY}', 7, '{YYYY}'],
|
||||||
|
[3600 * 24 * 28, `{${timeFormatToTemplate(systemDateFormats.interval.month)}`, 7, '{MMM}\n{YYYY}'],
|
||||||
|
[
|
||||||
|
3600 * 24,
|
||||||
|
`{${timeFormatToTemplate(systemDateFormats.interval.day)}`,
|
||||||
|
7,
|
||||||
|
`${timeFormatToTemplate(systemDateFormats.interval.day)}\n${timeFormatToTemplate(systemDateFormats.interval.year)}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3600,
|
||||||
|
`{${timeFormatToTemplate(systemDateFormats.interval.minute)}`,
|
||||||
|
4,
|
||||||
|
`${timeFormatToTemplate(systemDateFormats.interval.minute)}\n${timeFormatToTemplate(
|
||||||
|
systemDateFormats.interval.day
|
||||||
|
)}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
60,
|
||||||
|
`{${timeFormatToTemplate(systemDateFormats.interval.second)}`,
|
||||||
|
4,
|
||||||
|
`${timeFormatToTemplate(systemDateFormats.interval.second)}\n${timeFormatToTemplate(
|
||||||
|
systemDateFormats.interval.day
|
||||||
|
)}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
`:{ss}`,
|
||||||
|
2,
|
||||||
|
`:{ss}\n${timeFormatToTemplate(systemDateFormats.interval.day)} ${timeFormatToTemplate(
|
||||||
|
systemDateFormats.interval.minute
|
||||||
|
)}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1e-3,
|
||||||
|
':{ss}.{fff}',
|
||||||
|
2,
|
||||||
|
`:{ss}.{fff}\n${timeFormatToTemplate(systemDateFormats.interval.day)} ${timeFormatToTemplate(
|
||||||
|
systemDateFormats.interval.minute
|
||||||
|
)}`,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||||
|
|
||||||
|
const TIME_FIELD_NAME = 'Time';
|
||||||
|
|
||||||
|
interface GraphNGProps extends Omit<PlotProps, 'data'> {
|
||||||
|
data: DataFrame[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GraphNG: React.FC<GraphNGProps> = ({ data, children, ...plotProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [alignedData, setAlignedData] = useState<DataFrame | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data.length === 0) {
|
||||||
|
setAlignedData(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscription = alignAndSortDataFramesByFieldName(data, TIME_FIELD_NAME).subscribe(setAlignedData);
|
||||||
|
|
||||||
|
return function unsubscribe() {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (!alignedData) {
|
||||||
|
return (
|
||||||
|
<div className="panel-empty">
|
||||||
|
<p>No data found in response</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const geometries: React.ReactNode[] = [];
|
||||||
|
const scales: React.ReactNode[] = [];
|
||||||
|
const axes: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
let { timeIndex } = getTimeField(alignedData);
|
||||||
|
if (timeIndex === undefined) {
|
||||||
|
timeIndex = 0; // assuming first field represents x-domain
|
||||||
|
scales.push(<Scale key="scale-x" scaleKey="x" />);
|
||||||
|
} else {
|
||||||
|
scales.push(<Scale key="scale-x" scaleKey="x" time />);
|
||||||
|
}
|
||||||
|
|
||||||
|
axes.push(<Axis key="axis-scale--x" scaleKey="x" values={timeStampsConfig} side={2} />);
|
||||||
|
|
||||||
|
let seriesIdx = 0;
|
||||||
|
const uniqueScales: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < alignedData.fields.length; i++) {
|
||||||
|
const seriesGeometry = [];
|
||||||
|
const field = alignedData.fields[i];
|
||||||
|
const config = field.config as FieldConfig<GraphCustomFieldConfig>;
|
||||||
|
const customConfig = config.custom;
|
||||||
|
|
||||||
|
if (i === timeIndex || field.type !== FieldType.number) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fmt = field.display ?? defaultFormatter;
|
||||||
|
const scale = config.unit || '__fixed';
|
||||||
|
|
||||||
|
if (!uniqueScales[scale]) {
|
||||||
|
uniqueScales[scale] = true;
|
||||||
|
scales.push(<Scale key={`scale-${scale}`} scaleKey={scale} />);
|
||||||
|
axes.push(
|
||||||
|
<Axis
|
||||||
|
key={`axis-${scale}-${i}`}
|
||||||
|
scaleKey={scale}
|
||||||
|
label={config.custom?.axis?.label}
|
||||||
|
size={config.custom?.axis?.width}
|
||||||
|
side={config.custom?.axis?.side || 3}
|
||||||
|
grid={config.custom?.axis?.grid}
|
||||||
|
formatValue={v => formattedValueToString(fmt(v))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to update field state here because we use a transform to merge framesP
|
||||||
|
field.state = { ...field.state, seriesIndex: seriesIdx };
|
||||||
|
|
||||||
|
const colorMode = getFieldColorModeForField(field);
|
||||||
|
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
|
||||||
|
|
||||||
|
if (customConfig?.line?.show) {
|
||||||
|
seriesGeometry.push(
|
||||||
|
<Line
|
||||||
|
key={`line-${scale}-${i}`}
|
||||||
|
scaleKey={scale}
|
||||||
|
stroke={seriesColor}
|
||||||
|
width={customConfig?.line.show ? customConfig?.line.width || 1 : 0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customConfig?.points?.show) {
|
||||||
|
seriesGeometry.push(
|
||||||
|
<Point key={`point-${scale}-${i}`} scaleKey={scale} size={customConfig?.points?.radius} stroke={seriesColor} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customConfig?.fill?.alpha) {
|
||||||
|
seriesGeometry.push(
|
||||||
|
<Area key={`area-${scale}-${i}`} scaleKey={scale} fill={customConfig?.fill.alpha} color={seriesColor} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (seriesGeometry.length > 1) {
|
||||||
|
geometries.push(
|
||||||
|
<SeriesGeometry key={`seriesGeometry-${scale}-${i}`} scaleKey={scale}>
|
||||||
|
{seriesGeometry}
|
||||||
|
</SeriesGeometry>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
geometries.push(seriesGeometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesIdx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UPlotChart data={alignedData} {...plotProps}>
|
||||||
|
{scales}
|
||||||
|
{axes}
|
||||||
|
{geometries}
|
||||||
|
{children}
|
||||||
|
</UPlotChart>
|
||||||
|
);
|
||||||
|
};
|
@ -69,15 +69,6 @@ export {
|
|||||||
BigValueTextMode,
|
BigValueTextMode,
|
||||||
} from './BigValue/BigValue';
|
} from './BigValue/BigValue';
|
||||||
|
|
||||||
export { GraphCustomFieldConfig } from './uPlot/types';
|
|
||||||
export { UPlotChart } from './uPlot/Plot';
|
|
||||||
export * from './uPlot/geometries';
|
|
||||||
export { usePlotConfigContext } from './uPlot/context';
|
|
||||||
export { Canvas } from './uPlot/Canvas';
|
|
||||||
export * from './uPlot/plugins';
|
|
||||||
export { useRefreshAfterGraphRendered } from './uPlot/hooks';
|
|
||||||
export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context';
|
|
||||||
|
|
||||||
export { Gauge } from './Gauge/Gauge';
|
export { Gauge } from './Gauge/Gauge';
|
||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { GraphLegend } from './Graph/GraphLegend';
|
export { GraphLegend } from './Graph/GraphLegend';
|
||||||
@ -208,3 +199,14 @@ const LegacyForms = {
|
|||||||
Switch,
|
Switch,
|
||||||
};
|
};
|
||||||
export { LegacyForms, LegacyInputStatus };
|
export { LegacyForms, LegacyInputStatus };
|
||||||
|
|
||||||
|
// WIP, need renames and exports cleanup
|
||||||
|
export { GraphCustomFieldConfig } from './uPlot/types';
|
||||||
|
export { UPlotChart } from './uPlot/Plot';
|
||||||
|
export * from './uPlot/geometries';
|
||||||
|
export { usePlotConfigContext } from './uPlot/context';
|
||||||
|
export { Canvas } from './uPlot/Canvas';
|
||||||
|
export * from './uPlot/plugins';
|
||||||
|
export { useRefreshAfterGraphRendered } from './uPlot/hooks';
|
||||||
|
export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context';
|
||||||
|
export { GraphNG } from './GraphNG/GraphNG';
|
||||||
|
@ -3,7 +3,7 @@ import { css } from 'emotion';
|
|||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
import { usePrevious } from 'react-use';
|
import { usePrevious } from 'react-use';
|
||||||
import { buildPlotContext, PlotContext } from './context';
|
import { buildPlotContext, PlotContext } from './context';
|
||||||
import { pluginLog, preparePlotData, shouldReinitialisePlot } from './utils';
|
import { pluginLog, preparePlotData, shouldInitialisePlot } from './utils';
|
||||||
import { usePlotConfig } from './hooks';
|
import { usePlotConfig } from './hooks';
|
||||||
import { PlotProps } from './types';
|
import { PlotProps } from './types';
|
||||||
|
|
||||||
@ -13,6 +13,7 @@ import { PlotProps } from './types';
|
|||||||
export const UPlotChart: React.FC<PlotProps> = props => {
|
export const UPlotChart: React.FC<PlotProps> = props => {
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
const [plotInstance, setPlotInstance] = useState<uPlot>();
|
const [plotInstance, setPlotInstance] = useState<uPlot>();
|
||||||
|
const plotData = useRef<uPlot.AlignedData>();
|
||||||
|
|
||||||
// uPlot config API
|
// uPlot config API
|
||||||
const { currentConfig, addSeries, addAxis, addScale, registerPlugin } = usePlotConfig(
|
const { currentConfig, addSeries, addAxis, addScale, registerPlugin } = usePlotConfig(
|
||||||
@ -20,36 +21,29 @@ export const UPlotChart: React.FC<PlotProps> = props => {
|
|||||||
props.height,
|
props.height,
|
||||||
props.timeZone
|
props.timeZone
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevConfig = usePrevious(currentConfig);
|
const prevConfig = usePrevious(currentConfig);
|
||||||
|
|
||||||
const getPlotInstance = useCallback(() => {
|
const getPlotInstance = useCallback(() => {
|
||||||
if (!plotInstance) {
|
if (!plotInstance) {
|
||||||
throw new Error("Plot hasn't initialised yet");
|
throw new Error("Plot hasn't initialised yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
return plotInstance;
|
return plotInstance;
|
||||||
}, [plotInstance]);
|
}, [plotInstance]);
|
||||||
|
|
||||||
// Main function initialising uPlot. If final config is not settled it will do nothing
|
|
||||||
const initPlot = () => {
|
|
||||||
if (!currentConfig || !canvasRef.current) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const data = preparePlotData(props.data);
|
|
||||||
pluginLog('uPlot core', false, 'initialized with', data, currentConfig);
|
|
||||||
return new uPlot(currentConfig, data, canvasRef.current);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Callback executed when there was no change in plot config
|
// Callback executed when there was no change in plot config
|
||||||
const updateData = useCallback(() => {
|
const updateData = useCallback(() => {
|
||||||
if (!plotInstance) {
|
if (!plotInstance || !plotData.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = preparePlotData(props.data);
|
pluginLog('uPlot core', false, 'updating plot data(throttled log!)', plotData.current);
|
||||||
pluginLog('uPlot core', false, 'updating plot data(throttled log!)');
|
|
||||||
// If config hasn't changed just update uPlot's data
|
// If config hasn't changed just update uPlot's data
|
||||||
plotInstance.setData(data);
|
plotInstance.setData(plotData.current);
|
||||||
}, [plotInstance, props.data]);
|
|
||||||
|
if (props.onDataUpdate) {
|
||||||
|
props.onDataUpdate(plotData.current);
|
||||||
|
}
|
||||||
|
}, [plotInstance, props.onDataUpdate]);
|
||||||
|
|
||||||
// Destroys previous plot instance when plot re-initialised
|
// Destroys previous plot instance when plot re-initialised
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -59,22 +53,38 @@ export const UPlotChart: React.FC<PlotProps> = props => {
|
|||||||
};
|
};
|
||||||
}, [plotInstance]);
|
}, [plotInstance]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
plotData.current = preparePlotData(props.data);
|
||||||
|
}, [props.data]);
|
||||||
|
|
||||||
// Decides if plot should update data or re-initialise
|
// Decides if plot should update data or re-initialise
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!currentConfig) {
|
// Make sure everything is ready before proceeding
|
||||||
|
if (!currentConfig || !plotData.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldReinitialisePlot(prevConfig, currentConfig)) {
|
// Do nothing if there is data vs series config mismatch. This may happen when the data was updated and made this
|
||||||
const instance = initPlot();
|
// effect fire before the config update triggered the effect.
|
||||||
if (!instance) {
|
if (currentConfig.series.length !== plotData.current.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldInitialisePlot(prevConfig, currentConfig)) {
|
||||||
|
if (!canvasRef.current) {
|
||||||
|
throw new Error('Missing Canvas component as a child of the plot.');
|
||||||
|
}
|
||||||
|
const instance = initPlot(plotData.current, currentConfig, canvasRef.current);
|
||||||
|
|
||||||
|
if (props.onPlotInit) {
|
||||||
|
props.onPlotInit();
|
||||||
|
}
|
||||||
|
|
||||||
setPlotInstance(instance);
|
setPlotInstance(instance);
|
||||||
} else {
|
} else {
|
||||||
updateData();
|
updateData();
|
||||||
}
|
}
|
||||||
}, [props.data, props.timeRange, props.timeZone, currentConfig, setPlotInstance]);
|
}, [currentConfig, updateData, setPlotInstance, props.onPlotInit]);
|
||||||
|
|
||||||
// When size props changed update plot size synchronously
|
// When size props changed update plot size synchronously
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -114,3 +124,9 @@ export const UPlotChart: React.FC<PlotProps> = props => {
|
|||||||
</PlotContext.Provider>
|
</PlotContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Main function initialising uPlot. If final config is not settled it will do nothing
|
||||||
|
function initPlot(data: uPlot.AlignedData, config: uPlot.Options, ref: HTMLDivElement) {
|
||||||
|
pluginLog('uPlot core', false, 'initialized with', data, config);
|
||||||
|
return new uPlot(config, data, ref);
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { AxisProps } from './types';
|
import { AxisProps } from './types';
|
||||||
import { usePlotConfigContext } from '../context';
|
import { usePlotConfigContext } from '../context';
|
||||||
import { useTheme } from '../../../themes';
|
import { useTheme } from '../../../themes';
|
||||||
@ -32,6 +32,7 @@ export const useAxisConfig = (getConfig: () => any) => {
|
|||||||
|
|
||||||
export const Axis: React.FC<AxisProps> = props => {
|
export const Axis: React.FC<AxisProps> = props => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const gridColor = useMemo(() => (theme.isDark ? theme.palette.gray1 : theme.palette.gray4), [theme]);
|
||||||
const {
|
const {
|
||||||
scaleKey,
|
scaleKey,
|
||||||
label,
|
label,
|
||||||
@ -54,7 +55,12 @@ export const Axis: React.FC<AxisProps> = props => {
|
|||||||
side,
|
side,
|
||||||
grid: {
|
grid: {
|
||||||
show: grid,
|
show: grid,
|
||||||
stroke: theme.palette.gray4,
|
stroke: gridColor,
|
||||||
|
width: 1 / devicePixelRatio,
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
show: true,
|
||||||
|
stroke: gridColor,
|
||||||
width: 1 / devicePixelRatio,
|
width: 1 / devicePixelRatio,
|
||||||
},
|
},
|
||||||
values: values ? values : formatValue ? (u: uPlot, vals: any[]) => vals.map(v => formatValue(v)) : undefined,
|
values: values ? values : formatValue ? (u: uPlot, vals: any[]) => vals.map(v => formatValue(v)) : undefined,
|
||||||
|
@ -3,8 +3,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
|
|||||||
|
|
||||||
describe('usePlotConfig', () => {
|
describe('usePlotConfig', () => {
|
||||||
it('returns default plot config', async () => {
|
it('returns default plot config', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
@ -34,7 +33,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
describe('series config', () => {
|
describe('series config', () => {
|
||||||
it('should add series', async () => {
|
it('should add series', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addSeries = result.current.addSeries;
|
const addSeries = result.current.addSeries;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -42,7 +41,6 @@ describe('usePlotConfig', () => {
|
|||||||
stroke: '#ff0000',
|
stroke: '#ff0000',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.series).toHaveLength(2);
|
expect(result.current.currentConfig?.series).toHaveLength(2);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -76,7 +74,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update series', async () => {
|
it('should update series', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addSeries = result.current.addSeries;
|
const addSeries = result.current.addSeries;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -88,7 +86,6 @@ describe('usePlotConfig', () => {
|
|||||||
stroke: '#00ff00',
|
stroke: '#00ff00',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.series).toHaveLength(2);
|
expect(result.current.currentConfig?.series).toHaveLength(2);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -122,7 +119,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove series', async () => {
|
it('should remove series', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addSeries = result.current.addSeries;
|
const addSeries = result.current.addSeries;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -132,7 +129,6 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
removeSeries();
|
removeSeries();
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.series).toHaveLength(1);
|
expect(result.current.currentConfig?.series).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -165,7 +161,7 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
describe('axis config', () => {
|
describe('axis config', () => {
|
||||||
it('should add axis', async () => {
|
it('should add axis', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addAxis = result.current.addAxis;
|
const addAxis = result.current.addAxis;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -173,7 +169,6 @@ describe('usePlotConfig', () => {
|
|||||||
side: 1,
|
side: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.axes).toHaveLength(1);
|
expect(result.current.currentConfig?.axes).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -208,7 +203,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update axis', async () => {
|
it('should update axis', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addAxis = result.current.addAxis;
|
const addAxis = result.current.addAxis;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -220,7 +215,6 @@ describe('usePlotConfig', () => {
|
|||||||
side: 3,
|
side: 3,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.axes).toHaveLength(1);
|
expect(result.current.currentConfig?.axes).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -255,7 +249,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove axis', async () => {
|
it('should remove axis', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addAxis = result.current.addAxis;
|
const addAxis = result.current.addAxis;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -265,7 +259,6 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
removeAxis();
|
removeAxis();
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(result.current.currentConfig?.axes).toHaveLength(0);
|
expect(result.current.currentConfig?.axes).toHaveLength(0);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -298,7 +291,7 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
describe('scales config', () => {
|
describe('scales config', () => {
|
||||||
it('should add scale', async () => {
|
it('should add scale', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addScale = result.current.addScale;
|
const addScale = result.current.addScale;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -306,7 +299,6 @@ describe('usePlotConfig', () => {
|
|||||||
time: true,
|
time: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(1);
|
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -341,7 +333,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update scale', async () => {
|
it('should update scale', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addScale = result.current.addScale;
|
const addScale = result.current.addScale;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -353,7 +345,6 @@ describe('usePlotConfig', () => {
|
|||||||
time: false,
|
time: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(1);
|
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -388,7 +379,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove scale', async () => {
|
it('should remove scale', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const addScale = result.current.addScale;
|
const addScale = result.current.addScale;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -398,7 +389,6 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
removeScale();
|
removeScale();
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(0);
|
expect(Object.keys(result.current.currentConfig?.scales!)).toHaveLength(0);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -431,7 +421,7 @@ describe('usePlotConfig', () => {
|
|||||||
|
|
||||||
describe('plugins config', () => {
|
describe('plugins config', () => {
|
||||||
it('should register plugin', async () => {
|
it('should register plugin', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const registerPlugin = result.current.registerPlugin;
|
const registerPlugin = result.current.registerPlugin;
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@ -440,7 +430,6 @@ describe('usePlotConfig', () => {
|
|||||||
hooks: {},
|
hooks: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(Object.keys(result.current.currentConfig?.plugins!)).toHaveLength(1);
|
expect(Object.keys(result.current.currentConfig?.plugins!)).toHaveLength(1);
|
||||||
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
expect(result.current.currentConfig).toMatchInlineSnapshot(`
|
||||||
@ -475,7 +464,7 @@ describe('usePlotConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should unregister plugin', async () => {
|
it('should unregister plugin', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
const { result } = renderHook(() => usePlotConfig(0, 0, 'browser'));
|
||||||
const registerPlugin = result.current.registerPlugin;
|
const registerPlugin = result.current.registerPlugin;
|
||||||
|
|
||||||
let unregister: () => void;
|
let unregister: () => void;
|
||||||
@ -485,7 +474,6 @@ describe('usePlotConfig', () => {
|
|||||||
hooks: {},
|
hooks: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForNextUpdate();
|
|
||||||
|
|
||||||
expect(Object.keys(result.current.currentConfig?.plugins!)).toHaveLength(1);
|
expect(Object.keys(result.current.currentConfig?.plugins!)).toHaveLength(1);
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ export const usePlotPlugins = () => {
|
|||||||
|
|
||||||
// arePluginsReady determines whether or not all plugins has already registered and uPlot should be initialised
|
// arePluginsReady determines whether or not all plugins has already registered and uPlot should be initialised
|
||||||
const [arePluginsReady, setPluginsReady] = useState(false);
|
const [arePluginsReady, setPluginsReady] = useState(false);
|
||||||
|
|
||||||
const cancellationToken = useRef<number>();
|
const cancellationToken = useRef<number>();
|
||||||
|
const isMounted = useRef(false);
|
||||||
|
|
||||||
const checkPluginsReady = useCallback(() => {
|
const checkPluginsReady = useCallback(() => {
|
||||||
if (cancellationToken.current) {
|
if (cancellationToken.current) {
|
||||||
@ -29,7 +29,9 @@ export const usePlotPlugins = () => {
|
|||||||
* and arePluginsReady will be deferred to next animation frame.
|
* and arePluginsReady will be deferred to next animation frame.
|
||||||
*/
|
*/
|
||||||
cancellationToken.current = window.requestAnimationFrame(function() {
|
cancellationToken.current = window.requestAnimationFrame(function() {
|
||||||
|
if (isMounted.current) {
|
||||||
setPluginsReady(true);
|
setPluginsReady(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [cancellationToken, setPluginsReady]);
|
}, [cancellationToken, setPluginsReady]);
|
||||||
|
|
||||||
@ -66,9 +68,9 @@ export const usePlotPlugins = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkPluginsReady();
|
checkPluginsReady();
|
||||||
return () => {
|
return () => {
|
||||||
|
isMounted.current = false;
|
||||||
if (cancellationToken.current) {
|
if (cancellationToken.current) {
|
||||||
window.cancelAnimationFrame(cancellationToken.current);
|
window.cancelAnimationFrame(cancellationToken.current);
|
||||||
cancellationToken.current = undefined;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@ -92,6 +94,7 @@ export const DEFAULT_PLOT_CONFIG = {
|
|||||||
legend: {
|
legend: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
|
series: [],
|
||||||
hooks: {},
|
hooks: {},
|
||||||
};
|
};
|
||||||
export const usePlotConfig = (width: number, height: number, timeZone: TimeZone) => {
|
export const usePlotConfig = (width: number, height: number, timeZone: TimeZone) => {
|
||||||
@ -113,7 +116,7 @@ export const usePlotConfig = (width: number, height: number, timeZone: TimeZone)
|
|||||||
return fmt;
|
return fmt;
|
||||||
}, [timeZone]);
|
}, [timeZone]);
|
||||||
|
|
||||||
const defaultConfig = useMemo(() => {
|
const defaultConfig = useMemo<uPlot.Options>(() => {
|
||||||
return {
|
return {
|
||||||
...DEFAULT_PLOT_CONFIG,
|
...DEFAULT_PLOT_CONFIG,
|
||||||
width,
|
width,
|
||||||
@ -122,7 +125,7 @@ export const usePlotConfig = (width: number, height: number, timeZone: TimeZone)
|
|||||||
hooks: p[1].hooks,
|
hooks: p[1].hooks,
|
||||||
})),
|
})),
|
||||||
tzDate,
|
tzDate,
|
||||||
} as any;
|
};
|
||||||
}, [plugins, width, height, tzDate]);
|
}, [plugins, width, height, tzDate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -56,9 +56,13 @@ export interface PlotPluginProps {
|
|||||||
|
|
||||||
export interface PlotProps {
|
export interface PlotProps {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
children: React.ReactNode[];
|
width: number;
|
||||||
|
height: number;
|
||||||
|
children?: React.ReactNode | React.ReactNode[];
|
||||||
|
/** Callback performed when uPlot data is updated */
|
||||||
|
onDataUpdate?: (data: uPlot.AlignedData) => {};
|
||||||
|
/** Callback performed when uPlot is (re)initialized */
|
||||||
|
onPlotInit?: () => {};
|
||||||
}
|
}
|
||||||
|
@ -93,17 +93,19 @@ const isPlottingTime = (config: uPlot.Options) => {
|
|||||||
* Based on two config objects indicates whether or not uPlot needs reinitialisation
|
* Based on two config objects indicates whether or not uPlot needs reinitialisation
|
||||||
* This COULD be done based on data frames, but keeping it this way for now as a simplification
|
* This COULD be done based on data frames, but keeping it this way for now as a simplification
|
||||||
*/
|
*/
|
||||||
export const shouldReinitialisePlot = (prevConfig?: uPlot.Options, config?: uPlot.Options) => {
|
export const shouldInitialisePlot = (prevConfig?: uPlot.Options, config?: uPlot.Options) => {
|
||||||
if (!config && !prevConfig) {
|
if (!config && !prevConfig) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prevConfig && config) {
|
if (config) {
|
||||||
if (config.width === 0 || config.height === 0) {
|
if (config.width === 0 || config.height === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!prevConfig) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlottingTime(config!) && prevConfig!.tzDate !== config!.tzDate) {
|
if (isPlottingTime(config!) && prevConfig!.tzDate !== config!.tzDate) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -1,90 +1,21 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Area,
|
|
||||||
Canvas,
|
Canvas,
|
||||||
ContextMenuPlugin,
|
ContextMenuPlugin,
|
||||||
GraphCustomFieldConfig,
|
|
||||||
LegendDisplayMode,
|
LegendDisplayMode,
|
||||||
LegendPlugin,
|
LegendPlugin,
|
||||||
Line,
|
|
||||||
Point,
|
|
||||||
Scale,
|
|
||||||
SeriesGeometry,
|
|
||||||
TooltipPlugin,
|
TooltipPlugin,
|
||||||
UPlotChart,
|
|
||||||
ZoomPlugin,
|
ZoomPlugin,
|
||||||
useTheme,
|
GraphNG,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { PanelProps } from '@grafana/data';
|
||||||
import {
|
|
||||||
DataFrame,
|
|
||||||
FieldConfig,
|
|
||||||
FieldType,
|
|
||||||
formattedValueToString,
|
|
||||||
getTimeField,
|
|
||||||
PanelProps,
|
|
||||||
getFieldColorModeForField,
|
|
||||||
systemDateFormats,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { alignAndSortDataFramesByFieldName } from './utils';
|
|
||||||
import { VizLayout } from './VizLayout';
|
import { VizLayout } from './VizLayout';
|
||||||
|
|
||||||
import { Axis } from '@grafana/ui/src/components/uPlot/geometries/Axis';
|
|
||||||
import { timeFormatToTemplate } from '@grafana/ui/src/components/uPlot/utils';
|
|
||||||
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
|
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
|
||||||
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
|
||||||
|
|
||||||
interface GraphPanelProps extends PanelProps<Options> {}
|
interface GraphPanelProps extends PanelProps<Options> {}
|
||||||
|
|
||||||
const TIME_FIELD_NAME = 'Time';
|
|
||||||
|
|
||||||
const timeStampsConfig = [
|
|
||||||
[3600 * 24 * 365, '{YYYY}', 7, '{YYYY}'],
|
|
||||||
[3600 * 24 * 28, `{${timeFormatToTemplate(systemDateFormats.interval.month)}`, 7, '{MMM}\n{YYYY}'],
|
|
||||||
[
|
|
||||||
3600 * 24,
|
|
||||||
`{${timeFormatToTemplate(systemDateFormats.interval.day)}`,
|
|
||||||
7,
|
|
||||||
`${timeFormatToTemplate(systemDateFormats.interval.day)}\n${timeFormatToTemplate(systemDateFormats.interval.year)}`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
3600,
|
|
||||||
`{${timeFormatToTemplate(systemDateFormats.interval.minute)}`,
|
|
||||||
4,
|
|
||||||
`${timeFormatToTemplate(systemDateFormats.interval.minute)}\n${timeFormatToTemplate(
|
|
||||||
systemDateFormats.interval.day
|
|
||||||
)}`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
60,
|
|
||||||
`{${timeFormatToTemplate(systemDateFormats.interval.second)}`,
|
|
||||||
4,
|
|
||||||
`${timeFormatToTemplate(systemDateFormats.interval.second)}\n${timeFormatToTemplate(
|
|
||||||
systemDateFormats.interval.day
|
|
||||||
)}`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1,
|
|
||||||
`:{ss}`,
|
|
||||||
2,
|
|
||||||
`:{ss}\n${timeFormatToTemplate(systemDateFormats.interval.day)} ${timeFormatToTemplate(
|
|
||||||
systemDateFormats.interval.minute
|
|
||||||
)}`,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1e-3,
|
|
||||||
':{ss}.{fff}',
|
|
||||||
2,
|
|
||||||
`:{ss}.{fff}\n${timeFormatToTemplate(systemDateFormats.interval.day)} ${timeFormatToTemplate(
|
|
||||||
systemDateFormats.interval.minute
|
|
||||||
)}`,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
|
||||||
|
|
||||||
export const GraphPanel: React.FC<GraphPanelProps> = ({
|
export const GraphPanel: React.FC<GraphPanelProps> = ({
|
||||||
data,
|
data,
|
||||||
timeRange,
|
timeRange,
|
||||||
@ -94,117 +25,6 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
|
|||||||
options,
|
options,
|
||||||
onChangeTimeRange,
|
onChangeTimeRange,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
|
||||||
const [alignedData, setAlignedData] = useState<DataFrame | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data || !data.series?.length) {
|
|
||||||
setAlignedData(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subscription = alignAndSortDataFramesByFieldName(data.series, TIME_FIELD_NAME).subscribe(setAlignedData);
|
|
||||||
|
|
||||||
return function unsubscribe() {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (!alignedData) {
|
|
||||||
return (
|
|
||||||
<div className="panel-empty">
|
|
||||||
<p>No data found in response</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const geometries: React.ReactNode[] = [];
|
|
||||||
const scales: React.ReactNode[] = [];
|
|
||||||
const axes: React.ReactNode[] = [];
|
|
||||||
|
|
||||||
let { timeIndex } = getTimeField(alignedData);
|
|
||||||
|
|
||||||
if (timeIndex === undefined) {
|
|
||||||
timeIndex = 0; // assuming first field represents x-domain
|
|
||||||
scales.push(<Scale scaleKey="x" />);
|
|
||||||
} else {
|
|
||||||
scales.push(<Scale scaleKey="x" time />);
|
|
||||||
}
|
|
||||||
|
|
||||||
axes.push(<Axis scaleKey="x" values={timeStampsConfig} side={2} />);
|
|
||||||
|
|
||||||
let seriesIdx = 0;
|
|
||||||
const uniqueScales: Record<string, boolean> = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < alignedData.fields.length; i++) {
|
|
||||||
const seriesGeometry = [];
|
|
||||||
const field = alignedData.fields[i];
|
|
||||||
const config = field.config as FieldConfig<GraphCustomFieldConfig>;
|
|
||||||
const customConfig = config.custom;
|
|
||||||
|
|
||||||
if (i === timeIndex || field.type !== FieldType.number) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fmt = field.display ?? defaultFormatter;
|
|
||||||
const scale = config.unit || '__fixed';
|
|
||||||
|
|
||||||
if (!uniqueScales[scale]) {
|
|
||||||
uniqueScales[scale] = true;
|
|
||||||
scales.push(<Scale scaleKey={scale} />);
|
|
||||||
axes.push(
|
|
||||||
<Axis
|
|
||||||
key={`axis-${scale}-${i}`}
|
|
||||||
scaleKey={scale}
|
|
||||||
label={config.custom?.axis?.label}
|
|
||||||
size={config.custom?.axis?.width}
|
|
||||||
side={config.custom?.axis?.side || 3}
|
|
||||||
grid={config.custom?.axis?.grid}
|
|
||||||
formatValue={v => formattedValueToString(fmt(v))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to update field state here because we use a transform to merge frames
|
|
||||||
field.state = { ...field.state, seriesIndex: seriesIdx };
|
|
||||||
|
|
||||||
const colorMode = getFieldColorModeForField(field);
|
|
||||||
const seriesColor = colorMode.getCalculator(field, theme)(0, 0);
|
|
||||||
|
|
||||||
if (customConfig?.line?.show) {
|
|
||||||
seriesGeometry.push(
|
|
||||||
<Line
|
|
||||||
key={`line-${scale}-${i}`}
|
|
||||||
scaleKey={scale}
|
|
||||||
stroke={seriesColor}
|
|
||||||
width={customConfig?.line.show ? customConfig?.line.width || 1 : 0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customConfig?.points?.show) {
|
|
||||||
seriesGeometry.push(
|
|
||||||
<Point key={`point-${scale}-${i}`} scaleKey={scale} size={customConfig?.points?.radius} stroke={seriesColor} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customConfig?.fill?.alpha) {
|
|
||||||
seriesGeometry.push(
|
|
||||||
<Area key={`area-${scale}-${i}`} scaleKey={scale} fill={customConfig?.fill.alpha} color={seriesColor} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (seriesGeometry.length > 1) {
|
|
||||||
geometries.push(
|
|
||||||
<SeriesGeometry key={`seriesGeometry-${scale}-${i}`} scaleKey={scale}>
|
|
||||||
{seriesGeometry}
|
|
||||||
</SeriesGeometry>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
geometries.push(seriesGeometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesIdx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout width={width} height={height}>
|
<VizLayout width={width} height={height}>
|
||||||
{({ builder, getLayout }) => {
|
{({ builder, getLayout }) => {
|
||||||
@ -230,10 +50,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UPlotChart data={alignedData} timeRange={timeRange} timeZone={timeZone} {...canvasSize}>
|
<GraphNG data={data.series} timeRange={timeRange} timeZone={timeZone} {...canvasSize}>
|
||||||
{scales}
|
|
||||||
{axes}
|
|
||||||
{geometries}
|
|
||||||
{builder.addSlot('canvas', <Canvas />).render()}
|
{builder.addSlot('canvas', <Canvas />).render()}
|
||||||
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
|
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
|
||||||
<ZoomPlugin onZoom={onChangeTimeRange} />
|
<ZoomPlugin onZoom={onChangeTimeRange} />
|
||||||
@ -243,7 +60,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
|
|||||||
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
|
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
|
||||||
{/* TODO: */}
|
{/* TODO: */}
|
||||||
{/*<AnnotationsEditorPlugin />*/}
|
{/*<AnnotationsEditorPlugin />*/}
|
||||||
</UPlotChart>
|
</GraphNG>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</VizLayout>
|
</VizLayout>
|
||||||
|
@ -33,7 +33,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (plotCtx.isPlotReady && annotations.length > 0) {
|
if (plotCtx.isPlotReady) {
|
||||||
const views: Array<DataFrameView<AnnotationsDataFrameViewDTO>> = [];
|
const views: Array<DataFrameView<AnnotationsDataFrameViewDTO>> = [];
|
||||||
|
|
||||||
for (const frame of annotations) {
|
for (const frame of annotations) {
|
||||||
|
@ -42,7 +42,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
|||||||
|
|
||||||
// THIS EVENT ONLY MOCKS EXEMPLAR Y VALUE!!!! TO BE REMOVED WHEN WE GET CORRECT EXEMPLARS SHAPE VIA PROPS
|
// THIS EVENT ONLY MOCKS EXEMPLAR Y VALUE!!!! TO BE REMOVED WHEN WE GET CORRECT EXEMPLARS SHAPE VIA PROPS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (plotCtx.isPlotReady && exemplars.length) {
|
if (plotCtx.isPlotReady) {
|
||||||
const mocks: DataFrame[] = [];
|
const mocks: DataFrame[] = [];
|
||||||
|
|
||||||
for (const frame of exemplars) {
|
for (const frame of exemplars) {
|
||||||
|
Loading…
Reference in New Issue
Block a user