import { DataFrame, Field, FieldType, getDisplayProcessor, GrafanaTheme2, isBooleanUnit, TimeRange, cacheFieldDisplayNames, fieldMatchers, FieldMatcherID, } from '@grafana/data'; import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold'; import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue'; import { GraphFieldConfig, LineInterpolation, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema'; import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal'; import { XYFieldMatchers } from 'app/core/components/GraphNG/types'; import { HeatmapTooltip } from '../heatmap/panelcfg.gen'; type ScaleKey = string; // this will re-enumerate all enum fields on the same scale to create one ordinal progression // e.g. ['a','b'][0,1,0] + ['c','d'][1,0,1] -> ['a','b'][0,1,0] + ['c','d'][3,2,3] function reEnumFields(frames: DataFrame[]): DataFrame[] { let allTextsByKey: Map = new Map(); let frames2: DataFrame[] = frames.map((frame) => { return { ...frame, fields: frame.fields.map((field) => { if (field.type === FieldType.enum) { let scaleKey = buildScaleKey(field.config, field.type); let allTexts = allTextsByKey.get(scaleKey); if (!allTexts) { allTexts = []; allTextsByKey.set(scaleKey, allTexts); } let idxs: number[] = field.values.toArray().slice(); let txts = field.config.type!.enum!.text!; // by-reference incrementing if (allTexts.length > 0) { for (let i = 0; i < idxs.length; i++) { idxs[i] += allTexts.length; } } allTexts.push(...txts); // shared among all enum fields on same scale field.config.type!.enum!.text! = allTexts; return { ...field, values: idxs, }; // TODO: update displayProcessor? } return field; }), }; }); return frames2; } // 그룹화 되어 있는 값들을 풀어주는 함수(포인트 그래프에 제한적) function assembleData(series: DataFrame[]): DataFrame[] | null { let timeField: Field | null = null; // 시간 필드 const matchTimefn = fieldMatchers.get(FieldMatcherID.firstTimeField).get({}); for (let frame of series) { for (let field of frame.fields) { if (matchTimefn(field, frame, series)) { timeField = field; break; } } } if (!timeField) { return null; } const fields = series[0].fields.filter((f) => f.type === FieldType.string); if (fields.length === 0) { return null; } const alignedFieldTimes: number[] = []; const alignedFieldValues: number[] = []; const alignedFieldIDs: number[] = []; timeField.values.forEach((v, vi) => { const arrValues = JSON.parse(fields[0].values[vi]); const arrIDs = JSON.parse(fields[1].values[vi]); arrValues.forEach((f: number, i: number) => { alignedFieldTimes.push(v); alignedFieldValues.push(f); alignedFieldIDs.push(arrIDs[i]); }); }); const newFieldTime: Field = { values: alignedFieldTimes, name: timeField.name, type: timeField.type, config: timeField.config, state: timeField.state, display: timeField.display, }; const newFieldValues: Field = { values: alignedFieldValues, name: fields[0].name, type: FieldType.number, config: fields[0].config, state: fields[0].state, display: fields[0].display, }; const newFieldIDs: Field = { values: alignedFieldIDs, name: fields[1].name, type: FieldType.number, config: fields[1].config, state: fields[1].state, display: fields[1].display, }; const newFrame: DataFrame = { //length: series[0].length, fields: [newFieldTime, newFieldValues, newFieldIDs], length: 3, }; return [newFrame]; } export function preparePlotFramePoints(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) { const dataFrame: DataFrame = { length: frames[0].length, fields: frames[0].fields, }; return dataFrame; } /** * Returns null if there are no graphable fields */ export function prepareGraphableFields( series: DataFrame[], theme: GrafanaTheme2, timeRange?: TimeRange, // numeric X requires a single frame where the first field is numeric xNumFieldIdx?: number ): DataFrame[] | null { if (!series?.length) { return null; } const pointSeries = assembleData(series)!; if (pointSeries) { series = pointSeries; } cacheFieldDisplayNames(series); let useNumericX = xNumFieldIdx != null; // Make sure the numeric x field is first in the frame if (xNumFieldIdx != null && xNumFieldIdx > 0) { series = [ { ...series[0], fields: [series[0].fields[xNumFieldIdx], ...series[0].fields.filter((f, i) => i !== xNumFieldIdx)], }, ]; } // some datasources simply tag the field as time, but don't convert to milli epochs // so we're stuck with doing the parsing here to avoid Moment slowness everywhere later // this mutates (once) for (let frame of series) { for (let field of frame.fields) { if (field.type === FieldType.time && typeof field.values[0] !== 'number') { field.values = convertFieldType(field, { destinationType: FieldType.time }).values; } } } let enumFieldsCount = 0; loopy: for (let frame of series) { for (let field of frame.fields) { if (field.type === FieldType.enum && ++enumFieldsCount > 1) { series = reEnumFields(series); break loopy; } } } let copy: Field; const frames: DataFrame[] = []; for (let frame of series) { const fields: Field[] = []; let hasTimeField = false; let hasValueField = false; let nulledFrame = useNumericX ? frame : applyNullInsertThreshold({ frame, refFieldPseudoMin: timeRange?.from.valueOf(), refFieldPseudoMax: timeRange?.to.valueOf(), }); const frameFields = nullToValue(nulledFrame).fields; for (let fieldIdx = 0; fieldIdx < (frameFields?.length || 0); fieldIdx++) { const field = frameFields[fieldIdx]; switch (field.type) { case FieldType.time: hasTimeField = true; fields.push(field); break; case FieldType.number: hasValueField = useNumericX ? fieldIdx > 0 : true; copy = { ...field, values: field.values.map((v) => { if (!(Number.isFinite(v) || v == null)) { return null; } return v; }), }; fields.push(copy); break; // ok case FieldType.enum: hasValueField = true; case FieldType.string: copy = { ...field, values: field.values, }; fields.push(copy); break; // ok case FieldType.boolean: hasValueField = true; const custom: GraphFieldConfig = field.config?.custom ?? {}; const config = { ...field.config, max: 1, min: 0, custom, }; // smooth and linear do not make sense if (custom.lineInterpolation !== LineInterpolation.StepBefore) { custom.lineInterpolation = LineInterpolation.StepAfter; } copy = { ...field, config, type: FieldType.number, values: field.values.map((v) => { if (v == null) { return v; } return Boolean(v) ? 1 : 0; }), }; if (!isBooleanUnit(config.unit)) { config.unit = 'bool'; copy.display = getDisplayProcessor({ field: copy, theme }); } fields.push(copy); break; } } if ((useNumericX || hasTimeField) && hasValueField) { frames.push({ ...frame, length: nulledFrame.length, fields, }); } } if (frames.length) { setClassicPaletteIdxs(frames, theme, 0); matchEnumColorToSeriesColor(frames, theme); return frames; } return null; } const matchEnumColorToSeriesColor = (frames: DataFrame[], theme: GrafanaTheme2) => { const { palette } = theme.visualization; for (const frame of frames) { for (const field of frame.fields) { if (field.type === FieldType.enum) { const namedColor = palette[field.state?.seriesIndex! % palette.length]; const hexColor = theme.visualization.getColorByName(namedColor); const enumConfig = field.config.type!.enum!; enumConfig.color = Array(enumConfig.text!.length).fill(hexColor); field.display = getDisplayProcessor({ field, theme }); } } } }; export const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => { let seriesIndex = 0; frames.forEach((frame) => { frame.fields.forEach((field, fieldIdx) => { if ( fieldIdx !== skipFieldIdx && (field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum) ) { field.state = { ...field.state, seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)? }; field.display = getDisplayProcessor({ field, theme }); } }); }); }; export function getTimezones(timezones: string[] | undefined, defaultTimezone: string): string[] { if (!timezones || !timezones.length) { return [defaultTimezone]; } return timezones.map((v) => (v?.length ? v : defaultTimezone)); } export const isTooltipScrollable = (tooltipOptions: VizTooltipOptions | HeatmapTooltip) => { return tooltipOptions.mode === TooltipDisplayMode.Multi && tooltipOptions.maxHeight != null; };