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