mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
DataFrame: cache frame/field index in field state (#30529)
This commit is contained in:
parent
f97348ff5b
commit
f2327baf66
@ -162,6 +162,13 @@ export interface FieldState {
|
||||
* Useful for assigning color to series by looking up a color in a palette using this index
|
||||
*/
|
||||
seriesIndex?: number;
|
||||
|
||||
/**
|
||||
* Location of this field within the context frames results
|
||||
*
|
||||
* @internal -- we will try to make this unnecessary
|
||||
*/
|
||||
origin?: DataFrameFieldIndex;
|
||||
}
|
||||
|
||||
export interface NumericRange {
|
||||
@ -206,7 +213,8 @@ export const TIME_SERIES_METRIC_FIELD_NAME = 'Metric';
|
||||
/**
|
||||
* Describes where a specific data frame field is located within a
|
||||
* dataset of type DataFrame[]
|
||||
* @public
|
||||
*
|
||||
* @internal -- we will try to make this unnecessary
|
||||
*/
|
||||
export interface DataFrameFieldIndex {
|
||||
frameIndex: number;
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
reduceField,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { alignDataFrames } from './utils';
|
||||
import { joinDataFrames } from './utils';
|
||||
import { useTheme } from '../../themes';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { PlotProps } from '../uPlot/types';
|
||||
@ -64,9 +64,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
const theme = useTheme();
|
||||
const hasLegend = useRef(legend && legend.displayMode !== LegendDisplayMode.Hidden);
|
||||
|
||||
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
|
||||
const alignedFrame = alignedFrameWithGapTest?.frame;
|
||||
const getDataFrameFieldIndex = alignedFrameWithGapTest?.getDataFrameFieldIndex;
|
||||
const frame = useMemo(() => joinDataFrames(data, fields), [data, fields]);
|
||||
|
||||
const compareFrames = useCallback((a?: DataFrame | null, b?: DataFrame | null) => {
|
||||
if (a && b) {
|
||||
@ -98,17 +96,17 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
currentTimeRange.current = timeRange;
|
||||
}, [timeRange]);
|
||||
|
||||
const configRev = useRevision(alignedFrame, compareFrames);
|
||||
const configRev = useRevision(frame, compareFrames);
|
||||
|
||||
const configBuilder = useMemo(() => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
if (!alignedFrame) {
|
||||
if (!frame) {
|
||||
return builder;
|
||||
}
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = alignedFrame.fields[0];
|
||||
const xField = frame.fields[0];
|
||||
|
||||
if (xField.type === FieldType.time) {
|
||||
builder.addScale({
|
||||
@ -141,8 +139,8 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
}
|
||||
let indexByName: Map<string, number> | undefined = undefined;
|
||||
|
||||
for (let i = 0; i < alignedFrame.fields.length; i++) {
|
||||
const field = alignedFrame.fields[i];
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
const field = frame.fields[i];
|
||||
const config = field.config as FieldConfig<GraphFieldConfig>;
|
||||
const customConfig: GraphFieldConfig = {
|
||||
...defaultConfig,
|
||||
@ -182,14 +180,13 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
}
|
||||
|
||||
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
|
||||
const dataFrameFieldIndex = getDataFrameFieldIndex ? getDataFrameFieldIndex(i) : undefined;
|
||||
|
||||
let { fillOpacity } = customConfig;
|
||||
if (customConfig.fillBelowTo) {
|
||||
if (!indexByName) {
|
||||
indexByName = getNamesToFieldIndex(alignedFrame);
|
||||
indexByName = getNamesToFieldIndex(frame);
|
||||
}
|
||||
const t = indexByName.get(getFieldDisplayName(field, alignedFrame));
|
||||
const t = indexByName.get(getFieldDisplayName(field, frame));
|
||||
const b = indexByName.get(customConfig.fillBelowTo);
|
||||
if (isNumber(b) && isNumber(t)) {
|
||||
builder.addBand({
|
||||
@ -221,15 +218,15 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
thresholds: config.thresholds,
|
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex,
|
||||
fieldName: getFieldDisplayName(field, alignedFrame),
|
||||
dataFrameFieldIndex: field.state?.origin,
|
||||
fieldName: getFieldDisplayName(field, frame),
|
||||
hideInLegend: customConfig.hideFrom?.legend,
|
||||
});
|
||||
}
|
||||
return builder;
|
||||
}, [configRev, timeZone]);
|
||||
|
||||
if (alignedFrameWithGapTest == null) {
|
||||
if (!frame) {
|
||||
return (
|
||||
<div className="panel-empty">
|
||||
<p>No data found in response</p>
|
||||
@ -299,7 +296,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
<VizLayout width={width} height={height} legend={legendElement}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
data={alignedFrameWithGapTest}
|
||||
data={frame}
|
||||
config={configBuilder}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { ArrayVector, DataFrame, FieldType, toDataFrame } from '@grafana/data';
|
||||
import { AlignedFrameWithGapTest } from '../uPlot/types';
|
||||
import { alignDataFrames, isLikelyAscendingVector } from './utils';
|
||||
import { joinDataFrames, isLikelyAscendingVector } from './utils';
|
||||
|
||||
describe('alignDataFrames', () => {
|
||||
describe('aligned frame', () => {
|
||||
describe('joinDataFrames', () => {
|
||||
describe('joined frame', () => {
|
||||
it('should align multiple data frames into one data frame', () => {
|
||||
const data: DataFrame[] = [
|
||||
toDataFrame({
|
||||
@ -20,37 +19,64 @@ describe('alignDataFrames', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const aligned = alignDataFrames(data);
|
||||
const joined = joinDataFrames(data);
|
||||
|
||||
expect(aligned?.frame.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
state: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature A',
|
||||
seriesIndex: 0,
|
||||
expect(joined?.fields).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "time",
|
||||
"state": Object {
|
||||
"origin": undefined,
|
||||
},
|
||||
"type": "time",
|
||||
"values": Array [
|
||||
1000,
|
||||
2000,
|
||||
3000,
|
||||
4000,
|
||||
],
|
||||
},
|
||||
name: 'temperature A',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([1, 3, 5, 7]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature B',
|
||||
seriesIndex: 1,
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature A",
|
||||
"state": Object {
|
||||
"displayName": "temperature A",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 0,
|
||||
},
|
||||
"seriesIndex": 0,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
],
|
||||
},
|
||||
name: 'temperature B',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([0, 2, 6, 7]),
|
||||
},
|
||||
]);
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature B",
|
||||
"state": Object {
|
||||
"displayName": "temperature B",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 1,
|
||||
},
|
||||
"seriesIndex": 1,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
0,
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should align multiple data frames into one data frame but only keep first time field', () => {
|
||||
@ -69,37 +95,64 @@ describe('alignDataFrames', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const aligned = alignDataFrames(data);
|
||||
const aligned = joinDataFrames(data);
|
||||
|
||||
expect(aligned?.frame.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
state: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature',
|
||||
seriesIndex: 0,
|
||||
expect(aligned?.fields).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "time",
|
||||
"state": Object {
|
||||
"origin": undefined,
|
||||
},
|
||||
"type": "time",
|
||||
"values": Array [
|
||||
1000,
|
||||
2000,
|
||||
3000,
|
||||
4000,
|
||||
],
|
||||
},
|
||||
name: 'temperature',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([1, 3, 5, 7]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature B',
|
||||
seriesIndex: 1,
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature",
|
||||
"state": Object {
|
||||
"displayName": "temperature",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 0,
|
||||
},
|
||||
"seriesIndex": 0,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
],
|
||||
},
|
||||
name: 'temperature B',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([0, 2, 6, 7]),
|
||||
},
|
||||
]);
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature B",
|
||||
"state": Object {
|
||||
"displayName": "temperature B",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 1,
|
||||
},
|
||||
"seriesIndex": 1,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
0,
|
||||
2,
|
||||
6,
|
||||
7,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should align multiple data frames into one data frame and skip non-numeric fields', () => {
|
||||
@ -113,27 +166,45 @@ describe('alignDataFrames', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const aligned = alignDataFrames(data);
|
||||
const aligned = joinDataFrames(data);
|
||||
|
||||
expect(aligned?.frame.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
state: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature',
|
||||
seriesIndex: 0,
|
||||
expect(aligned?.fields).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "time",
|
||||
"state": Object {
|
||||
"origin": undefined,
|
||||
},
|
||||
"type": "time",
|
||||
"values": Array [
|
||||
1000,
|
||||
2000,
|
||||
3000,
|
||||
4000,
|
||||
],
|
||||
},
|
||||
name: 'temperature',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([1, 3, 5, 7]),
|
||||
},
|
||||
]);
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature",
|
||||
"state": Object {
|
||||
"displayName": "temperature",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 0,
|
||||
},
|
||||
"seriesIndex": 0,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should align multiple data frames into one data frame and skip non-numeric fields', () => {
|
||||
@ -147,32 +218,50 @@ describe('alignDataFrames', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
const aligned = alignDataFrames(data);
|
||||
const aligned = joinDataFrames(data);
|
||||
|
||||
expect(aligned?.frame.fields).toEqual([
|
||||
{
|
||||
config: {},
|
||||
state: {},
|
||||
name: 'time',
|
||||
type: FieldType.time,
|
||||
values: new ArrayVector([1000, 2000, 3000, 4000]),
|
||||
},
|
||||
{
|
||||
config: {},
|
||||
state: {
|
||||
displayName: 'temperature',
|
||||
seriesIndex: 0,
|
||||
expect(aligned?.fields).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "time",
|
||||
"state": Object {
|
||||
"origin": undefined,
|
||||
},
|
||||
"type": "time",
|
||||
"values": Array [
|
||||
1000,
|
||||
2000,
|
||||
3000,
|
||||
4000,
|
||||
],
|
||||
},
|
||||
name: 'temperature',
|
||||
type: FieldType.number,
|
||||
values: new ArrayVector([1, 3, 5, 7]),
|
||||
},
|
||||
]);
|
||||
Object {
|
||||
"config": Object {},
|
||||
"name": "temperature",
|
||||
"state": Object {
|
||||
"displayName": "temperature",
|
||||
"origin": Object {
|
||||
"fieldIndex": 1,
|
||||
"frameIndex": 0,
|
||||
},
|
||||
"seriesIndex": 0,
|
||||
},
|
||||
"type": "number",
|
||||
"values": Array [
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
7,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDataFrameFieldIndex', () => {
|
||||
let aligned: AlignedFrameWithGapTest | null;
|
||||
let aligned: DataFrame | null;
|
||||
|
||||
beforeAll(() => {
|
||||
const data: DataFrame[] = [
|
||||
@ -197,7 +286,7 @@ describe('alignDataFrames', () => {
|
||||
}),
|
||||
];
|
||||
|
||||
aligned = alignDataFrames(data);
|
||||
aligned = joinDataFrames(data);
|
||||
});
|
||||
|
||||
it.each`
|
||||
@ -209,7 +298,7 @@ describe('alignDataFrames', () => {
|
||||
`('should return correct index for yDim', ({ yDim, index }) => {
|
||||
const [frameIndex, fieldIndex] = index;
|
||||
|
||||
expect(aligned?.getDataFrameFieldIndex(yDim)).toEqual({
|
||||
expect(aligned?.fields[yDim].state?.origin).toEqual({
|
||||
frameIndex,
|
||||
fieldIndex,
|
||||
});
|
||||
|
@ -12,7 +12,6 @@ import {
|
||||
sortDataFrame,
|
||||
Vector,
|
||||
} from '@grafana/data';
|
||||
import { AlignedFrameWithGapTest } from '../uPlot/types';
|
||||
import uPlot, { AlignedData, JoinNullMode } from 'uplot';
|
||||
import { XYFieldMatchers } from './GraphNG';
|
||||
|
||||
@ -44,7 +43,7 @@ export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?:
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null {
|
||||
export function joinDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): DataFrame | null {
|
||||
const valuesFromFrames: AlignedData[] = [];
|
||||
const sourceFields: Field[] = [];
|
||||
const sourceFieldsRefs: Record<number, DataFrameFieldIndex> = {};
|
||||
@ -118,39 +117,34 @@ export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers):
|
||||
}
|
||||
|
||||
// do the actual alignment (outerJoin on the first arrays)
|
||||
let alignedData = uPlot.join(valuesFromFrames, nullModes);
|
||||
let joinedData = uPlot.join(valuesFromFrames, nullModes);
|
||||
|
||||
if (alignedData!.length !== sourceFields.length) {
|
||||
if (joinedData!.length !== sourceFields.length) {
|
||||
throw new Error('outerJoinValues lost a field?');
|
||||
}
|
||||
|
||||
let seriesIdx = 0;
|
||||
// Replace the values from the outer-join field
|
||||
return {
|
||||
frame: {
|
||||
length: alignedData![0].length,
|
||||
fields: alignedData!.map((vals, idx) => {
|
||||
let state: FieldState = { ...sourceFields[idx].state };
|
||||
...frames[0],
|
||||
length: joinedData![0].length,
|
||||
fields: joinedData!.map((vals, idx) => {
|
||||
let state: FieldState = {
|
||||
...sourceFields[idx].state,
|
||||
origin: sourceFieldsRefs[idx],
|
||||
};
|
||||
|
||||
if (sourceFields[idx].type !== FieldType.time) {
|
||||
state.seriesIndex = seriesIdx;
|
||||
seriesIdx++;
|
||||
}
|
||||
|
||||
return {
|
||||
...sourceFields[idx],
|
||||
state,
|
||||
values: new ArrayVector(vals),
|
||||
};
|
||||
}),
|
||||
},
|
||||
getDataFrameFieldIndex: (alignedFieldIndex: number) => {
|
||||
const index = sourceFieldsRefs[alignedFieldIndex];
|
||||
if (!index) {
|
||||
throw new Error(`Could not find index for ${alignedFieldIndex}`);
|
||||
if (sourceFields[idx].type !== FieldType.time) {
|
||||
state.seriesIndex = seriesIdx;
|
||||
seriesIdx++;
|
||||
}
|
||||
return index;
|
||||
},
|
||||
|
||||
return {
|
||||
...sourceFields[idx],
|
||||
state,
|
||||
values: new ArrayVector(vals),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -160,10 +160,7 @@ export class Sparkline extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<UPlotChart
|
||||
data={{
|
||||
frame: data,
|
||||
getDataFrameFieldIndex: () => undefined,
|
||||
}}
|
||||
data={data}
|
||||
config={configBuilder}
|
||||
width={width}
|
||||
height={height}
|
||||
|
@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { UPlotChart } from './Plot';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import { ArrayVector, DataFrame, dateTime, FieldConfig, FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { ArrayVector, dateTime, FieldConfig, FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { GraphFieldConfig, DrawStyle } from '../uPlot/config';
|
||||
import uPlot from 'uplot';
|
||||
import createMockRaf from 'mock-raf';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
import { AlignedFrameWithGapTest } from './types';
|
||||
|
||||
const mockRaf = createMockRaf();
|
||||
const setDataMock = jest.fn();
|
||||
@ -69,11 +68,10 @@ describe('UPlotChart', () => {
|
||||
|
||||
it('destroys uPlot instance when component unmounts', () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const uPlotData = createPlotData(data);
|
||||
|
||||
const { unmount } = render(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -95,11 +93,10 @@ describe('UPlotChart', () => {
|
||||
describe('data update', () => {
|
||||
it('skips uPlot reinitialization when there are no field config changes', () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const uPlotData = createPlotData(data);
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -116,11 +113,10 @@ describe('UPlotChart', () => {
|
||||
expect(uPlot).toBeCalledTimes(1);
|
||||
|
||||
data.fields[1].values.set(0, 1);
|
||||
uPlotData.frame = data;
|
||||
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // changed
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -136,10 +132,9 @@ describe('UPlotChart', () => {
|
||||
describe('config update', () => {
|
||||
it('skips uPlot intialization for width and height equal 0', async () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const uPlotData = createPlotData(data);
|
||||
|
||||
const { queryAllByTestId } = render(
|
||||
<UPlotChart data={uPlotData} config={config} timeRange={timeRange} timeZone={'browser'} width={0} height={0} />
|
||||
<UPlotChart data={data} config={config} timeRange={timeRange} timeZone={'browser'} width={0} height={0} />
|
||||
);
|
||||
|
||||
expect(queryAllByTestId('uplot-main-div')).toHaveLength(1);
|
||||
@ -148,11 +143,10 @@ describe('UPlotChart', () => {
|
||||
|
||||
it('reinitializes uPlot when config changes', () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const uPlotData = createPlotData(data);
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -170,7 +164,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data}
|
||||
config={new UPlotConfigBuilder()}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -185,11 +179,10 @@ describe('UPlotChart', () => {
|
||||
|
||||
it('skips uPlot reinitialization when only dimensions change', () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const uPlotData = createPlotData(data);
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -205,7 +198,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={uPlotData}
|
||||
data={data} // frame
|
||||
config={new UPlotConfigBuilder()}
|
||||
timeRange={timeRange}
|
||||
timeZone={'browser'}
|
||||
@ -220,10 +213,3 @@ describe('UPlotChart', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const createPlotData = (frame: DataFrame): AlignedFrameWithGapTest => {
|
||||
return {
|
||||
frame,
|
||||
getDataFrameFieldIndex: () => undefined,
|
||||
};
|
||||
};
|
||||
|
@ -39,7 +39,7 @@ export const UPlotChart: React.FC<PlotProps> = (props) => {
|
||||
|
||||
// 1. When config is ready and there is no uPlot instance, create new uPlot and return
|
||||
if (isConfigReady && !plotInstance.current) {
|
||||
plotInstance.current = initializePlot(prepareData(props.data.frame), currentConfig.current, canvasRef.current);
|
||||
plotInstance.current = initializePlot(prepareData(props.data), currentConfig.current, canvasRef.current);
|
||||
setIsPlotReady(true);
|
||||
return;
|
||||
}
|
||||
@ -60,12 +60,12 @@ export const UPlotChart: React.FC<PlotProps> = (props) => {
|
||||
pluginLog('uPlot core', false, 'destroying instance');
|
||||
plotInstance.current.destroy();
|
||||
}
|
||||
plotInstance.current = initializePlot(prepareData(props.data.frame), currentConfig.current, canvasRef.current);
|
||||
plotInstance.current = initializePlot(prepareData(props.data), currentConfig.current, canvasRef.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Otherwise, assume only data has changed and update uPlot data
|
||||
updateData(props.data.frame, props.config, plotInstance.current, prepareData(props.data.frame));
|
||||
updateData(props.data, props.config, plotInstance.current, prepareData(props.data));
|
||||
}, [props, isConfigReady]);
|
||||
|
||||
// When component unmounts, clean the existing uPlot instance
|
||||
@ -86,7 +86,7 @@ export const UPlotChart: React.FC<PlotProps> = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
function prepareData(frame: DataFrame) {
|
||||
function prepareData(frame: DataFrame): AlignedData {
|
||||
return frame.fields.map((f) => f.values.toArray()) as AlignedData;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import uPlot, { Series } from 'uplot';
|
||||
import { PlotPlugin, AlignedFrameWithGapTest } from './types';
|
||||
import { PlotPlugin } from './types';
|
||||
import { DataFrame, Field, FieldConfig } from '@grafana/data';
|
||||
|
||||
interface PlotCanvasContextType {
|
||||
@ -26,7 +26,7 @@ interface PlotContextType extends PlotPluginsContextType {
|
||||
getSeries: () => Series[];
|
||||
getCanvas: () => PlotCanvasContextType;
|
||||
canvasRef: any;
|
||||
data: AlignedFrameWithGapTest;
|
||||
data: DataFrame;
|
||||
}
|
||||
|
||||
export const PlotContext = React.createContext<PlotContextType>({} as PlotContextType);
|
||||
@ -76,7 +76,7 @@ export const usePlotData = (): PlotDataAPI => {
|
||||
if (!ctx) {
|
||||
throwWhenNoContext('usePlotData');
|
||||
}
|
||||
return ctx!.data.frame.fields[idx];
|
||||
return ctx!.data.fields[idx];
|
||||
},
|
||||
[ctx]
|
||||
);
|
||||
@ -109,7 +109,7 @@ export const usePlotData = (): PlotDataAPI => {
|
||||
}
|
||||
// by uPlot convention x-axis is always first field
|
||||
// this may change when we introduce non-time x-axis and multiple x-axes (https://leeoniya.github.io/uPlot/demos/time-periods.html)
|
||||
return ctx!.data.frame.fields.slice(1);
|
||||
return ctx!.data.fields.slice(1);
|
||||
}, [ctx]);
|
||||
|
||||
if (!ctx) {
|
||||
@ -117,7 +117,7 @@ export const usePlotData = (): PlotDataAPI => {
|
||||
}
|
||||
|
||||
return {
|
||||
data: ctx.data.frame,
|
||||
data: ctx.data,
|
||||
getField,
|
||||
getFieldValue,
|
||||
getFieldConfig,
|
||||
@ -129,7 +129,7 @@ export const usePlotData = (): PlotDataAPI => {
|
||||
export const buildPlotContext = (
|
||||
isPlotReady: boolean,
|
||||
canvasRef: any,
|
||||
data: AlignedFrameWithGapTest,
|
||||
data: DataFrame,
|
||||
registerPlugin: any,
|
||||
getPlotInstance: () => uPlot | undefined
|
||||
): PlotContextType => {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import uPlot, { Options, Hooks } from 'uplot';
|
||||
import { DataFrame, DataFrameFieldIndex, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { DataFrame, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
|
||||
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor' | 'bands'>;
|
||||
@ -16,7 +16,7 @@ export interface PlotPluginProps {
|
||||
}
|
||||
|
||||
export interface PlotProps {
|
||||
data: AlignedFrameWithGapTest;
|
||||
data: DataFrame;
|
||||
timeRange: TimeRange;
|
||||
timeZone: TimeZone;
|
||||
width: number;
|
||||
@ -29,8 +29,3 @@ export abstract class PlotConfigBuilder<P, T> {
|
||||
constructor(public props: P) {}
|
||||
abstract getConfig(): T;
|
||||
}
|
||||
|
||||
export interface AlignedFrameWithGapTest {
|
||||
frame: DataFrame;
|
||||
getDataFrameFieldIndex: (alignedFieldIndex: number) => DataFrameFieldIndex | undefined;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user