Commit 9dbf54eb by Ryan McKinley Committed by GitHub

GraphNG: support x != time in library (#29353)

parent 0b451486
......@@ -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',
......
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
// 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',
isTime: true,
});
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;
}
......
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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment