grafana/public/app/plugins/panel/timeseries/utils.ts
Piotr Jamróz 2ae226de89
Explore: Decouple SplitOpen and getFieldLinksForExplore from Panel visualizations (#71811)
* Allow overriding internal data link supplier

* Remove SplitOpen and getFieldLinksForExplore dependencies

* Fix checking if row index is provided

* Fix unit test

* Add a comment

* Mark SplitOpen as deprecated

* Use Panel Context to provide internal data link supplier

* Update packages/grafana-ui/src/components/PanelChrome/PanelContext.ts

Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>

* Update packages/grafana-data/src/utils/dataLinks.ts

Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>

* Add missing eventsScope

* Fix infinite render loops

* Rename internal data link supplier to data link post processor

* Update packages/grafana-data/src/field/fieldOverrides.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>

---------

Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
2023-07-31 14:10:03 +02:00

314 lines
8.9 KiB
TypeScript

import {
ArrayVector,
DataFrame,
Field,
FieldType,
getDisplayProcessor,
getLinksSupplier,
GrafanaTheme2,
DataLinkPostProcessor,
InterpolateFunction,
isBooleanUnit,
SortedVector,
TimeRange,
} from '@grafana/data';
import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType';
import { GraphFieldConfig, LineInterpolation } from '@grafana/schema';
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
import { buildScaleKey } from '@grafana/ui/src/components/GraphNG/utils';
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[]) {
let allTextsByKey: Map<ScaleKey, string[]> = 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: new ArrayVector(idxs),
};
// TODO: update displayProcessor?
}
return field;
}),
};
});
return frames2;
}
/**
* 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;
}
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 });
}
}
}
};
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 function regenerateLinksSupplier(
alignedDataFrame: DataFrame,
frames: DataFrame[],
replaceVariables: InterpolateFunction,
timeZone: string,
dataLinkPostProcessor?: DataLinkPostProcessor
): DataFrame {
alignedDataFrame.fields.forEach((field) => {
if (field.state?.origin?.frameIndex === undefined || frames[field.state?.origin?.frameIndex] === undefined) {
return;
}
/* check if field has sortedVector values
if it does, sort all string fields in the original frame by the order array already used for the field
otherwise just attach the fields to the temporary frame used to get the links
*/
const tempFields: Field[] = [];
for (const frameField of frames[field.state?.origin?.frameIndex].fields) {
if (frameField.type === FieldType.string) {
if (field.values instanceof SortedVector) {
const copiedField = { ...frameField };
copiedField.values = new SortedVector(frameField.values, field.values.getOrderArray());
tempFields.push(copiedField);
} else {
tempFields.push(frameField);
}
}
}
const tempFrame: DataFrame = {
fields: [...alignedDataFrame.fields, ...tempFields],
length: alignedDataFrame.fields.length + tempFields.length,
};
field.getLinks = getLinksSupplier(
tempFrame,
field,
field.state!.scopedVars!,
replaceVariables,
timeZone,
dataLinkPostProcessor
);
});
return alignedDataFrame;
}