Chore: Move internal GraphNG+Timeseries components into core (#77525)

* move to core where possible

* update imports

* ignore import order for now

* add graveyard files

* update codeowners
This commit is contained in:
Ryan McKinley
2023-11-01 21:59:55 -07:00
committed by GitHub
parent fb9732e319
commit c6e27e00b4
50 changed files with 3091 additions and 102 deletions
+66 -33
View File
@@ -256,6 +256,17 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"packages/grafana-data/src/transformations/transformers/nulls/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-data/src/transformations/transformers/nulls/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-data/src/transformations/transformers/reduce.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -802,34 +813,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Graph/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"packages/grafana-ui/src/components/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"packages/grafana-ui/src/components/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/GraphNG/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-ui/src/components/GraphNG/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/components/InfoBox/InfoBox.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@@ -981,11 +964,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Tags/Tag.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/components/ValuePicker/ValuePicker.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
@@ -1032,6 +1010,39 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-ui/src/graveyard/GraphNG/nullToUndefThreshold.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/options/builder/axis.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -1177,6 +1188,23 @@ exports[`better eslint`] = {
"public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/core/components/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"]
],
"public/app/core/components/GraphNG/hooks.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/core/components/Layers/LayerDragDropList.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@@ -1411,6 +1439,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/core/components/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/core/components/TraceToLogs/TagMappingInput.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
+4 -2
View File
@@ -312,9 +312,7 @@
/packages/grafana-ui/src/components/Table/ @grafana/grafana-bi-squad
/packages/grafana-ui/src/components/Gauge/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/BarGauge/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/GraphNG/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/Graph/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/TimeSeries/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/uPlot/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/DataLinks/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/ValuePicker/ @grafana/dataviz-squad
@@ -322,6 +320,8 @@
/packages/grafana-ui/src/components/VizLegend/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad
/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad
/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
/packages/grafana-data/src/transformations/ @grafana/grafana-bi-squad
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
@@ -366,6 +366,8 @@ cypress.config.js @grafana/grafana-frontend-platform
/public/app/core/components/TimePicker/ @grafana/grafana-frontend-platform
/public/app/core/components/Layers/ @grafana/dataviz-squad
/public/app/core/components/TraceToLogs @grafana/observability-traces-and-profiling
/public/app/core/components/GraphNG/ @grafana/dataviz-squad
/public/app/core/components/TimeSeries/ @grafana/dataviz-squad
/public/app/features/all.ts @grafana/grafana-frontend-platform
/public/app/features/admin/ @grafana/grafana-authnz-team
/public/app/features/auth-config/ @grafana/grafana-authnz-team
@@ -20,3 +20,6 @@ export type { RenameByRegexTransformerOptions } from './transformers/renameByReg
export { joinDataFrames as outerJoinDataFrames, isLikelyAscendingVector } from './transformers/joinDataFrames';
export * from './transformers/histogram';
export { ensureTimeField } from './transformers/convertFieldType';
// Required for Sparklines util to work in @grafana/data, but ideally kept internal
export { applyNullInsertThreshold } from './transformers/nulls/nullInsertThreshold';
@@ -0,0 +1,186 @@
import { DataFrame, FieldType } from '../../../types';
type InsertMode = (prev: number, next: number, threshold: number) => number;
const INSERT_MODES = {
threshold: (prev: number, next: number, threshold: number) => prev + threshold,
midpoint: (prev: number, next: number, threshold: number) => (prev + next) / 2,
// previous time + 1ms to prevent StateTimeline from forward-interpolating prior state
plusone: (prev: number, next: number, threshold: number) => prev + 1,
};
interface NullInsertOptions {
frame: DataFrame;
refFieldName?: string | null;
refFieldPseudoMax?: number;
refFieldPseudoMin?: number;
insertMode?: InsertMode;
}
function getRefField(frame: DataFrame, refFieldName?: string | null) {
return frame.fields.find((field) => {
// note: getFieldDisplayName() would require full DF[]
return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time;
});
}
/** @internal */
export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame {
if (opts.frame.length === 0) {
return opts.frame;
}
let thorough = true;
let { frame, refFieldName, refFieldPseudoMax, refFieldPseudoMin, insertMode } = opts;
if (!insertMode) {
insertMode = INSERT_MODES.threshold;
}
const refField = getRefField(frame, refFieldName);
if (refField == null) {
return frame;
}
refField.state = {
...refField.state,
nullThresholdApplied: true,
};
const thresholds = frame.fields.map((field) => field.config.custom?.insertNulls || refField.config.interval || null);
const uniqueThresholds = new Set<number>(thresholds);
uniqueThresholds.delete(null as any);
if (uniqueThresholds.size === 0) {
return frame;
}
if (uniqueThresholds.size === 1) {
const threshold = uniqueThresholds.values().next().value;
if (threshold <= 0) {
return frame;
}
const refValues = refField.values;
const frameValues = frame.fields.map((field) => field.values);
const filledFieldValues = nullInsertThreshold(
refValues,
frameValues,
threshold,
refFieldPseudoMin,
refFieldPseudoMax,
insertMode,
thorough
);
if (filledFieldValues === frameValues) {
return frame;
}
return {
...frame,
length: filledFieldValues[0].length,
fields: frame.fields.map((field, i) => ({
...field,
values: filledFieldValues[i],
})),
};
}
// TODO: unique threshold-per-field (via overrides) is unimplemented
// should be done by processing each (refField + thresholdA-field1 + thresholdA-field2...)
// as a separate nullInsertThreshold() dataset, then re-join into single dataset via join()
return frame;
}
function nullInsertThreshold(
refValues: number[],
frameValues: any[][],
threshold: number,
refFieldPseudoMin: number | null = null,
// will insert a trailing null when refFieldPseudoMax > last datapoint + threshold
refFieldPseudoMax: number | null = null,
getInsertValue: InsertMode,
// will insert the value at every missing interval
thorough: boolean
) {
const len = refValues.length;
const refValuesNew: number[] = [];
// Continuously subtract the threshold from the first data point, filling in insert values accordingly
if (refFieldPseudoMin != null && refFieldPseudoMin < refValues[0]) {
let preFillCount = Math.ceil((refValues[0] - refFieldPseudoMin) / threshold);
// this will be 0 or 1 threshold increment left of visible range
let prevSlot = refValues[0] - preFillCount * threshold;
while (prevSlot < refValues[0]) {
// (prevSlot - threshold) is used to simulate the previous 'real' data point, as getInsertValue expects
refValuesNew.push(getInsertValue(prevSlot - threshold, prevSlot, threshold));
prevSlot += threshold;
}
}
// Insert initial value
refValuesNew.push(refValues[0]);
let prevValue: number = refValues[0];
// Fill nulls when a value is greater than the threshold value
for (let i = 1; i < len; i++) {
const curValue = refValues[i];
while (curValue - prevValue > threshold) {
refValuesNew.push(getInsertValue(prevValue, curValue, threshold));
prevValue += threshold;
if (!thorough) {
break;
}
}
refValuesNew.push(curValue);
prevValue = curValue;
}
// At the end of the sequence
if (refFieldPseudoMax != null && refFieldPseudoMax > prevValue) {
while (prevValue + threshold < refFieldPseudoMax) {
refValuesNew.push(getInsertValue(prevValue, refFieldPseudoMax, threshold));
prevValue += threshold;
}
}
const filledLen = refValuesNew.length;
if (filledLen === len) {
return frameValues;
}
const filledFieldValues: any[][] = [];
for (let fieldValues of frameValues) {
let filledValues;
if (fieldValues !== refValues) {
filledValues = Array(filledLen);
for (let i = 0, j = 0; i < filledLen; i++) {
filledValues[i] = refValues[j] === refValuesNew[i] ? fieldValues[j++] : null;
}
} else {
filledValues = refValuesNew;
}
filledFieldValues.push(filledValues);
}
return filledFieldValues;
}
@@ -0,0 +1,27 @@
import { DataFrame } from '../../../types';
export function nullToValue(frame: DataFrame) {
return {
...frame,
fields: frame.fields.map((field) => {
const noValue = +field.config?.noValue!;
if (!Number.isNaN(noValue)) {
const transformedVals = field.values.slice();
for (let i = 0; i < transformedVals.length; i++) {
if (transformedVals[i] === null) {
transformedVals[i] = noValue;
}
}
return {
...field,
values: transformedVals,
};
} else {
return field;
}
}),
};
}
@@ -5,11 +5,10 @@ import {
FieldType,
isLikelyAscendingVector,
sortDataFrame,
applyNullInsertThreshold,
} from '@grafana/data';
import { GraphFieldConfig } from '@grafana/schema';
import { applyNullInsertThreshold } from '../GraphNG/nullInsertThreshold';
/** @internal
* Given a sparkline config returns a DataFrame ready to be turned into Plot data set
**/
+7 -6
View File
@@ -289,13 +289,14 @@ export { UPlotChart } from './uPlot/Plot';
export { PlotLegend } from './uPlot/PlotLegend';
export * from './uPlot/geometries';
export * from './uPlot/plugins';
export { type PlotTooltipInterpolator, type PlotSelection } from './uPlot/types';
export { type PlotTooltipInterpolator, type PlotSelection, FIXED_UNIT } from './uPlot/types';
export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder';
export { GraphNG, type GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG';
export { TimeSeries } from './TimeSeries/TimeSeries';
export { useGraphNGContext } from './GraphNG/hooks';
export { preparePlotFrame, buildScaleKey } from './GraphNG/utils';
export { type GraphNGLegendEvent } from './GraphNG/types';
export * from './PanelChrome/types';
export { Label as BrowserLabel } from './BrowserLabel/Label';
export { PanelContainer } from './PanelContainer/PanelContainer';
export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG';
export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries';
export { useGraphNGContext } from '../graveyard/GraphNG/hooks';
export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils';
export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
@@ -0,0 +1,40 @@
import { FieldConfig, FieldType } from '@grafana/data';
import { AxisPlacement, GraphFieldConfig, ScaleDistribution, ScaleDistributionConfig } from '@grafana/schema';
import { FIXED_UNIT } from './types';
/**
* @internal -- not a public API
*/
export function buildScaleKey(config: FieldConfig<GraphFieldConfig>, fieldType: FieldType) {
const defaultPart = 'na';
const scaleRange = `${config.min !== undefined ? config.min : defaultPart}-${
config.max !== undefined ? config.max : defaultPart
}`;
const scaleSoftRange = `${config.custom?.axisSoftMin !== undefined ? config.custom.axisSoftMin : defaultPart}-${
config.custom?.axisSoftMax !== undefined ? config.custom.axisSoftMax : defaultPart
}`;
const scalePlacement = `${
config.custom?.axisPlacement !== undefined ? config.custom?.axisPlacement : AxisPlacement.Auto
}`;
const scaleUnit = config.unit ?? FIXED_UNIT;
const scaleDistribution = config.custom?.scaleDistribution
? getScaleDistributionPart(config.custom.scaleDistribution)
: ScaleDistribution.Linear;
const scaleLabel = Boolean(config.custom?.axisLabel) ? config.custom!.axisLabel : defaultPart;
return `${scaleUnit}/${scaleRange}/${scaleSoftRange}/${scalePlacement}/${scaleDistribution}/${scaleLabel}/${fieldType}`;
}
function getScaleDistributionPart(config: ScaleDistributionConfig) {
if (config.type === ScaleDistribution.Log) {
return `${config.type}${config.log}`;
}
return config.type;
}
@@ -3,6 +3,11 @@ import uPlot, { Options, AlignedData } from 'uplot';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
/**
* @internal -- not a public API
*/
export const FIXED_UNIT = '__fixed';
export type PlotConfig = Pick<
Options,
'mode' | 'series' | 'scales' | 'axes' | 'cursor' | 'bands' | 'hooks' | 'select' | 'tzDate' | 'padding'
@@ -1,7 +1,7 @@
import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data';
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
import { preparePlotFrame } from '../GraphNG/utils';
import { preparePlotFrame } from '../../../../../public/app/core/components/GraphNG/utils';
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
@@ -5,7 +5,8 @@ import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, Stacki
import { attachDebugger } from '../../utils';
import { createLogger } from '../../utils/logger';
import { buildScaleKey } from '../GraphNG/utils';
import { buildScaleKey } from './internal';
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
@@ -0,0 +1,275 @@
import React, { Component } from 'react';
import { Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import uPlot, { AlignedData } from 'uplot';
import {
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
Field,
FieldMatcherID,
fieldMatchers,
FieldType,
LegacyGraphHoverEvent,
TimeRange,
TimeZone,
} from '@grafana/data';
import { VizLegendOptions } from '@grafana/schema';
import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { VizLayout } from '../../components/VizLayout/VizLayout';
import { UPlotChart } from '../../components/uPlot/Plot';
import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder';
import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder';
import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils';
import { Themeable2 } from '../../types';
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
/**
* @deprecated
* @internal -- not a public API
*/
export type PropDiffFn<T extends any = any> = (prev: T, next: T) => boolean;
/** @deprecated */
export interface GraphNGProps extends Themeable2 {
frames: DataFrame[];
structureRev?: number; // a number that will change when the frames[] structure changes
width: number;
height: number;
timeRange: TimeRange;
timeZone: TimeZone[] | TimeZone;
legend: VizLegendOptions;
fields?: XYFieldMatchers; // default will assume timeseries data
renderers?: Renderers;
tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps;
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
onLegendClick?: (event: GraphNGLegendEvent) => void;
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
propsToDiff?: Array<string | PropDiffFn>;
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
/**
* needed for propsToDiff to re-init the plot & config
* this is a generic approach to plot re-init, without having to specify which panel-level options
* should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in
* similar to structureRev. then we can drop propsToDiff entirely.
*/
options?: Record<string, any>;
}
function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | PropDiffFn> = []) {
for (const propName of propsToDiff) {
if (typeof propName === 'function') {
if (!propName(prevProps, nextProps)) {
return false;
}
} else if (nextProps[propName] !== prevProps[propName]) {
return false;
}
}
return true;
}
/**
* @internal -- not a public API
* @deprecated
*/
export interface GraphNGState {
alignedFrame: DataFrame;
alignedData?: AlignedData;
config?: UPlotConfigBuilder;
}
/**
* "Time as X" core component, expects ascending x
* @deprecated
*/
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
private plotInstance: React.RefObject<uPlot>;
private subscription = new Subscription();
constructor(props: GraphNGProps) {
super(props);
let state = this.prepState(props);
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
this.state = state;
this.plotInstance = React.createRef();
}
getTimeRange = () => this.props.timeRange;
prepState(props: GraphNGProps, withConfig = true) {
let state: GraphNGState = null as any;
const { frames, fields, preparePlotFrame } = props;
const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame;
const alignedFrame = preparePlotFrameFn(
frames,
fields || {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
},
props.timeRange
);
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
if (alignedFrame) {
let config = this.state?.config;
if (withConfig) {
config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
pluginLog('GraphNG', false, 'config prepared', config);
}
state = {
alignedFrame,
config,
};
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
}
return state;
}
handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) {
const time = evt.payload?.point?.time;
const u = this.plotInstance.current;
if (u && time) {
// Try finding left position on time axis
const left = u.valToPos(time, 'x');
let top;
if (left) {
// find midpoint between points at current idx
top = findMidPointYPosition(u, u.posToIdx(left));
}
if (!top || !left) {
return;
}
u.setCursor({
left,
top,
});
}
}
componentDidMount() {
this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext;
this.subscription.add(
eventBus
.getStream(DataHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => {
if (eventBus === evt.origin) {
return;
}
this.handleCursorUpdate(evt);
},
})
);
// Legacy events (from flot graph)
this.subscription.add(
eventBus
.getStream(LegacyGraphHoverEvent)
.pipe(throttleTime(50))
.subscribe({
next: (evt) => this.handleCursorUpdate(evt),
})
);
this.subscription.add(
eventBus
.getStream(DataHoverClearEvent)
.pipe(throttleTime(50))
.subscribe({
next: () => {
const u = this.plotInstance?.current;
// @ts-ignore
if (u && !u.cursor._lock) {
u.setCursor({
left: -10,
top: -10,
});
}
},
})
);
}
componentDidUpdate(prevProps: GraphNGProps) {
const { frames, structureRev, timeZone, propsToDiff } = this.props;
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) {
let newState = this.prepState(this.props, false);
if (newState) {
const shouldReconfig =
this.state.config === undefined ||
timeZone !== prevProps.timeZone ||
structureRev !== prevProps.structureRev ||
!structureRev ||
propsChanged;
if (shouldReconfig) {
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
pluginLog('GraphNG', false, 'config recreated', newState.config);
}
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
this.setState(newState);
}
}
}
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() {
const { width, height, children, renderLegend } = this.props;
const { config, alignedFrame, alignedData } = this.state;
if (!config) {
return null;
}
return (
<VizLayout width={width} height={height} legend={renderLegend(config)}>
{(vizWidth: number, vizHeight: number) => (
<UPlotChart
config={config}
data={alignedData!}
width={vizWidth}
height={vizHeight}
plotRef={(u) => ((this.plotInstance as React.MutableRefObject<uPlot>).current = u)}
>
{children ? children(config, alignedFrame) : null}
</UPlotChart>
)}
</VizLayout>
);
}
}
@@ -0,0 +1,41 @@
import React, { useCallback, useContext } from 'react';
import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data';
import { XYFieldMatchers } from './types';
/** @deprecated */
interface GraphNGContextType {
mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex;
dimFields: XYFieldMatchers;
data: DataFrame;
}
/** @deprecated */
export const GraphNGContext = React.createContext<GraphNGContextType>({} as GraphNGContextType);
/** @deprecated */
export const useGraphNGContext = () => {
const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext<GraphNGContextType>(GraphNGContext);
const getXAxisField = useCallback(() => {
const xFieldMatcher = dimFields.x;
let xField: Field | null = null;
for (let j = 0; j < data.fields.length; j++) {
if (xFieldMatcher(data.fields[j], data, [data])) {
xField = data.fields[j];
break;
}
}
return xField;
}, [data, dimFields]);
return {
dimFields,
mapSeriesIndexToDataFrameFieldIndex,
getXAxisField,
alignedData: data,
};
};
@@ -0,0 +1,334 @@
import { FieldType, createDataFrame } from '@grafana/data';
import { applyNullInsertThreshold } from './nullInsertThreshold';
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function genFrame() {
let fieldCount = 10;
let valueCount = 3000;
let step = 1000;
let skipProb = 0.5;
let skipSteps = [1, 5]; // min, max
let allValues = Array(fieldCount);
allValues[0] = Array(valueCount);
for (let i = 0, curStep = Date.now(); i < valueCount; i++) {
curStep = allValues[0][i] = curStep + step * (Math.random() < skipProb ? randInt(skipSteps[0], skipSteps[1]) : 1);
}
for (let fi = 1; fi < fieldCount; fi++) {
let values = Array(valueCount);
for (let i = 0; i < valueCount; i++) {
values[i] = Math.random() * 100;
}
allValues[fi] = values;
}
return {
length: valueCount,
fields: allValues.map((values, i) => {
return {
name: 'A-' + i,
type: i === 0 ? FieldType.time : FieldType.number,
config: {
interval: i === 0 ? step : null,
},
values: values,
};
}),
};
}
describe('nullInsertThreshold Transformer', () => {
test('should insert nulls at +threshold between adjacent > threshold: 1', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']);
});
test('should insert nulls at +threshold between adjacent > threshold: 2', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 2 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 2 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([5, 7, 9, 11]);
expect(result.fields[1].values).toEqual([4, 6, null, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', null, 'c']);
});
test('should insert nulls at +interval between adjacent > interval: 1', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c']);
});
test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [4, 6, 13] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({
frame: df,
refFieldName: null,
refFieldPseudoMin: -0.5,
refFieldPseudoMax: 13,
});
expect(result.fields[0].values).toEqual([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
expect(result.fields[1].values).toEqual([
null,
null,
null,
null,
null,
4,
null,
6,
null,
null,
null,
null,
null,
null,
8,
]);
expect(result.fields[2].values).toEqual([
null,
null,
null,
null,
null,
'a',
null,
'b',
null,
null,
null,
null,
null,
null,
'c',
]);
});
// this tests that intervals at 24hr but starting not at 12am UTC are not always snapped to 12am UTC
test('should insert leading null at beginning +interval when timeRange.from.valueOf() exceeds threshold 11PM UTC', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{
name: 'Time',
type: FieldType.time,
config: { interval: 86400000 },
values: [1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000],
},
{ name: 'One', type: FieldType.number, values: [0, 1, 2, 3, 4] },
],
});
const result = applyNullInsertThreshold({
frame: df,
refFieldName: null,
refFieldPseudoMin: 1679320395828,
refFieldPseudoMax: 1679815217157,
});
expect(result.fields[0].values).toEqual([
1679266800000, 1679353200000, 1679439600000, 1679526000000, 1679612400000, 1679698800000, 1679785200000,
]);
expect(result.fields[1].values).toEqual([null, null, 0, 1, 2, 3, 4]);
});
test('should insert trailing null at end +interval when timeRange.to.valueOf() exceeds threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 13 });
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
expect(result.fields[1].values).toEqual([4, null, 6, null, null, null, null, null, null, 8, null, null]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, null, null, null, null, null, 'c', null, null]);
// should work for frames with 1 datapoint
const df2 = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'One', type: FieldType.number, values: [1] },
{ name: 'Two', type: FieldType.string, values: ['a'] },
],
});
// Max is 2.5 as opposed to the above 13 otherwise
// we get 12 nulls instead of the additional 1
const result2 = applyNullInsertThreshold({ frame: df2, refFieldName: null, refFieldPseudoMax: 2.5 });
expect(result2.fields[0].values).toEqual([1, 2]);
expect(result2.fields[1].values).toEqual([1, null]);
expect(result2.fields[2].values).toEqual(['a', null]);
});
test('should not insert trailing null at end +interval when timeRange.to.valueOf() equals threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1] },
{ name: 'One', type: FieldType.number, values: [1] },
{ name: 'Two', type: FieldType.string, values: ['a'] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: null, refFieldPseudoMax: 2 });
expect(result.fields[0].values).toEqual([1]);
expect(result.fields[1].values).toEqual([1]);
expect(result.fields[2].values).toEqual(['a']);
});
// TODO: make this work
test.skip('should insert nulls at +threshold (when defined) instead of +interval', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 2 }, values: [5, 7, 11] },
{ name: 'One', type: FieldType.number, config: { custom: { insertNulls: 1 } }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { custom: { insertNulls: 1 } }, values: ['a', 'b', 'c'] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result.fields[0].values).toEqual([5, 6, 7, 8, 11]);
expect(result.fields[1].values).toEqual([4, null, 6, null, 8]);
expect(result.fields[2].values).toEqual(['a', null, 'b', null, 'c']);
});
test('should noop on 0 datapoints', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [] },
{ name: 'Value', type: FieldType.number, values: [] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop on invalid threshold', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, config: { custom: { insertNulls: -1 } }, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop on invalid interval', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: -1 }, values: [1, 2, 4] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop when no missing steps', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df });
expect(result).toBe(df);
});
test('should noop when refFieldName not found', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 5] },
{ name: 'Value', type: FieldType.number, values: [1, 1, 1] },
],
});
const result = applyNullInsertThreshold({ frame: df, refFieldName: 'Time2' });
expect(result).toBe(df);
});
// Leave this test skipped - it should be run manually
test.skip('perf stress test should be <= 10ms', () => {
// 10 fields x 3,000 values with 50% skip (output = 10 fields x 6,000 values)
let bigFrameA = genFrame();
// eslint-disable-next-line no-console
console.time('insertValues-10x3k');
applyNullInsertThreshold({ frame: bigFrameA });
// eslint-disable-next-line no-console
console.timeEnd('insertValues-10x3k');
});
});
@@ -19,6 +19,7 @@ interface NullInsertOptions {
insertMode?: InsertMode;
}
/** @deprecated */
export function applyNullInsertThreshold(opts: NullInsertOptions): DataFrame {
if (opts.frame.length === 0) {
return opts.frame;
@@ -0,0 +1,33 @@
/**
* mutates all nulls -> undefineds in the fieldValues array for value-less refValues ranges below maxThreshold
* refValues is typically a time array and maxThreshold is the allowable distance between in time
* @deprecated
*/
export function nullToUndefThreshold(refValues: number[], fieldValues: any[], maxThreshold: number): any[] {
let prevRef;
let nullIdx;
for (let i = 0; i < fieldValues.length; i++) {
let fieldVal = fieldValues[i];
if (fieldVal == null) {
if (nullIdx == null && prevRef != null) {
nullIdx = i;
}
} else {
if (nullIdx != null) {
if (refValues[i] - (prevRef as number) < maxThreshold) {
while (nullIdx < i) {
fieldValues[nullIdx++] = undefined;
}
}
nullIdx = null;
}
prevRef = refValues[i];
}
}
return fieldValues;
}
@@ -0,0 +1,94 @@
import { FieldType, createDataFrame } from '@grafana/data';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToValue } from './nullToValue';
describe('nullToValue Transformer', () => {
test('should change all nulls to configured zero value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [1, 3, 10] },
{
name: 'One',
type: FieldType.number,
config: { custom: { insertNulls: 1 }, noValue: '0' },
values: [4, 6, 8],
},
{
name: 'Two',
type: FieldType.string,
config: { custom: { insertNulls: 1 }, noValue: '0' },
values: ['a', 'b', 'c'],
},
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, 0, 6, 0, 0, 0, 0, 0, 0, 8]);
expect(result.fields[2].values).toEqual(['a', 0, 'b', 0, 0, 0, 0, 0, 0, 'c']);
});
test('should change all nulls to configured positive value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [5, 7, 11] },
{
name: 'One',
type: FieldType.number,
config: { custom: { insertNulls: 2 }, noValue: '1' },
values: [4, 6, 8],
},
{
name: 'Two',
type: FieldType.string,
config: { custom: { insertNulls: 2 }, noValue: '1' },
values: ['a', 'b', 'c'],
},
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([5, 7, 9, 11]);
expect(result.fields[1].values).toEqual([4, 6, 1, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', 1, 'c']);
});
test('should change all nulls to configured negative value', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 3, 10] },
{ name: 'One', type: FieldType.number, config: { noValue: '-1' }, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, config: { noValue: '-1' }, values: ['a', 'b', 'c'] },
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df }));
expect(result.fields[0].values).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
expect(result.fields[1].values).toEqual([4, -1, 6, -1, -1, -1, -1, -1, -1, 8]);
expect(result.fields[2].values).toEqual(['a', -1, 'b', -1, -1, -1, -1, -1, -1, 'c']);
});
test('should have no effect without nulls', () => {
const df = createDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, config: { interval: 1 }, values: [1, 2, 3] },
{ name: 'One', type: FieldType.number, values: [4, 6, 8] },
{ name: 'Two', type: FieldType.string, values: ['a', 'b', 'c'] },
],
});
const result = nullToValue(applyNullInsertThreshold({ frame: df, refFieldName: null }));
expect(result.fields[0].values).toEqual([1, 2, 3]);
expect(result.fields[1].values).toEqual([4, 6, 8]);
expect(result.fields[2].values).toEqual(['a', 'b', 'c']);
});
});
@@ -1,5 +1,6 @@
import { DataFrame } from '@grafana/data';
/** @deprecated */
export function nullToValue(frame: DataFrame) {
return {
...frame,
@@ -0,0 +1,18 @@
import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data';
import { SeriesVisibilityChangeMode } from '../../components/PanelChrome';
/**
* Event being triggered when the user interact with the Graph legend.
* @deprecated
*/
export interface GraphNGLegendEvent {
fieldIndex: DataFrameFieldIndex;
mode: SeriesVisibilityChangeMode;
}
/** @deprecated */
export interface XYFieldMatchers {
x: FieldMatcher; // first match
y: FieldMatcher;
}
@@ -7,7 +7,8 @@ import {
ScaleDistributionConfig,
} from '@grafana/schema';
import { FIXED_UNIT } from './GraphNG';
import { FIXED_UNIT } from '../../components/uPlot/types';
import { applyNullInsertThreshold } from './nullInsertThreshold';
import { nullToUndefThreshold } from './nullToUndefThreshold';
import { XYFieldMatchers } from './types';
@@ -0,0 +1 @@
Items in this folder are all deprecated and will be removed in the future
@@ -2,11 +2,11 @@ import React, { Component } from 'react';
import { DataFrame, TimeRange } from '@grafana/data';
import { PanelContextRoot } from '../../components/PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend';
import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
import { withTheme2 } from '../../themes/ThemeContext';
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
import { PanelContextRoot } from '../PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '../uPlot/PlotLegend';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { preparePlotConfigBuilder } from './utils';
@@ -61,10 +61,10 @@ for (let i = 0; i < BIN_INCRS.length; i++) {
BIN_INCRS[i] = 2 ** i;
}
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder';
import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills';
import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils';
import { buildScaleKey } from '../GraphNG/utils';
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConfigBuilder';
import { getScaleGradientFn } from '../uPlot/config/gradientFills';
import { getStackingGroups, preparePlotData2 } from '../uPlot/utils';
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
@@ -16,24 +16,16 @@ import {
TimeZone,
} from '@grafana/data';
import { VizLegendOptions } from '@grafana/schema';
import { Themeable2 } from '../../types';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
import { VizLayout } from '../VizLayout/VizLayout';
import { UPlotChart } from '../uPlot/Plot';
import { AxisProps } from '../uPlot/config/UPlotAxisBuilder';
import { Renderers, UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { ScaleProps } from '../uPlot/config/UPlotScaleBuilder';
import { findMidPointYPosition, pluginLog } from '../uPlot/utils';
import { Themeable2, PanelContext, PanelContextRoot, VizLayout } from '@grafana/ui';
import { UPlotChart } from '@grafana/ui/src/components/uPlot/Plot';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { Renderers, UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { findMidPointYPosition, pluginLog } from '@grafana/ui/src/components/uPlot/utils';
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
/**
* @internal -- not a public API
*/
export const FIXED_UNIT = '__fixed';
/**
* @internal -- not a public API
*/
@@ -0,0 +1,245 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
{
"axes": [
{
"filter": undefined,
"font": "12px "Inter", "Helvetica", "Arial", sans-serif",
"gap": 5,
"grid": {
"show": true,
"stroke": "rgba(240, 250, 255, 0.09)",
"width": 1,
},
"incrs": undefined,
"labelGap": 0,
"rotate": undefined,
"scale": "x",
"show": true,
"side": 2,
"size": [Function],
"space": [Function],
"splits": undefined,
"stroke": "rgb(204, 204, 220)",
"ticks": {
"show": true,
"size": 4,
"stroke": "rgba(240, 250, 255, 0.09)",
"width": 1,
},
"timeZone": "utc",
"values": [Function],
},
{
"filter": undefined,
"font": "12px "Inter", "Helvetica", "Arial", sans-serif",
"gap": 5,
"grid": {
"show": true,
"stroke": "rgba(240, 250, 255, 0.09)",
"width": 1,
},
"incrs": undefined,
"labelGap": 0,
"rotate": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"side": 3,
"size": [Function],
"space": [Function],
"splits": undefined,
"stroke": "rgb(204, 204, 220)",
"ticks": {
"show": false,
"size": 4,
"stroke": "rgb(204, 204, 220)",
"width": 1,
},
"timeZone": undefined,
"values": [Function],
},
],
"cursor": {
"dataIdx": [Function],
"drag": {
"setScale": false,
},
"focus": {
"prox": 30,
},
"points": {
"fill": [Function],
"size": [Function],
"stroke": [Function],
"width": [Function],
},
"sync": {
"filters": {
"pub": [Function],
},
"key": "__global_",
"scales": [
"x",
"__fixed/na-na/na-na/auto/linear/na/number",
],
},
},
"focus": {
"alpha": 1,
},
"hooks": {},
"legend": {
"show": false,
},
"mode": 1,
"ms": 1,
"padding": [
[Function],
[Function],
[Function],
[Function],
],
"scales": {
"__fixed/na-na/na-na/auto/linear/na/number": {
"asinh": undefined,
"auto": true,
"dir": 1,
"distr": 1,
"log": undefined,
"ori": 1,
"range": [Function],
"time": undefined,
},
"x": {
"auto": false,
"dir": 1,
"ori": 0,
"range": [Function],
"time": true,
},
},
"select": undefined,
"series": [
{
"value": [Function],
},
{
"dash": [
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": {
"fill": "#ff0000",
"filter": [Function],
"show": true,
"size": undefined,
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"value": [Function],
"width": 2,
},
{
"dash": [
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": {
"fill": "#ff0000",
"filter": [Function],
"show": true,
"size": undefined,
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"value": [Function],
"width": 2,
},
{
"dash": [
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": {
"fill": "#ff0000",
"filter": [Function],
"show": true,
"size": undefined,
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"value": [Function],
"width": 2,
},
{
"dash": [
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": {
"fill": "#ff0000",
"filter": [Function],
"show": true,
"size": undefined,
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"value": [Function],
"width": 2,
},
{
"dash": [
1,
2,
],
"facets": undefined,
"fill": [Function],
"paths": [Function],
"points": {
"fill": "#ff0000",
"filter": [Function],
"show": true,
"size": undefined,
"stroke": "#ff0000",
},
"pxAlign": undefined,
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
"show": true,
"spanGaps": false,
"stroke": "#ff0000",
"value": [Function],
"width": 2,
},
],
"tzDate": [Function],
}
`;
@@ -1,6 +1,5 @@
import { DataFrameFieldIndex, FieldMatcher } from '@grafana/data';
import { SeriesVisibilityChangeMode } from '../PanelChrome';
import { SeriesVisibilityChangeMode } from '@grafana/ui';
/**
* Event being triggered when the user interact with the Graph legend.
@@ -0,0 +1,522 @@
import {
createTheme,
DashboardCursorSync,
DataFrame,
DefaultTimeZone,
EventBusSrv,
FieldColorModeId,
FieldConfig,
FieldMatcherID,
fieldMatchers,
FieldType,
getDefaultTimeRange,
MutableDataFrame,
} from '@grafana/data';
import {
BarAlignment,
GraphDrawStyle,
GraphFieldConfig,
GraphGradientMode,
LineInterpolation,
VisibilityMode,
StackingMode,
} from '@grafana/schema';
import { preparePlotConfigBuilder } from '../TimeSeries/utils';
import { preparePlotFrame } from './utils';
function mockDataFrame() {
const df1 = new MutableDataFrame({
refId: 'A',
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }],
});
const df2 = new MutableDataFrame({
refId: 'B',
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }],
});
const f1Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 1',
color: {
mode: FieldColorModeId.Fixed,
},
decimals: 2,
custom: {
drawStyle: GraphDrawStyle.Line,
gradientMode: GraphGradientMode.Opacity,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
spanNulls: false,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: VisibilityMode.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
},
},
};
const f2Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 2',
color: {
mode: FieldColorModeId.Fixed,
},
decimals: 2,
custom: {
drawStyle: GraphDrawStyle.Bars,
gradientMode: GraphGradientMode.Hue,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: VisibilityMode.Always,
stacking: {
group: 'A',
mode: StackingMode.Normal,
},
},
};
const f3Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 3',
decimals: 2,
color: {
mode: FieldColorModeId.Fixed,
},
custom: {
drawStyle: GraphDrawStyle.Line,
gradientMode: GraphGradientMode.Opacity,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
spanNulls: false,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
},
},
};
const f4Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 4',
decimals: 2,
color: {
mode: FieldColorModeId.Fixed,
},
custom: {
drawStyle: GraphDrawStyle.Bars,
gradientMode: GraphGradientMode.Hue,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.Normal,
},
},
};
const f5Config: FieldConfig<GraphFieldConfig> = {
displayName: 'Metric 4',
decimals: 2,
color: {
mode: FieldColorModeId.Fixed,
},
custom: {
drawStyle: GraphDrawStyle.Bars,
gradientMode: GraphGradientMode.Hue,
lineColor: '#ff0000',
lineWidth: 2,
lineInterpolation: LineInterpolation.Linear,
lineStyle: {
fill: 'dash',
dash: [1, 2],
},
barAlignment: BarAlignment.Before,
fillColor: '#ff0000',
fillOpacity: 0.1,
showPoints: VisibilityMode.Always,
stacking: {
group: 'B',
mode: StackingMode.None,
},
},
};
df1.addField({
name: 'metric1',
type: FieldType.number,
config: f1Config,
});
df2.addField({
name: 'metric2',
type: FieldType.number,
config: f2Config,
});
df2.addField({
name: 'metric3',
type: FieldType.number,
config: f3Config,
});
df2.addField({
name: 'metric4',
type: FieldType.number,
config: f4Config,
});
df2.addField({
name: 'metric5',
type: FieldType.number,
config: f5Config,
});
return preparePlotFrame([df1, df2], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
});
}
jest.mock('@grafana/data', () => ({
...jest.requireActual('@grafana/data'),
DefaultTimeZone: 'utc',
}));
describe('GraphNG utils', () => {
test('preparePlotConfigBuilder', () => {
const frame = mockDataFrame();
const result = preparePlotConfigBuilder({
frame: frame!,
theme: createTheme(),
timeZones: [DefaultTimeZone],
getTimeRange: getDefaultTimeRange,
eventBus: new EventBusSrv(),
sync: () => DashboardCursorSync.Tooltip,
allFrames: [frame!],
}).getConfig();
expect(result).toMatchSnapshot();
});
test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => {
const df1: DataFrame = {
name: 'A',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: [1, 1, 1, 1, 1],
},
],
};
const df2: DataFrame = {
name: 'B',
length: 5,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
},
},
values: [2, 2, 2, 2, 2], // bar series should be appended with nulls
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: [3, 3, 3, 3, 3], // line series should be appended with undefineds
},
],
};
const df3: DataFrame = {
name: 'C',
length: 2,
fields: [
{
name: 'time',
type: FieldType.time,
config: {},
values: [1, 1.1], // should not trip up on smaller deltas of non-bars
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Line,
},
},
values: [4, 4],
},
{
name: 'value',
type: FieldType.number,
config: {
custom: {
drawStyle: GraphDrawStyle.Bars,
hideFrom: {
viz: true, // should ignore hidden bar series
},
},
},
values: [4, 4],
},
],
};
let aligndFrame = preparePlotFrame([df1, df2, df3], {
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
});
expect(aligndFrame).toMatchInlineSnapshot(`
{
"fields": [
{
"config": {},
"name": "time",
"state": {
"nullThresholdApplied": true,
"origin": {
"fieldIndex": 0,
"frameIndex": 0,
},
},
"type": "time",
"values": [
1,
1.1,
2,
4,
6,
30,
40,
50,
90,
100,
101,
102,
],
},
{
"config": {
"custom": {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": {
"name": "A",
},
"name": "value",
"state": {
"origin": {
"fieldIndex": 1,
"frameIndex": 0,
},
},
"type": "number",
"values": [
1,
undefined,
1,
1,
1,
undefined,
undefined,
undefined,
undefined,
1,
null,
null,
],
},
{
"config": {
"custom": {
"drawStyle": "bars",
"spanNulls": -1,
},
},
"labels": {
"name": "B",
},
"name": "value",
"state": {
"origin": {
"fieldIndex": 1,
"frameIndex": 1,
},
},
"type": "number",
"values": [
undefined,
undefined,
undefined,
undefined,
undefined,
2,
2,
2,
2,
2,
null,
null,
],
},
{
"config": {
"custom": {
"drawStyle": "line",
},
},
"labels": {
"name": "B",
},
"name": "value",
"state": {
"origin": {
"fieldIndex": 2,
"frameIndex": 1,
},
},
"type": "number",
"values": [
undefined,
undefined,
undefined,
undefined,
undefined,
3,
3,
3,
3,
3,
undefined,
undefined,
],
},
{
"config": {
"custom": {
"drawStyle": "line",
},
},
"labels": {
"name": "C",
},
"name": "value",
"state": {
"origin": {
"fieldIndex": 1,
"frameIndex": 2,
},
},
"type": "number",
"values": [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
{
"config": {
"custom": {
"drawStyle": "bars",
"hideFrom": {
"viz": true,
},
},
},
"labels": {
"name": "C",
},
"name": "value",
"state": {
"origin": {
"fieldIndex": 2,
"frameIndex": 2,
},
},
"type": "number",
"values": [
4,
4,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
],
},
],
"length": 12,
}
`);
});
});
+140
View File
@@ -0,0 +1,140 @@
import { DataFrame, Field, FieldType, outerJoinDataFrames, TimeRange } from '@grafana/data';
import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold';
import { nullToUndefThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullToUndefThreshold';
import { GraphDrawStyle } from '@grafana/schema';
import { XYFieldMatchers } from './types';
function isVisibleBarField(f: Field) {
return (
f.type === FieldType.number && f.config.custom?.drawStyle === GraphDrawStyle.Bars && !f.config.custom?.hideFrom?.viz
);
}
export function getRefField(frame: DataFrame, refFieldName?: string | null) {
return frame.fields.find((field) => {
// note: getFieldDisplayName() would require full DF[]
return refFieldName != null ? field.name === refFieldName : field.type === FieldType.time;
});
}
// will mutate the DataFrame's fields' values
function applySpanNullsThresholds(frame: DataFrame, refFieldName?: string | null) {
const refField = getRefField(frame, refFieldName);
let refValues = refField?.values;
for (let i = 0; i < frame.fields.length; i++) {
let field = frame.fields[i];
if (field === refField || isVisibleBarField(field)) {
continue;
}
let spanNulls = field.config.custom?.spanNulls;
if (typeof spanNulls === 'number') {
if (spanNulls !== -1 && refValues) {
field.values = nullToUndefThreshold(refValues, field.values, spanNulls);
}
}
}
return frame;
}
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers, timeRange?: TimeRange | null) {
let xField: Field;
loop: for (let frame of frames) {
for (let field of frame.fields) {
if (dimFields.x(field, frame, frames)) {
xField = field;
break loop;
}
}
}
// apply null insertions at interval
frames = frames.map((frame) => {
if (!xField?.state?.nullThresholdApplied) {
return applyNullInsertThreshold({
frame,
refFieldName: xField.name,
refFieldPseudoMin: timeRange?.from.valueOf(),
refFieldPseudoMax: timeRange?.to.valueOf(),
});
} else {
return frame;
}
});
let numBarSeries = 0;
frames.forEach((frame) => {
frame.fields.forEach((f) => {
if (isVisibleBarField(f)) {
// prevent minesweeper-expansion of nulls (gaps) when joining bars
// since bar width is determined from the minimum distance between non-undefined values
// (this strategy will still retain any original pre-join nulls, though)
f.config.custom = {
...f.config.custom,
spanNulls: -1,
};
numBarSeries++;
}
});
});
// to make bar widths of all series uniform (equal to narrowest bar series), find smallest distance between x points
let minXDelta = Infinity;
if (numBarSeries > 1) {
frames.forEach((frame) => {
if (!frame.fields.some(isVisibleBarField)) {
return;
}
const xVals = xField.values;
for (let i = 0; i < xVals.length; i++) {
if (i > 0) {
minXDelta = Math.min(minXDelta, xVals[i] - xVals[i - 1]);
}
}
});
}
let alignedFrame = outerJoinDataFrames({
frames,
joinBy: dimFields.x,
keep: dimFields.y,
keepOriginIndices: true,
});
if (alignedFrame) {
alignedFrame = applySpanNullsThresholds(alignedFrame, xField!.name);
// append 2 null vals at minXDelta to bar series
if (minXDelta !== Infinity) {
alignedFrame.fields.forEach((f, fi) => {
let vals = f.values;
if (fi === 0) {
let lastVal = vals[vals.length - 1];
vals.push(lastVal + minXDelta, lastVal + 2 * minXDelta);
} else if (isVisibleBarField(f)) {
vals.push(null, null);
} else {
vals.push(undefined, undefined);
}
});
alignedFrame.length += 2;
}
return alignedFrame;
}
return null;
}
@@ -0,0 +1,63 @@
import React, { Component } from 'react';
import { DataFrame, TimeRange } from '@grafana/data';
import { PanelContextRoot } from '@grafana/ui/src/components/PanelChrome/PanelContext';
import { hasVisibleLegendSeries, PlotLegend } from '@grafana/ui/src/components/uPlot/PlotLegend';
import { UPlotConfigBuilder } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder';
import { withTheme2 } from '@grafana/ui/src/themes/ThemeContext';
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
import { preparePlotConfigBuilder } from './utils';
const propsToDiff: Array<string | PropDiffFn> = ['legend', 'options', 'theme'];
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
static contextType = PanelContextRoot;
declare context: React.ContextType<typeof PanelContextRoot>;
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
const { eventBus, eventsScope, sync } = this.context;
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
return preparePlotConfigBuilder({
frame: alignedFrame,
theme,
timeZones: Array.isArray(timeZone) ? timeZone : [timeZone],
getTimeRange,
eventBus,
sync,
allFrames,
renderers,
tweakScale,
tweakAxis,
eventsScope,
});
};
renderLegend = (config: UPlotConfigBuilder) => {
const { legend, frames } = this.props;
if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) {
return null;
}
return <PlotLegend data={frames} config={config} {...legend} />;
};
render() {
return (
<GraphNG
{...this.props}
prepConfig={this.prepConfig}
propsToDiff={propsToDiff}
renderLegend={this.renderLegend}
/>
);
}
}
export const TimeSeries = withTheme2(UnthemedTimeSeries);
TimeSeries.displayName = 'TimeSeries';
@@ -0,0 +1,274 @@
import { EventBus, FieldType } from '@grafana/data';
import { getTheme } from '@grafana/ui';
import { preparePlotConfigBuilder } from './utils';
describe('when fill below to option is used', () => {
let eventBus: EventBus;
// eslint-disable-next-line
let renderers: any[];
// eslint-disable-next-line
let tests: any;
beforeEach(() => {
eventBus = {
publish: jest.fn(),
getStream: jest.fn(),
subscribe: jest.fn(),
removeAllListeners: jest.fn(),
newScopedBus: jest.fn(),
};
renderers = [];
tests = [
{
alignedFrame: {
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'Time',
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } },
type: FieldType.time,
},
{
config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 },
values: [1, 2, 3],
name: 'Value',
state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } },
type: FieldType.number,
},
{
config: { displayNameFromDS: 'Test2', min: 0, max: 100 },
values: [4, 5, 6],
name: 'Value',
state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } },
type: FieldType.number,
},
],
length: 3,
},
allFrames: [
{
name: 'Test1',
refId: 'A',
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'Time',
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } },
type: FieldType.time,
},
{
config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 },
values: [1, 2, 3],
name: 'Value',
state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } },
type: FieldType.number,
},
],
length: 2,
},
{
name: 'Test2',
refId: 'B',
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'Time',
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } },
type: FieldType.time,
},
{
config: { displayNameFromDS: 'Test2', min: 0, max: 100 },
values: [1, 2, 3],
name: 'Value',
state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } },
type: FieldType.number,
},
],
length: 2,
},
],
expectedResult: 1,
},
{
alignedFrame: {
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'time',
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } },
type: FieldType.time,
},
{
config: { custom: { fillBelowTo: 'below_value1' } },
values: [1, 2, 3],
name: 'value1',
state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } },
type: FieldType.number,
},
{
config: { custom: { fillBelowTo: 'below_value2' } },
values: [4, 5, 6],
name: 'value2',
state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } },
type: FieldType.number,
},
{
config: {},
values: [4, 5, 6],
name: 'below_value1',
state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } },
type: FieldType.number,
},
{
config: {},
values: [4, 5, 6],
name: 'below_value2',
state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } },
type: FieldType.number,
},
],
length: 5,
},
allFrames: [
{
refId: 'A',
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'time',
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } },
type: FieldType.time,
},
{
config: { custom: { fillBelowTo: 'below_value1' } },
values: [1, 2, 3],
name: 'value1',
state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } },
type: FieldType.number,
},
{
config: { custom: { fillBelowTo: 'below_value2' } },
values: [4, 5, 6],
name: 'value2',
state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } },
type: FieldType.number,
},
],
length: 3,
},
{
refId: 'B',
fields: [
{
config: {},
values: [1667406900000, 1667407170000, 1667407185000],
name: 'time',
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } },
type: FieldType.time,
},
{
config: {},
values: [4, 5, 6],
name: 'below_value1',
state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } },
type: FieldType.number,
},
{
config: {},
values: [4, 5, 6],
name: 'below_value2',
state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } },
type: FieldType.number,
},
],
length: 3,
},
],
expectedResult: 2,
},
];
});
it('should verify if fill below to is set then builder bands are set', () => {
for (const test of tests) {
const builder = preparePlotConfigBuilder({
frame: test.alignedFrame,
//@ts-ignore
theme: getTheme(),
timeZones: ['browser'],
getTimeRange: jest.fn(),
eventBus,
sync: jest.fn(),
allFrames: test.allFrames,
renderers,
});
//@ts-ignore
expect(builder.bands.length).toBe(test.expectedResult);
}
});
it('should verify if fill below to is not set then builder bands are empty', () => {
tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined;
tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined;
tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined;
tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined;
tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined;
tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined;
tests[0].expectedResult = 0;
tests[1].expectedResult = 0;
for (const test of tests) {
const builder = preparePlotConfigBuilder({
frame: test.alignedFrame,
//@ts-ignore
theme: getTheme(),
timeZones: ['browser'],
getTimeRange: jest.fn(),
eventBus,
sync: jest.fn(),
allFrames: test.allFrames,
renderers,
});
//@ts-ignore
expect(builder.bands.length).toBe(test.expectedResult);
}
});
it('should verify if fill below to is set and field name is overriden then builder bands are set', () => {
tests[0].alignedFrame.fields[2].config.displayName = 'newName';
tests[0].alignedFrame.fields[2].state.displayName = 'newName';
tests[0].allFrames[1].fields[1].config.displayName = 'newName';
tests[0].allFrames[1].fields[1].state.displayName = 'newName';
tests[1].alignedFrame.fields[3].config.displayName = 'newName';
tests[1].alignedFrame.fields[3].state.displayName = 'newName';
tests[1].allFrames[1].fields[1].config.displayName = 'newName';
tests[1].allFrames[1].fields[1].state.displayName = 'newName';
for (const test of tests) {
const builder = preparePlotConfigBuilder({
frame: test.alignedFrame,
//@ts-ignore
theme: getTheme(),
timeZones: ['browser'],
getTimeRange: jest.fn(),
eventBus,
sync: jest.fn(),
allFrames: test.allFrames,
renderers,
});
//@ts-ignore
expect(builder.bands.length).toBe(test.expectedResult);
}
});
});
@@ -0,0 +1,669 @@
import { isNumber } from 'lodash';
import uPlot from 'uplot';
import {
DashboardCursorSync,
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
DataHoverPayload,
FieldConfig,
FieldType,
formattedValueToString,
getFieldColorModeForField,
getFieldSeriesColor,
getFieldDisplayName,
getDisplayProcessor,
FieldColorModeId,
DecimalCount,
} from '@grafana/data';
// eslint-disable-next-line import/order
import {
AxisPlacement,
GraphDrawStyle,
GraphFieldConfig,
GraphTresholdsStyleMode,
VisibilityMode,
ScaleDirection,
ScaleOrientation,
StackingMode,
GraphTransform,
AxisColorMode,
GraphGradientMode,
} from '@grafana/schema';
// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks
// see categories.ts is @grafana/data
const IEC_UNITS = new Set([
'bytes',
'bits',
'kbytes',
'mbytes',
'gbytes',
'tbytes',
'pbytes',
'binBps',
'binbps',
'KiBs',
'Kibits',
'MiBs',
'Mibits',
'GiBs',
'Gibits',
'TiBs',
'Tibits',
'PiBs',
'Pibits',
]);
const BIN_INCRS = Array(53);
for (let i = 0; i < BIN_INCRS.length; i++) {
BIN_INCRS[i] = 2 ** i;
}
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui/src/components/uPlot/config/UPlotConfigBuilder';
import { getScaleGradientFn } from '@grafana/ui/src/components/uPlot/config/gradientFills';
import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal';
import { getStackingGroups, preparePlotData2 } from '@grafana/ui/src/components/uPlot/utils';
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line,
showPoints: VisibilityMode.Auto,
axisPlacement: AxisPlacement.Auto,
};
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
sync?: () => DashboardCursorSync;
}> = ({
frame,
theme,
timeZones,
getTimeRange,
eventBus,
sync,
allFrames,
renderers,
tweakScale = (opts) => opts,
tweakAxis = (opts) => opts,
eventsScope = '__global_',
}) => {
const builder = new UPlotConfigBuilder(timeZones[0]);
let alignedFrame: DataFrame;
builder.setPrepData((frames) => {
// cache alignedFrame
alignedFrame = frames[0];
return preparePlotData2(frames[0], builder.getStackingGroups());
});
// X is the first field in the aligned frame
const xField = frame.fields[0];
if (!xField) {
return builder; // empty frame with no options
}
const xScaleKey = 'x';
let xScaleUnit = '_x';
let yScaleKey = '';
const xFieldAxisPlacement =
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden;
const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden;
if (xField.type === FieldType.time) {
xScaleUnit = 'time';
builder.addScale({
scaleKey: xScaleKey,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
isTime: true,
range: () => {
const r = getTimeRange();
return [r.from.valueOf(), r.to.valueOf()];
},
});
// filters first 2 ticks to make space for timezone labels
const filterTicks: uPlot.Axis.Filter | undefined =
timeZones.length > 1
? (u, splits) => {
return splits.map((v, i) => (i < 2 ? null : v));
}
: undefined;
for (let i = 0; i < timeZones.length; i++) {
const timeZone = timeZones[i];
builder.addAxis({
scaleKey: xScaleKey,
isTime: true,
placement: xFieldAxisPlacement,
show: xFieldAxisShow,
label: xField.config.custom?.axisLabel,
timeZone,
theme,
grid: { show: i === 0 && xField.config.custom?.axisGridShow },
filter: filterTicks,
});
}
// render timezone labels
if (timeZones.length > 1) {
builder.addHook('drawAxes', (u: uPlot) => {
u.ctx.save();
u.ctx.fillStyle = theme.colors.text.primary;
u.ctx.textAlign = 'left';
u.ctx.textBaseline = 'bottom';
let i = 0;
u.axes.forEach((a) => {
if (a.side === 2) {
//@ts-ignore
let cssBaseline: number = a._pos + a._size;
u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio);
i++;
}
});
u.ctx.restore();
});
}
} else {
// Not time!
if (xField.config.unit) {
xScaleUnit = xField.config.unit;
}
builder.addScale({
scaleKey: xScaleKey,
orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right,
range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax],
});
builder.addAxis({
scaleKey: xScaleKey,
placement: xFieldAxisPlacement,
show: xFieldAxisShow,
label: xField.config.custom?.axisLabel,
theme,
grid: { show: xField.config.custom?.axisGridShow },
formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
});
}
let customRenderedFields =
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
let indexByName: Map<string, number> | undefined;
for (let i = 1; i < frame.fields.length; i++) {
const field = frame.fields[i];
const config: FieldConfig<GraphFieldConfig> = {
...field.config,
custom: {
...defaultConfig,
...field.config.custom,
},
};
const customConfig: GraphFieldConfig = config.custom!;
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
continue;
}
let fmt = field.display ?? defaultFormatter;
if (field.config.custom?.stacking?.mode === StackingMode.Percent) {
fmt = getDisplayProcessor({
field: {
...field,
config: {
...field.config,
unit: 'percentunit',
},
},
theme,
});
}
const scaleKey = buildScaleKey(config, field.type);
const colorMode = getFieldColorModeForField(field);
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
// The builder will manage unique scaleKeys and combine where appropriate
builder.addScale(
tweakScale(
{
scaleKey,
orientation: ScaleOrientation.Vertical,
direction: ScaleDirection.Up,
distribution: customConfig.scaleDistribution?.type,
log: customConfig.scaleDistribution?.log,
linearThreshold: customConfig.scaleDistribution?.linearThreshold,
min: field.config.min,
max: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
centeredZero: customConfig.axisCenteredZero,
range:
customConfig.stacking?.mode === StackingMode.Percent
? (u: uPlot, dataMin: number, dataMax: number) => {
dataMin = dataMin < 0 ? -1 : 0;
dataMax = dataMax > 0 ? 1 : 0;
return [dataMin, dataMax];
}
: field.type === FieldType.enum
? (u: uPlot, dataMin: number, dataMax: number) => {
// this is the exhaustive enum (stable)
let len = field.config.type!.enum!.text!.length;
return [-1, len];
// these are only values that are present
// return [dataMin - 1, dataMax + 1]
}
: undefined,
decimals: field.config.decimals,
},
field
)
);
if (!yScaleKey) {
yScaleKey = scaleKey;
}
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
let axisColor: uPlot.Axis.Stroke | undefined;
if (customConfig.axisColorMode === AxisColorMode.Series) {
if (
colorMode.isByValue &&
field.config.custom?.gradientMode === GraphGradientMode.Scheme &&
colorMode.id === FieldColorModeId.Thresholds
) {
axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds);
} else {
axisColor = seriesColor;
}
}
const axisDisplayOptions = {
border: {
show: customConfig.axisBorderShow || false,
width: 1 / devicePixelRatio,
stroke: axisColor || theme.colors.text.primary,
},
ticks: {
show: customConfig.axisBorderShow || false,
stroke: axisColor || theme.colors.text.primary,
},
color: axisColor || theme.colors.text.primary,
};
let incrs: uPlot.Axis.Incrs | undefined;
// TODO: these will be dynamic with frame updates, so need to accept getYTickLabels()
let values: uPlot.Axis.Values | undefined;
let splits: uPlot.Axis.Splits | undefined;
if (IEC_UNITS.has(config.unit!)) {
incrs = BIN_INCRS;
} else if (field.type === FieldType.enum) {
let text = field.config.type!.enum!.text!;
splits = text.map((v: string, i: number) => i);
values = text;
}
builder.addAxis(
tweakAxis(
{
scaleKey,
label: customConfig.axisLabel,
size: customConfig.axisWidth,
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)),
theme,
grid: { show: customConfig.axisGridShow },
decimals: field.config.decimals,
distr: customConfig.scaleDistribution?.type,
splits,
values,
incrs,
...axisDisplayOptions,
},
field
)
);
}
const showPoints =
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
let pointsFilter: uPlot.Series.Points.Filter = () => null;
if (customConfig.spanNulls !== true) {
pointsFilter = (u, seriesIdx, show, gaps) => {
let filtered = [];
let series = u.series[seriesIdx];
if (!show && gaps && gaps.length) {
const [firstIdx, lastIdx] = series.idxs!;
const xData = u.data[0];
const yData = u.data[seriesIdx];
const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true));
const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true));
if (gaps[0][0] === firstPos) {
filtered.push(firstIdx);
}
// show single points between consecutive gaps that share end/start
for (let i = 0; i < gaps.length; i++) {
let thisGap = gaps[i];
let nextGap = gaps[i + 1];
if (nextGap && thisGap[1] === nextGap[0]) {
// approx when data density is > 1pt/px, since gap start/end pixels are rounded
let approxIdx = u.posToIdx(thisGap[1], true);
if (yData[approxIdx] == null) {
// scan left/right alternating to find closest index with non-null value
for (let j = 1; j < 100; j++) {
if (yData[approxIdx + j] != null) {
approxIdx += j;
break;
}
if (yData[approxIdx - j] != null) {
approxIdx -= j;
break;
}
}
}
filtered.push(approxIdx);
}
}
if (gaps[gaps.length - 1][1] === lastPos) {
filtered.push(lastIdx);
}
}
return filtered.length ? filtered : null;
};
}
let { fillOpacity } = customConfig;
let pathBuilder: uPlot.Series.PathBuilder | null = null;
let pointsBuilder: uPlot.Series.Points.Show | null = null;
if (field.state?.origin) {
if (!indexByName) {
indexByName = getNamesToFieldIndex(frame, allFrames);
}
const originFrame = allFrames[field.state.origin.frameIndex];
const originField = originFrame?.fields[field.state.origin.fieldIndex];
const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames);
// disable default renderers
if (customRenderedFields.indexOf(dispName) >= 0) {
pathBuilder = () => null;
pointsBuilder = () => undefined;
} else if (customConfig.transform === GraphTransform.Constant) {
// patch some monkeys!
const defaultBuilder = uPlot.paths!.linear!();
pathBuilder = (u, seriesIdx) => {
//eslint-disable-next-line
const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types
// the data we want the line renderer to pull is x at each plot edge with paired flat y values
const r = getTimeRange();
let xData = [r.from.valueOf(), r.to.valueOf()];
let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null);
let yData = [firstY, firstY];
let fauxData = _data.slice();
fauxData[0] = xData;
fauxData[seriesIdx] = yData;
//eslint-disable-next-line
return defaultBuilder(
{
...u,
_data: fauxData,
} as any,
seriesIdx,
0,
1
);
};
}
if (customConfig.fillBelowTo) {
const fillBelowToField = frame.fields.find(
(f) =>
customConfig.fillBelowTo === f.name ||
customConfig.fillBelowTo === f.config?.displayNameFromDS ||
customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames)
);
const fillBelowDispName = fillBelowToField
? getFieldDisplayName(fillBelowToField, frame, allFrames)
: customConfig.fillBelowTo;
const t = indexByName.get(dispName);
const b = indexByName.get(fillBelowDispName);
if (isNumber(b) && isNumber(t)) {
builder.addBand({
series: [t, b],
fill: undefined, // using null will have the band use fill options from `t`
});
if (!fillOpacity) {
fillOpacity = 35; // default from flot
}
} else {
fillOpacity = 0;
}
}
}
let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined;
if (colorMode.id === FieldColorModeId.Thresholds) {
dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color;
}
builder.addSeries({
pathBuilder,
pointsBuilder,
scaleKey,
showPoints,
pointsFilter,
colorMode,
fillOpacity,
theme,
dynamicSeriesColor,
drawStyle: customConfig.drawStyle!,
lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle,
barAlignment: customConfig.barAlignment,
barWidthFactor: customConfig.barWidthFactor,
barMaxWidth: customConfig.barMaxWidth,
pointSize: customConfig.pointSize,
spanNulls: customConfig.spanNulls || false,
show: !customConfig.hideFrom?.viz,
gradientMode: customConfig.gradientMode,
thresholds: config.thresholds,
hardMin: field.config.min,
hardMax: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
dataFrameFieldIndex: field.state?.origin,
});
// Render thresholds in graph
if (customConfig.thresholdsStyle && config.thresholds) {
const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphTresholdsStyleMode.Off;
if (thresholdDisplay !== GraphTresholdsStyleMode.Off) {
builder.addThresholds({
config: customConfig.thresholdsStyle,
thresholds: config.thresholds,
scaleKey,
theme,
hardMin: field.config.min,
hardMax: field.config.max,
softMin: customConfig.axisSoftMin,
softMax: customConfig.axisSoftMax,
});
}
}
}
let stackingGroups = getStackingGroups(frame);
builder.setStackingGroups(stackingGroups);
// hook up custom/composite renderers
renderers?.forEach((r) => {
if (!indexByName) {
indexByName = getNamesToFieldIndex(frame, allFrames);
}
let fieldIndices: Record<string, number> = {};
for (let key in r.fieldMap) {
let dispName = r.fieldMap[key];
fieldIndices[key] = indexByName.get(dispName)!;
}
r.init(builder, fieldIndices);
});
builder.scaleKeys = [xScaleKey, yScaleKey];
// if hovered value is null, how far we may scan left/right to hover nearest non-null
const hoverProximityPx = 15;
let cursor: Partial<uPlot.Cursor> = {
// this scans left and right from cursor position to find nearest data index with value != null
// TODO: do we want to only scan past undefined values, but halt at explicit null values?
dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => {
let seriesData = self.data[seriesIdx];
if (seriesData[hoveredIdx] == null) {
let nonNullLft = null,
nonNullRgt = null,
i;
i = hoveredIdx;
while (nonNullLft == null && i-- > 0) {
if (seriesData[i] != null) {
nonNullLft = i;
}
}
i = hoveredIdx;
while (nonNullRgt == null && i++ < seriesData.length) {
if (seriesData[i] != null) {
nonNullRgt = i;
}
}
let xVals = self.data[0];
let curPos = self.valToPos(cursorXVal, 'x');
let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x');
let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x');
let lftDelta = curPos - lftPos;
let rgtDelta = rgtPos - curPos;
if (lftDelta <= rgtDelta) {
if (lftDelta <= hoverProximityPx) {
hoveredIdx = nonNullLft!;
}
} else {
if (rgtDelta <= hoverProximityPx) {
hoveredIdx = nonNullRgt!;
}
}
}
return hoveredIdx;
},
};
if (sync && sync() !== DashboardCursorSync.Off) {
const payload: DataHoverPayload = {
point: {
[xScaleKey]: null,
[yScaleKey]: null,
},
data: frame,
};
const hoverEvent = new DataHoverEvent(payload);
cursor.sync = {
key: eventsScope,
filters: {
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
if (sync && sync() === DashboardCursorSync.Off) {
return false;
}
payload.rowIndex = dataIdx;
if (x < 0 && y < 0) {
payload.point[xScaleUnit] = null;
payload.point[yScaleKey] = null;
eventBus.publish(new DataHoverClearEvent());
} else {
// convert the points
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
payload.point[yScaleKey] = src.posToVal(y, yScaleKey);
payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip
eventBus.publish(hoverEvent);
hoverEvent.payload.down = undefined;
}
return true;
},
},
scales: [xScaleKey, yScaleKey],
// match: [() => true, (a, b) => a === b],
};
}
builder.setSync();
builder.setCursor(cursor);
return builder;
};
export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
const originNames = new Map<string, number>();
frame.fields.forEach((field, i) => {
const origin = field.state?.origin;
if (origin) {
const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex];
if (origField) {
originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i);
}
}
});
return originNames;
}
@@ -2,16 +2,9 @@ import React from 'react';
import { DataFrame, FALLBACK_COLOR, FieldType, TimeRange } from '@grafana/data';
import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema';
import {
PanelContext,
PanelContextRoot,
GraphNG,
GraphNGProps,
UPlotConfigBuilder,
VizLayout,
VizLegend,
VizLegendItem,
} from '@grafana/ui';
import { PanelContext, PanelContextRoot, UPlotConfigBuilder, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui';
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
import { preparePlotConfigBuilder, TimelineMode } from './utils';
@@ -3,7 +3,7 @@ import uPlot, { Series } from 'uplot';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { alpha } from '@grafana/data/src/themes/colorManipulator';
import { VisibilityMode, TimelineValueAlignment } from '@grafana/schema';
import { FIXED_UNIT } from '@grafana/ui/src/components/GraphNG/GraphNG';
import { FIXED_UNIT } from '@grafana/ui';
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
import { pointWithin, Quadtree, Rect } from 'app/plugins/panel/barchart/quadtree';
import { FieldConfig as StateTimeLineFieldConfig } from 'app/plugins/panel/state-timeline/panelcfg.gen';
@@ -23,6 +23,8 @@ import {
TimeRange,
} from '@grafana/data';
import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold';
import { nullToValue } from '@grafana/data/src/transformations/transformers/nulls/nullToValue';
import {
VizLegendOptions,
AxisPlacement,
@@ -40,8 +42,6 @@ import {
UPlotConfigPrepFn,
VizLegendItem,
} from '@grafana/ui';
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types';
import { preparePlotData2, getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
@@ -16,8 +16,6 @@ import { PanelDataErrorView } from '@grafana/runtime';
import { SortOrder } from '@grafana/schema';
import {
GraphGradientMode,
GraphNG,
GraphNGProps,
measureText,
PlotLegend,
Portal,
@@ -31,9 +29,9 @@ import {
VizLegend,
VizTooltipContainer,
} from '@grafana/ui';
import { PropDiffFn } from '@grafana/ui/src/components/GraphNG/GraphNG';
import { HoverEvent, addTooltipSupport } from '@grafana/ui/src/components/uPlot/config/addTooltipSupport';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { GraphNG, GraphNGProps, PropDiffFn } from 'app/core/components/GraphNG/GraphNG';
import { getFieldLegendItem } from 'app/core/components/TimelineChart/utils';
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
@@ -7,9 +7,10 @@ import uPlot from 'uplot';
import { Field, getDisplayProcessor, getLinksSupplier, PanelProps } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import { TimeSeries, TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
import { TooltipPlugin, UPlotConfigBuilder, usePanelContext, useTheme2, ZoomPlugin } from '@grafana/ui';
import { AxisProps } from '@grafana/ui/src/components/uPlot/config/UPlotAxisBuilder';
import { ScaleProps } from '@grafana/ui/src/components/uPlot/config/UPlotScaleBuilder';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
import { AnnotationEditorPlugin } from '../timeseries/plugins/AnnotationEditorPlugin';
@@ -1,8 +1,8 @@
import { find } from 'lodash';
import { DataFrame, dateTime, Field, FieldType, getFieldDisplayName, getTimeField, TimeRange } from '@grafana/data';
import { applyNullInsertThreshold } from '@grafana/data/src/transformations/transformers/nulls/nullInsertThreshold';
import { colors } from '@grafana/ui';
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
@@ -3,7 +3,8 @@ import React, { useMemo } from 'react';
import { PanelProps, DataFrameType } from '@grafana/data';
import { PanelDataErrorView } from '@grafana/runtime';
import { TooltipDisplayMode } from '@grafana/schema';
import { KeyboardPlugin, TimeSeries, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
import { KeyboardPlugin, TooltipPlugin, usePanelContext, ZoomPlugin } from '@grafana/ui';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
import { Options } from './panelcfg.gen';
@@ -9,7 +9,7 @@ import {
TIME_SERIES_VALUE_FIELD_NAME,
TimeZone,
} from '@grafana/data';
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui';
import { FIXED_UNIT, EventsCanvas, UPlotConfigBuilder } from '@grafana/ui';
import { ExemplarMarker } from './ExemplarMarker';
@@ -2,7 +2,8 @@ import React, { useState, useLayoutEffect, useMemo, useRef } from 'react';
import uPlot from 'uplot';
import { FieldConfigSource, ThresholdsConfig, getValueFormat, FieldType } from '@grafana/data';
import { UPlotConfigBuilder, buildScaleKey } from '@grafana/ui';
import { UPlotConfigBuilder } from '@grafana/ui';
import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal';
import { ThresholdDragHandle } from './ThresholdDragHandle';
+3 -3
View File
@@ -13,10 +13,10 @@ import {
TimeRange,
} 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 } 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';
import { buildScaleKey } from '@grafana/ui/src/components/uPlot/internal';
type ScaleKey = string;
@@ -3,15 +3,10 @@ import React, { useMemo } from 'react';
import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, PanelProps, TimeRange } from '@grafana/data';
import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames';
import { config, PanelDataErrorView } from '@grafana/runtime';
import {
KeyboardPlugin,
preparePlotFrame,
TimeSeries,
TooltipDisplayMode,
TooltipPlugin,
usePanelContext,
} from '@grafana/ui';
import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types';
import { KeyboardPlugin, TooltipDisplayMode, TooltipPlugin, usePanelContext } from '@grafana/ui';
import { XYFieldMatchers } from 'app/core/components/GraphNG/types';
import { preparePlotFrame } from 'app/core/components/GraphNG/utils';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { findFieldIndex } from 'app/features/dimensions';
import { prepareGraphableFields, regenerateLinksSupplier } from '../timeseries/utils';
+1 -1
View File
@@ -1,5 +1,5 @@
import { DataFrame, Field, FieldMatcher, FieldType, getFieldDisplayName } from '@grafana/data';
import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types';
import { XYFieldMatchers } from 'app/core/components/GraphNG/types';
import { XYDimensionConfig } from './panelcfg.gen';