diff --git a/packages/grafana-data/src/transformations/matchers.ts b/packages/grafana-data/src/transformations/matchers.ts index 9b8d771e37c..a2ab1596c67 100644 --- a/packages/grafana-data/src/transformations/matchers.ts +++ b/packages/grafana-data/src/transformations/matchers.ts @@ -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(() => { ...getFieldPredicateMatchers(), // Predicates ...getFieldTypeMatchers(), // by type ...getFieldNameMatchers(), // by name + ...getSimpleFieldMatchers(), // first ]; }); @@ -43,9 +45,6 @@ export const frameMatchers = new Registry(() => { */ 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); } diff --git a/packages/grafana-data/src/transformations/matchers/ids.ts b/packages/grafana-data/src/transformations/matchers/ids.ts index 16f57f467f7..cd3976627a0 100644 --- a/packages/grafana-data/src/transformations/matchers/ids.ts +++ b/packages/grafana-data/src/transformations/matchers/ids.ts @@ -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', diff --git a/packages/grafana-data/src/transformations/matchers/simpleFieldMatcher.ts b/packages/grafana-data/src/transformations/matchers/simpleFieldMatcher.ts new file mode 100644 index 00000000000..b3a9fd3c49b --- /dev/null +++ b/packages/grafana-data/src/transformations/matchers/simpleFieldMatcher.ts @@ -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]; +} diff --git a/packages/grafana-data/src/utils/Registry.ts b/packages/grafana-data/src/utils/Registry.ts index 6dadbef2c82..0db77daa397 100644 --- a/packages/grafana-data/src/utils/Registry.ts +++ b/packages/grafana-data/src/utils/Registry.ts @@ -69,7 +69,7 @@ export class Registry { 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; } diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 85adaece96b..4d4268d688f 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -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 { 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 = ({ data, + fields, children, width, height, @@ -43,7 +50,7 @@ export const GraphNG: React.FC = ({ 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 = ({ 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 = ({ const config = field.config as FieldConfig; const customConfig = config.custom || defaultConfig; - if (i === timeIndex || field.type !== FieldType.number) { + if (field === xField || field.type !== FieldType.number) { continue; } diff --git a/packages/grafana-ui/src/components/GraphNG/utils.ts b/packages/grafana-ui/src/components/GraphNG/utils.ts index 499024cb35c..b865a33f1e6 100755 --- a/packages/grafana-ui/src/components/GraphNG/utils.ts +++ b/packages/grafana-ui/src/components/GraphNG/utils.ts @@ -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) {