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