mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: support x != time in library (#29353)
This commit is contained in:
parent
0b451486f8
commit
9dbf54eb61
@ -11,6 +11,7 @@ import {
|
||||
FrameMatcher,
|
||||
} from '../types/transformations';
|
||||
import { Registry } from '../utils/Registry';
|
||||
import { getSimpleFieldMatchers } from './matchers/simpleFieldMatcher';
|
||||
|
||||
/**
|
||||
* Registry that contains all of the built in field matchers.
|
||||
@ -21,6 +22,7 @@ export const fieldMatchers = new Registry<FieldMatcherInfo>(() => {
|
||||
...getFieldPredicateMatchers(), // Predicates
|
||||
...getFieldTypeMatchers(), // by type
|
||||
...getFieldNameMatchers(), // by name
|
||||
...getSimpleFieldMatchers(), // first
|
||||
];
|
||||
});
|
||||
|
||||
@ -43,9 +45,6 @@ export const frameMatchers = new Registry<FrameMatcherInfo>(() => {
|
||||
*/
|
||||
export function getFieldMatcher(config: MatcherConfig): FieldMatcher {
|
||||
const info = fieldMatchers.get(config.id);
|
||||
if (!info) {
|
||||
throw new Error('Unknown Matcher: ' + config.id);
|
||||
}
|
||||
return info.get(config.options);
|
||||
}
|
||||
|
||||
@ -56,8 +55,5 @@ export function getFieldMatcher(config: MatcherConfig): FieldMatcher {
|
||||
*/
|
||||
export function getFrameMatchers(config: MatcherConfig): FrameMatcher {
|
||||
const info = frameMatchers.get(config.id);
|
||||
if (!info) {
|
||||
throw new Error('Unknown Matcher: ' + config.id);
|
||||
}
|
||||
return info.get(config.options);
|
||||
}
|
||||
|
@ -13,7 +13,9 @@ export enum MatcherID {
|
||||
export enum FieldMatcherID {
|
||||
// Specific Types
|
||||
numeric = 'numeric',
|
||||
time = 'time',
|
||||
time = 'time', // Can be multiple times
|
||||
first = 'first',
|
||||
firstTimeField = 'firstTimeField', // Only the first fime field
|
||||
|
||||
// With arguments
|
||||
byType = 'byType',
|
||||
|
@ -0,0 +1,42 @@
|
||||
import { Field, FieldType, DataFrame } from '../../types/dataFrame';
|
||||
import { FieldMatcherID } from './ids';
|
||||
import { FieldMatcherInfo } from '../../types/transformations';
|
||||
|
||||
const firstFieldMatcher: FieldMatcherInfo = {
|
||||
id: FieldMatcherID.first,
|
||||
name: 'First Field',
|
||||
description: 'The first field in the frame',
|
||||
|
||||
get: (type: FieldType) => {
|
||||
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
|
||||
return field === frame.fields[0];
|
||||
};
|
||||
},
|
||||
|
||||
getOptionsDisplayText: () => {
|
||||
return `First field`;
|
||||
},
|
||||
};
|
||||
|
||||
const firstTimeFieldMatcher: FieldMatcherInfo = {
|
||||
id: FieldMatcherID.firstTimeField,
|
||||
name: 'First time field',
|
||||
description: 'The first field of type time in a frame',
|
||||
|
||||
get: (type: FieldType) => {
|
||||
return (field: Field, frame: DataFrame, allFrames: DataFrame[]) => {
|
||||
return field.type === FieldType.time && field === frame.fields.find(f => f.type === FieldType.time);
|
||||
};
|
||||
},
|
||||
|
||||
getOptionsDisplayText: () => {
|
||||
return `First time field`;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry Initialization
|
||||
*/
|
||||
export function getSimpleFieldMatchers(): FieldMatcherInfo[] {
|
||||
return [firstFieldMatcher, firstTimeFieldMatcher];
|
||||
}
|
@ -69,7 +69,7 @@ export class Registry<T extends RegistryItem> {
|
||||
get(id: string): T {
|
||||
const v = this.getIfExists(id);
|
||||
if (!v) {
|
||||
throw new Error('Undefined: ' + id);
|
||||
throw new Error(`"${id}" not found in: ${this.list().map(v => v.id)}`);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
@ -3,13 +3,13 @@ import {
|
||||
compareDataFrameStructures,
|
||||
DataFrame,
|
||||
FieldConfig,
|
||||
FieldMatcher,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldColorModeForField,
|
||||
getFieldDisplayName,
|
||||
getTimeField,
|
||||
} from '@grafana/data';
|
||||
import { mergeTimeSeriesData } from './utils';
|
||||
import { alignDataFrames } from './utils';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { PlotProps } from '../uPlot/types';
|
||||
import { AxisPlacement, GraphFieldConfig, GraphMode, PointMode } from '../uPlot/config';
|
||||
@ -22,9 +22,15 @@ import { useRevision } from '../uPlot/hooks';
|
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
|
||||
export interface XYFieldMatchers {
|
||||
x: FieldMatcher;
|
||||
y: FieldMatcher;
|
||||
}
|
||||
|
||||
export interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
|
||||
data: DataFrame[];
|
||||
legend?: LegendOptions;
|
||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||
}
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
@ -35,6 +41,7 @@ const defaultConfig: GraphFieldConfig = {
|
||||
|
||||
export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
data,
|
||||
fields,
|
||||
children,
|
||||
width,
|
||||
height,
|
||||
@ -43,7 +50,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
timeZone,
|
||||
...plotProps
|
||||
}) => {
|
||||
const alignedFrameWithGapTest = useMemo(() => mergeTimeSeriesData(data), [data]);
|
||||
const alignedFrameWithGapTest = useMemo(() => alignDataFrames(data, fields), [data, fields]);
|
||||
|
||||
if (alignedFrameWithGapTest == null) {
|
||||
return (
|
||||
@ -66,28 +73,32 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
const configBuilder = useMemo(() => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
|
||||
let { timeIndex } = getTimeField(alignedFrame);
|
||||
|
||||
if (timeIndex === undefined) {
|
||||
timeIndex = 0; // assuming first field represents x-domain
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
});
|
||||
} else {
|
||||
// X is the first field in the alligned frame
|
||||
const xField = alignedFrame.fields[0];
|
||||
if (xField.type === FieldType.time) {
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
});
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
placement: AxisPlacement.Bottom,
|
||||
timeZone,
|
||||
theme,
|
||||
});
|
||||
} else {
|
||||
// Not time!
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
});
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
placement: AxisPlacement.Bottom,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
placement: AxisPlacement.Bottom,
|
||||
timeZone,
|
||||
theme,
|
||||
});
|
||||
|
||||
let seriesIdx = 0;
|
||||
const legendItems: LegendItem[] = [];
|
||||
|
||||
@ -96,7 +107,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
const config = field.config as FieldConfig<GraphFieldConfig>;
|
||||
const customConfig = config.custom || defaultConfig;
|
||||
|
||||
if (i === timeIndex || field.type !== FieldType.number) {
|
||||
if (field === xField || field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -1,64 +1,90 @@
|
||||
import {
|
||||
DataFrame,
|
||||
FieldType,
|
||||
getTimeField,
|
||||
ArrayVector,
|
||||
NullValueMode,
|
||||
getFieldDisplayName,
|
||||
Field,
|
||||
fieldMatchers,
|
||||
FieldMatcherID,
|
||||
} from '@grafana/data';
|
||||
import { AlignedFrameWithGapTest } from '../uPlot/types';
|
||||
import uPlot, { AlignedData, AlignedDataWithGapTest } from 'uplot';
|
||||
import { XYFieldMatchers } from './GraphNG';
|
||||
|
||||
// the results ofter passing though data
|
||||
export interface XYDimensionFields {
|
||||
x: Field[];
|
||||
y: Field[];
|
||||
}
|
||||
|
||||
export function mapDimesions(match: XYFieldMatchers, frame: DataFrame, frames?: DataFrame[]): XYDimensionFields {
|
||||
const out: XYDimensionFields = {
|
||||
x: [],
|
||||
y: [],
|
||||
};
|
||||
for (const field of frame.fields) {
|
||||
if (match.x(field, frame, frames ?? [])) {
|
||||
out.x.push(field);
|
||||
}
|
||||
if (match.y(field, frame, frames ?? [])) {
|
||||
out.y.push(field);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single DataFrame with:
|
||||
* - A shared time column
|
||||
* - only numeric fields
|
||||
*
|
||||
* The input expects all frames to have a time field with values in ascending order
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export function mergeTimeSeriesData(frames: DataFrame[]): AlignedFrameWithGapTest | null {
|
||||
export function alignDataFrames(frames: DataFrame[], fields?: XYFieldMatchers): AlignedFrameWithGapTest | null {
|
||||
const valuesFromFrames: AlignedData[] = [];
|
||||
const sourceFields: Field[] = [];
|
||||
|
||||
// Default to timeseries config
|
||||
if (!fields) {
|
||||
fields = {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
};
|
||||
}
|
||||
|
||||
for (const frame of frames) {
|
||||
const { timeField } = getTimeField(frame);
|
||||
if (!timeField) {
|
||||
continue;
|
||||
const dims = mapDimesions(fields, frame, frames);
|
||||
if (!(dims.x.length && dims.y.length)) {
|
||||
continue; // both x and y matched something!
|
||||
}
|
||||
|
||||
if (dims.x.length > 1) {
|
||||
throw new Error('Only a single x field is supported');
|
||||
}
|
||||
|
||||
// Add the first X axis
|
||||
if (!sourceFields.length) {
|
||||
sourceFields.push(dims.x[0]);
|
||||
}
|
||||
|
||||
const alignedData: AlignedData = [
|
||||
timeField.values.toArray(), // The x axis (time)
|
||||
dims.x[0].values.toArray(), // The x axis (time)
|
||||
];
|
||||
|
||||
// find numeric fields
|
||||
for (const field of frame.fields) {
|
||||
if (field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the Y values
|
||||
for (const field of dims.y) {
|
||||
let values = field.values.toArray();
|
||||
if (field.config.nullValueMode === NullValueMode.AsZero) {
|
||||
values = values.map(v => (v === null ? 0 : v));
|
||||
}
|
||||
alignedData.push(values);
|
||||
|
||||
// Add the first time field
|
||||
if (sourceFields.length < 1) {
|
||||
sourceFields.push(timeField);
|
||||
}
|
||||
|
||||
// This will cache an appropriate field name in the field state
|
||||
getFieldDisplayName(field, frame, frames);
|
||||
sourceFields.push(field);
|
||||
}
|
||||
|
||||
// Timeseries has tima and at least one number
|
||||
if (alignedData.length > 1) {
|
||||
valuesFromFrames.push(alignedData);
|
||||
}
|
||||
valuesFromFrames.push(alignedData);
|
||||
}
|
||||
|
||||
if (valuesFromFrames.length === 0) {
|
||||
|
Loading…
Reference in New Issue
Block a user