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:
+66
-33
@@ -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
@@ -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
|
||||
**/
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
+1
@@ -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
@@ -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;
|
||||
}
|
||||
+2
-1
@@ -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
|
||||
+3
-3
@@ -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';
|
||||
|
||||
+3
-3
@@ -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));
|
||||
|
||||
+6
-14
@@ -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
-2
@@ -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,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user