mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: refactor (#33348)
This commit is contained in:
parent
545d930a13
commit
a5c13feb61
@ -1,4 +1,4 @@
|
||||
import uPlot, { Axis, Series, Cursor, BBox } from 'uplot';
|
||||
import uPlot, { Axis, Series, Cursor, Select } from 'uplot';
|
||||
import { Quadtree, Rect, pointWithin } from './quadtree';
|
||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||
|
||||
@ -212,7 +212,7 @@ export function getConfig(opts: BarsOptions) {
|
||||
|
||||
// disable selection
|
||||
// uPlot types do not export the Select interface prior to 1.6.4
|
||||
const select: Partial<BBox> = {
|
||||
const select: Partial<Select> = {
|
||||
show: false,
|
||||
};
|
||||
|
||||
|
@ -14,6 +14,7 @@ import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarCha
|
||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config';
|
||||
import { BarsOptions, getConfig } from './bars';
|
||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||
import { Select } from 'uplot';
|
||||
|
||||
/** @alpha */
|
||||
export function preparePlotConfigBuilder(
|
||||
@ -68,7 +69,7 @@ export function preparePlotConfigBuilder(
|
||||
builder.addHook('setCursor', config.setCursor);
|
||||
|
||||
builder.setCursor(config.cursor);
|
||||
builder.setSelect(config.select);
|
||||
builder.setSelect(config.select as Select);
|
||||
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { FieldColorModeId, toDataFrame, dateTime } from '@grafana/data';
|
||||
import React from 'react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { GraphNG, GraphNGProps } from './GraphNG';
|
||||
import { GraphNGProps } from './GraphNG';
|
||||
import { LegendDisplayMode, LegendPlacement } from '../VizLegend/models.gen';
|
||||
import { prepDataForStorybook } from '../../utils/storybook/data';
|
||||
import { useTheme2 } from '../../themes';
|
||||
import { Story } from '@storybook/react';
|
||||
import { TimeSeries } from '../TimeSeries/TimeSeries';
|
||||
|
||||
export default {
|
||||
title: 'Visualizations/GraphNG',
|
||||
component: GraphNG,
|
||||
component: TimeSeries,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
knobs: {
|
||||
@ -51,9 +52,9 @@ export const Lines: Story<StoryProps> = ({ placement, unit, legendDisplayMode, .
|
||||
const data = prepDataForStorybook([seriesA], theme);
|
||||
|
||||
return (
|
||||
<GraphNG
|
||||
<TimeSeries
|
||||
{...args}
|
||||
data={data}
|
||||
frames={data}
|
||||
legend={{
|
||||
displayMode:
|
||||
legendDisplayMode === 'hidden'
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React from 'react';
|
||||
import { AlignedData } from 'uplot';
|
||||
import { DataFrame, FieldMatcherID, fieldMatchers, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { DataFrame, FieldMatcherID, fieldMatchers, FieldType, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { Themeable2 } from '../../types';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||
import { pluginLog, preparePlotData } from '../uPlot/utils';
|
||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { preparePlotData } from '../uPlot/utils';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { LegendDisplayMode, VizLegendOptions } from '../VizLegend/models.gen';
|
||||
import { VizLegendOptions } from '../VizLegend/models.gen';
|
||||
import { VizLayout } from '../VizLayout/VizLayout';
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
/**
|
||||
* @internal -- not a public API
|
||||
@ -20,141 +18,124 @@ export const FIXED_UNIT = '__fixed';
|
||||
export interface GraphNGProps extends Themeable2 {
|
||||
width: number;
|
||||
height: number;
|
||||
data: DataFrame[];
|
||||
structureRev?: number; // a number that will change when the data[] structure changes
|
||||
frames: DataFrame[];
|
||||
structureRev?: number; // a number that will change when the frames[] structure changes
|
||||
timeRange: TimeRange;
|
||||
legend: VizLegendOptions;
|
||||
timeZone: TimeZone;
|
||||
legend: VizLegendOptions;
|
||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
||||
children?: (builder: UPlotConfigBuilder, alignedDataFrame: DataFrame) => React.ReactNode;
|
||||
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
||||
|
||||
prepConfig: (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
||||
propsToDiff?: string[];
|
||||
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement;
|
||||
}
|
||||
|
||||
function sameProps(prevProps: any, nextProps: any, propsToDiff: string[] = []) {
|
||||
for (const propName of propsToDiff) {
|
||||
if (nextProps[propName] !== prevProps[propName]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal -- not a public API
|
||||
*/
|
||||
export interface GraphNGState {
|
||||
alignedDataFrame: DataFrame;
|
||||
data: AlignedData;
|
||||
alignedFrame: DataFrame;
|
||||
alignedData: AlignedData;
|
||||
config?: UPlotConfigBuilder;
|
||||
}
|
||||
|
||||
class UnthemedGraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
/**
|
||||
* "Time as X" core component, expectes ascending x
|
||||
*/
|
||||
export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
constructor(props: GraphNGProps) {
|
||||
super(props);
|
||||
this.state = this.prepState(props);
|
||||
}
|
||||
|
||||
pluginLog('GraphNG', false, 'constructor, data aligment');
|
||||
const alignedData = preparePlotFrame(props.data, {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
});
|
||||
getTimeRange = () => this.props.timeRange;
|
||||
|
||||
if (!alignedData) {
|
||||
return;
|
||||
prepState(props: GraphNGProps, withConfig = true) {
|
||||
let state: GraphNGState = null as any;
|
||||
|
||||
const { frames, fields } = props;
|
||||
|
||||
const alignedFrame = preparePlotFrame(
|
||||
frames,
|
||||
fields || {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
}
|
||||
);
|
||||
|
||||
if (alignedFrame) {
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: preparePlotData(alignedFrame, [FieldType.number]),
|
||||
};
|
||||
|
||||
if (withConfig) {
|
||||
state.config = props.prepConfig(alignedFrame, this.getTimeRange);
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
alignedDataFrame: alignedData,
|
||||
data: preparePlotData(alignedData),
|
||||
config: preparePlotConfigBuilder(alignedData, props.theme, this.getTimeRange, this.getTimeZone),
|
||||
};
|
||||
return state;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: GraphNGProps) {
|
||||
const { theme, structureRev, data } = this.props;
|
||||
let shouldConfigUpdate = false;
|
||||
let stateUpdate = {} as GraphNGState;
|
||||
const { frames, structureRev, timeZone, propsToDiff } = this.props;
|
||||
|
||||
if (this.state.config === undefined || this.props.timeZone !== prevProps.timeZone) {
|
||||
shouldConfigUpdate = true;
|
||||
}
|
||||
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
|
||||
|
||||
if (data !== prevProps.data) {
|
||||
pluginLog('GraphNG', false, 'data changed');
|
||||
const hasStructureChanged = structureRev !== prevProps.structureRev || !structureRev;
|
||||
if (frames !== prevProps.frames || propsChanged) {
|
||||
let newState = this.prepState(this.props, false);
|
||||
|
||||
if (hasStructureChanged) {
|
||||
pluginLog('GraphNG', false, 'schema changed');
|
||||
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.getTimeRange);
|
||||
}
|
||||
}
|
||||
|
||||
pluginLog('GraphNG', false, 'componentDidUpdate, data aligment');
|
||||
const alignedData = preparePlotFrame(data, {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
});
|
||||
|
||||
if (!alignedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateUpdate = {
|
||||
alignedDataFrame: alignedData,
|
||||
data: preparePlotData(alignedData),
|
||||
};
|
||||
|
||||
if (shouldConfigUpdate || hasStructureChanged) {
|
||||
pluginLog('GraphNG', false, 'updating config');
|
||||
const builder = preparePlotConfigBuilder(alignedData, theme, this.getTimeRange, this.getTimeZone);
|
||||
stateUpdate = { ...stateUpdate, config: builder };
|
||||
}
|
||||
newState && this.setState(newState);
|
||||
}
|
||||
|
||||
if (Object.keys(stateUpdate).length > 0) {
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
getTimeRange = () => {
|
||||
return this.props.timeRange;
|
||||
};
|
||||
|
||||
getTimeZone = () => {
|
||||
return this.props.timeZone;
|
||||
};
|
||||
|
||||
renderLegend() {
|
||||
const { legend, onLegendClick, data } = this.props;
|
||||
const { config } = this.state;
|
||||
|
||||
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<PlotLegend
|
||||
data={data}
|
||||
config={config}
|
||||
onLegendClick={onLegendClick}
|
||||
maxHeight="35%"
|
||||
maxWidth="60%"
|
||||
{...legend}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height, children, timeRange } = this.props;
|
||||
const { config, alignedDataFrame } = this.state;
|
||||
const { width, height, children, timeRange, renderLegend } = this.props;
|
||||
const { config, alignedFrame } = this.state;
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VizLayout width={width} height={height} legend={this.renderLegend()}>
|
||||
<VizLayout width={width} height={height} legend={renderLegend(config)}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
config={this.state.config!}
|
||||
data={this.state.data}
|
||||
data={this.state.alignedData}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
timeRange={timeRange}
|
||||
>
|
||||
{children ? children(config, alignedDataFrame) : null}
|
||||
{children ? children(config, alignedFrame) : null}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const GraphNG = withTheme2(UnthemedGraphNG);
|
||||
GraphNG.displayName = 'GraphNG';
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { preparePlotConfigBuilder } from '../TimeSeries/utils';
|
||||
import {
|
||||
createTheme,
|
||||
DefaultTimeZone,
|
||||
@ -188,12 +189,12 @@ jest.mock('@grafana/data', () => ({
|
||||
describe('GraphNG utils', () => {
|
||||
test('preparePlotConfigBuilder', () => {
|
||||
const frame = mockDataFrame();
|
||||
const result = preparePlotConfigBuilder(
|
||||
frame!,
|
||||
createTheme(),
|
||||
getDefaultTimeRange,
|
||||
() => DefaultTimeZone
|
||||
).getConfig();
|
||||
const result = preparePlotConfigBuilder({
|
||||
frame: frame!,
|
||||
theme: createTheme(),
|
||||
timeZone: DefaultTimeZone,
|
||||
getTimeRange: getDefaultTimeRange,
|
||||
}).getConfig();
|
||||
expect(result).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
@ -1,41 +1,22 @@
|
||||
import React from 'react';
|
||||
import { isNumber } from 'lodash';
|
||||
import { GraphNGLegendEventMode, XYFieldMatchers } from './types';
|
||||
import {
|
||||
ArrayVector,
|
||||
DataFrame,
|
||||
FieldConfig,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldColorModeForField,
|
||||
getFieldDisplayName,
|
||||
getFieldSeriesColor,
|
||||
GrafanaTheme2,
|
||||
outerJoinDataFrames,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
} from '@grafana/data';
|
||||
import { nullToUndefThreshold } from './nullToUndefThreshold';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { FIXED_UNIT } from './GraphNG';
|
||||
import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
GraphTresholdsStyleMode,
|
||||
PointVisibility,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '../uPlot/config';
|
||||
import { collectStackingGroups } from '../uPlot/utils';
|
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
drawStyle: DrawStyle.Line,
|
||||
showPoints: PointVisibility.Auto,
|
||||
axisPlacement: AxisPlacement.Auto,
|
||||
};
|
||||
export interface PrepConfigOpts {
|
||||
frame: DataFrame;
|
||||
theme: GrafanaTheme2;
|
||||
timeZone: TimeZone;
|
||||
getTimeRange: () => TimeRange;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export function mapMouseEventToMode(event: React.MouseEvent): GraphNGLegendEventMode {
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
@ -72,192 +53,10 @@ function applySpanNullsThresholds(frames: DataFrame[]) {
|
||||
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) {
|
||||
applySpanNullsThresholds(frames);
|
||||
|
||||
let joined = outerJoinDataFrames({
|
||||
return outerJoinDataFrames({
|
||||
frames: frames,
|
||||
joinBy: dimFields.x,
|
||||
keep: dimFields.y,
|
||||
keepOriginIndices: true,
|
||||
});
|
||||
|
||||
return joined;
|
||||
}
|
||||
|
||||
export function preparePlotConfigBuilder(
|
||||
frame: DataFrame,
|
||||
theme: GrafanaTheme2,
|
||||
getTimeRange: () => TimeRange,
|
||||
getTimeZone: () => TimeZone
|
||||
): UPlotConfigBuilder {
|
||||
const builder = new UPlotConfigBuilder(getTimeZone);
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
if (!xField) {
|
||||
return builder; // empty frame with no options
|
||||
}
|
||||
|
||||
let seriesIndex = 0;
|
||||
|
||||
if (xField.type === FieldType.time) {
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: true,
|
||||
range: () => {
|
||||
const r = getTimeRange();
|
||||
return [r.from.valueOf(), r.to.valueOf()];
|
||||
},
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
placement: AxisPlacement.Bottom,
|
||||
timeZone: getTimeZone(),
|
||||
theme,
|
||||
});
|
||||
} else {
|
||||
// Not time!
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
placement: AxisPlacement.Bottom,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
|
||||
let indexByName: Map<string, number> | undefined = undefined;
|
||||
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
const field = frame.fields[i];
|
||||
const config = field.config as FieldConfig<GraphFieldConfig>;
|
||||
const customConfig: GraphFieldConfig = {
|
||||
...defaultConfig,
|
||||
...config.custom,
|
||||
};
|
||||
|
||||
if (field === xField || field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
field.state!.seriesIndex = seriesIndex++;
|
||||
|
||||
const fmt = field.display ?? defaultFormatter;
|
||||
const scaleKey = config.unit || FIXED_UNIT;
|
||||
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({
|
||||
scaleKey,
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
distribution: customConfig.scaleDistribution?.type,
|
||||
log: customConfig.scaleDistribution?.log,
|
||||
min: field.config.min,
|
||||
max: field.config.max,
|
||||
softMin: customConfig.axisSoftMin,
|
||||
softMax: customConfig.axisSoftMax,
|
||||
});
|
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||
builder.addAxis({
|
||||
scaleKey,
|
||||
label: customConfig.axisLabel,
|
||||
size: customConfig.axisWidth,
|
||||
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
|
||||
formatValue: (v) => formattedValueToString(fmt(v)),
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
|
||||
|
||||
let { fillOpacity } = customConfig;
|
||||
|
||||
if (customConfig.fillBelowTo) {
|
||||
if (!indexByName) {
|
||||
indexByName = getNamesToFieldIndex(frame);
|
||||
}
|
||||
const t = indexByName.get(getFieldDisplayName(field, frame));
|
||||
const b = indexByName.get(customConfig.fillBelowTo);
|
||||
if (isNumber(b) && isNumber(t)) {
|
||||
builder.addBand({
|
||||
series: [t, b],
|
||||
fill: null as any, // using null will have the band use fill options from `t`
|
||||
});
|
||||
}
|
||||
if (!fillOpacity) {
|
||||
fillOpacity = 35; // default from flot
|
||||
}
|
||||
}
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey,
|
||||
showPoints,
|
||||
colorMode,
|
||||
fillOpacity,
|
||||
theme,
|
||||
drawStyle: customConfig.drawStyle!,
|
||||
lineColor: customConfig.lineColor ?? seriesColor,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineInterpolation: customConfig.lineInterpolation,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
barAlignment: customConfig.barAlignment,
|
||||
pointSize: customConfig.pointSize,
|
||||
pointColor: customConfig.pointColor ?? seriesColor,
|
||||
spanNulls: customConfig.spanNulls || false,
|
||||
show: !customConfig.hideFrom?.graph,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
thresholds: config.thresholds,
|
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex: field.state?.origin,
|
||||
fieldName: getFieldDisplayName(field, frame),
|
||||
hideInLegend: customConfig.hideFrom?.legend,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
builder.setStacking(true);
|
||||
for (const [_, seriesIdxs] of stackingGroups.entries()) {
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||||
const names = new Map<string, number>();
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
names.set(getFieldDisplayName(frame.fields[i], frame), i);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
55
packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx
Normal file
55
packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { DataFrame, TimeRange } from '@grafana/data';
|
||||
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
||||
import { preparePlotConfigBuilder } from './utils';
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
const propsToDiff: string[] = [];
|
||||
|
||||
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
|
||||
|
||||
export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
...this.props,
|
||||
});
|
||||
};
|
||||
|
||||
renderLegend = (config: UPlotConfigBuilder) => {
|
||||
const { legend, onLegendClick, frames } = this.props;
|
||||
|
||||
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<PlotLegend
|
||||
data={frames}
|
||||
config={config}
|
||||
onLegendClick={onLegendClick}
|
||||
maxHeight="35%"
|
||||
maxWidth="60%"
|
||||
{...legend}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<GraphNG
|
||||
{...this.props}
|
||||
prepConfig={this.prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
renderLegend={this.renderLegend as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TimeSeries = withTheme2(UnthemedTimeSeries);
|
||||
TimeSeries.displayName = 'TimeSeries';
|
209
packages/grafana-ui/src/components/TimeSeries/utils.ts
Normal file
209
packages/grafana-ui/src/components/TimeSeries/utils.ts
Normal file
@ -0,0 +1,209 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import {
|
||||
DataFrame,
|
||||
FieldConfig,
|
||||
FieldType,
|
||||
formattedValueToString,
|
||||
getFieldColorModeForField,
|
||||
getFieldDisplayName,
|
||||
getFieldSeriesColor,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { PrepConfigOpts } from '../GraphNG/utils';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||
import {
|
||||
AxisPlacement,
|
||||
DrawStyle,
|
||||
GraphFieldConfig,
|
||||
GraphTresholdsStyleMode,
|
||||
PointVisibility,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
} from '../uPlot/config';
|
||||
import { collectStackingGroups } from '../uPlot/utils';
|
||||
|
||||
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
drawStyle: DrawStyle.Line,
|
||||
showPoints: PointVisibility.Auto,
|
||||
axisPlacement: AxisPlacement.Auto,
|
||||
};
|
||||
|
||||
type PrepConfig = (opts: PrepConfigOpts) => UPlotConfigBuilder;
|
||||
|
||||
export const preparePlotConfigBuilder: PrepConfig = ({ frame, theme, timeZone, getTimeRange }) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
if (!xField) {
|
||||
return builder; // empty frame with no options
|
||||
}
|
||||
|
||||
let seriesIndex = 0;
|
||||
|
||||
if (xField.type === FieldType.time) {
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
isTime: true,
|
||||
range: () => {
|
||||
const timeRange = getTimeRange();
|
||||
return [timeRange.from.valueOf(), timeRange.to.valueOf()];
|
||||
},
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
placement: AxisPlacement.Bottom,
|
||||
timeZone,
|
||||
theme,
|
||||
});
|
||||
} else {
|
||||
// Not time!
|
||||
builder.addScale({
|
||||
scaleKey: 'x',
|
||||
orientation: ScaleOrientation.Horizontal,
|
||||
direction: ScaleDirection.Right,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
placement: AxisPlacement.Bottom,
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
|
||||
let indexByName: Map<string, number> | undefined = undefined;
|
||||
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
const field = frame.fields[i];
|
||||
const config = field.config as FieldConfig<GraphFieldConfig>;
|
||||
const customConfig: GraphFieldConfig = {
|
||||
...defaultConfig,
|
||||
...config.custom,
|
||||
};
|
||||
|
||||
if (field === xField || field.type !== FieldType.number) {
|
||||
continue;
|
||||
}
|
||||
field.state!.seriesIndex = seriesIndex++;
|
||||
|
||||
const fmt = field.display ?? defaultFormatter;
|
||||
const scaleKey = config.unit || FIXED_UNIT;
|
||||
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({
|
||||
scaleKey,
|
||||
orientation: ScaleOrientation.Vertical,
|
||||
direction: ScaleDirection.Up,
|
||||
distribution: customConfig.scaleDistribution?.type,
|
||||
log: customConfig.scaleDistribution?.log,
|
||||
min: field.config.min,
|
||||
max: field.config.max,
|
||||
softMin: customConfig.axisSoftMin,
|
||||
softMax: customConfig.axisSoftMax,
|
||||
});
|
||||
|
||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||
builder.addAxis({
|
||||
scaleKey,
|
||||
label: customConfig.axisLabel,
|
||||
size: customConfig.axisWidth,
|
||||
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
|
||||
formatValue: (v) => formattedValueToString(fmt(v)),
|
||||
theme,
|
||||
});
|
||||
}
|
||||
|
||||
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
|
||||
|
||||
let { fillOpacity } = customConfig;
|
||||
|
||||
if (customConfig.fillBelowTo) {
|
||||
if (!indexByName) {
|
||||
indexByName = getNamesToFieldIndex(frame);
|
||||
}
|
||||
const t = indexByName.get(getFieldDisplayName(field, frame));
|
||||
const b = indexByName.get(customConfig.fillBelowTo);
|
||||
if (isNumber(b) && isNumber(t)) {
|
||||
builder.addBand({
|
||||
series: [t, b],
|
||||
fill: null as any, // using null will have the band use fill options from `t`
|
||||
});
|
||||
}
|
||||
if (!fillOpacity) {
|
||||
fillOpacity = 35; // default from flot
|
||||
}
|
||||
}
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey,
|
||||
showPoints,
|
||||
colorMode,
|
||||
fillOpacity,
|
||||
theme,
|
||||
drawStyle: customConfig.drawStyle!,
|
||||
lineColor: customConfig.lineColor ?? seriesColor,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
lineInterpolation: customConfig.lineInterpolation,
|
||||
lineStyle: customConfig.lineStyle,
|
||||
barAlignment: customConfig.barAlignment,
|
||||
pointSize: customConfig.pointSize,
|
||||
pointColor: customConfig.pointColor ?? seriesColor,
|
||||
spanNulls: customConfig.spanNulls || false,
|
||||
show: !customConfig.hideFrom?.graph,
|
||||
gradientMode: customConfig.gradientMode,
|
||||
thresholds: config.thresholds,
|
||||
|
||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||||
dataFrameFieldIndex: field.state?.origin,
|
||||
fieldName: getFieldDisplayName(field, frame),
|
||||
hideInLegend: customConfig.hideFrom?.legend,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
builder.setStacking(true);
|
||||
for (const [_, seriesIdxs] of stackingGroups.entries()) {
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return builder;
|
||||
};
|
||||
|
||||
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||||
const names = new Map<string, number>();
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
names.set(getFieldDisplayName(frame.fields[i], frame), i);
|
||||
}
|
||||
return names;
|
||||
}
|
@ -1,146 +1,44 @@
|
||||
import React from 'react';
|
||||
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
|
||||
import { withTheme2 } from '../../themes/ThemeContext';
|
||||
import { GraphNGState } from '../GraphNG/GraphNG';
|
||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; // << preparePlotConfigBuilder is really the only change vs GraphNG
|
||||
import { pluginLog, preparePlotData } from '../uPlot/utils';
|
||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
||||
import { VizLayout } from '../VizLayout/VizLayout';
|
||||
import { TimelineProps } from './types';
|
||||
import { DataFrame, TimeRange } from '@grafana/data';
|
||||
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { preparePlotConfigBuilder } from './utils';
|
||||
import { BarValueVisibility, TimelineMode } from './types';
|
||||
|
||||
class UnthemedTimelineChart extends React.Component<TimelineProps, GraphNGState> {
|
||||
constructor(props: TimelineProps) {
|
||||
super(props);
|
||||
const { theme, mode, rowHeight, colWidth, showValue } = props;
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'> {
|
||||
mode: TimelineMode;
|
||||
rowHeight: number;
|
||||
showValue: BarValueVisibility;
|
||||
colWidth?: number;
|
||||
}
|
||||
|
||||
pluginLog('TimelineChart', false, 'constructor, data aligment');
|
||||
const alignedData = preparePlotFrame(
|
||||
props.data,
|
||||
props.fields || {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
}
|
||||
);
|
||||
const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue'];
|
||||
|
||||
if (!alignedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
alignedDataFrame: alignedData,
|
||||
data: preparePlotData(alignedData),
|
||||
config: preparePlotConfigBuilder(alignedData, theme, this.getTimeRange, this.getTimeZone, {
|
||||
mode,
|
||||
rowHeight,
|
||||
colWidth,
|
||||
showValue,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: TimelineProps) {
|
||||
const { data, theme, timeZone, mode, rowHeight, colWidth, showValue, structureRev } = this.props;
|
||||
let shouldConfigUpdate = false;
|
||||
let stateUpdate = {} as GraphNGState;
|
||||
|
||||
if (
|
||||
this.state.config === undefined ||
|
||||
timeZone !== prevProps.timeZone ||
|
||||
mode !== prevProps.mode ||
|
||||
rowHeight !== prevProps.rowHeight ||
|
||||
colWidth !== prevProps.colWidth ||
|
||||
showValue !== prevProps.showValue
|
||||
) {
|
||||
shouldConfigUpdate = true;
|
||||
}
|
||||
|
||||
if (data !== prevProps.data || shouldConfigUpdate) {
|
||||
const hasStructureChanged = structureRev !== prevProps.structureRev || !structureRev;
|
||||
|
||||
const alignedData = preparePlotFrame(
|
||||
data,
|
||||
this.props.fields || {
|
||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
||||
}
|
||||
);
|
||||
if (!alignedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
stateUpdate = {
|
||||
alignedDataFrame: alignedData,
|
||||
data: preparePlotData(alignedData),
|
||||
};
|
||||
if (shouldConfigUpdate || hasStructureChanged) {
|
||||
pluginLog('TimelineChart', false, 'updating config');
|
||||
const builder = preparePlotConfigBuilder(alignedData, theme, this.getTimeRange, this.getTimeZone, {
|
||||
mode,
|
||||
rowHeight,
|
||||
colWidth,
|
||||
showValue,
|
||||
});
|
||||
stateUpdate = { ...stateUpdate, config: builder };
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(stateUpdate).length > 0) {
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
getTimeRange = () => {
|
||||
return this.props.timeRange;
|
||||
export class UnthemedTimelineChart extends React.Component<TimelineProps> {
|
||||
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
getTimeRange,
|
||||
...this.props,
|
||||
});
|
||||
};
|
||||
|
||||
getTimeZone = () => {
|
||||
return this.props.timeZone;
|
||||
renderLegend = (config: UPlotConfigBuilder) => {
|
||||
return;
|
||||
};
|
||||
|
||||
renderLegend() {
|
||||
const { legend, onLegendClick, data } = this.props;
|
||||
const { config } = this.state;
|
||||
|
||||
if (!config || (legend && legend.displayMode === LegendDisplayMode.Hidden)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<PlotLegend
|
||||
data={data}
|
||||
config={config}
|
||||
onLegendClick={onLegendClick}
|
||||
maxHeight="35%"
|
||||
maxWidth="60%"
|
||||
{...legend}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { width, height, children, timeRange } = this.props;
|
||||
const { config, alignedDataFrame } = this.state;
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VizLayout width={width} height={height}>
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
config={this.state.config!}
|
||||
data={this.state.data}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
timeRange={timeRange}
|
||||
>
|
||||
{children ? children(config, alignedDataFrame) : null}
|
||||
</UPlotChart>
|
||||
)}
|
||||
</VizLayout>
|
||||
<GraphNG
|
||||
{...this.props}
|
||||
prepConfig={this.prepConfig}
|
||||
propsToDiff={propsToDiff}
|
||||
renderLegend={this.renderLegend as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { GraphNGProps } from '../GraphNG/GraphNG';
|
||||
import { GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
|
||||
import { VizLegendOptions } from '../VizLegend/models.gen';
|
||||
|
||||
@ -47,13 +46,3 @@ export enum TimelineMode {
|
||||
Spans = 'spans',
|
||||
Grid = 'grid',
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface TimelineProps extends GraphNGProps {
|
||||
mode: TimelineMode;
|
||||
rowHeight: number;
|
||||
showValue: BarValueVisibility;
|
||||
colWidth?: number;
|
||||
}
|
||||
|
@ -7,19 +7,18 @@ import {
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
outerJoinDataFrames,
|
||||
TimeRange,
|
||||
TimeZone,
|
||||
classicColors,
|
||||
Field,
|
||||
GrafanaTheme2,
|
||||
} from '@grafana/data';
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { TimelineCoreOptions, getConfig } from './timeline';
|
||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||
import { AxisPlacement, GraphGradientMode, ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
||||
import { measureText } from '../../utils/measureText';
|
||||
import { PrepConfigOpts } from '../GraphNG/utils';
|
||||
|
||||
import { TimelineFieldConfig } from '../..';
|
||||
import { BarValueVisibility, TimelineMode } from './types';
|
||||
|
||||
const defaultConfig: TimelineFieldConfig = {
|
||||
lineWidth: 0,
|
||||
@ -43,21 +42,27 @@ export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers)
|
||||
});
|
||||
}
|
||||
|
||||
export type uPlotConfigBuilderSupplier = (
|
||||
frame: DataFrame,
|
||||
theme: GrafanaTheme2,
|
||||
getTimeRange: () => TimeRange,
|
||||
getTimeZone: () => TimeZone
|
||||
) => UPlotConfigBuilder;
|
||||
interface PrepConfigOptsTimeline extends PrepConfigOpts {
|
||||
mode: TimelineMode;
|
||||
rowHeight: number;
|
||||
colWidth?: number;
|
||||
showValue: BarValueVisibility;
|
||||
}
|
||||
|
||||
export function preparePlotConfigBuilder(
|
||||
frame: DataFrame,
|
||||
theme: GrafanaTheme2,
|
||||
getTimeRange: () => TimeRange,
|
||||
getTimeZone: () => TimeZone,
|
||||
coreOptions: Partial<TimelineCoreOptions>
|
||||
): UPlotConfigBuilder {
|
||||
const builder = new UPlotConfigBuilder(getTimeZone);
|
||||
type PrepConfig = (opts: PrepConfigOptsTimeline) => UPlotConfigBuilder;
|
||||
|
||||
export const preparePlotConfigBuilder: PrepConfig = ({
|
||||
frame,
|
||||
theme,
|
||||
timeZone,
|
||||
getTimeRange,
|
||||
|
||||
mode,
|
||||
rowHeight,
|
||||
colWidth,
|
||||
showValue,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
const isDiscrete = (field: Field) => {
|
||||
const mode = field.config?.color?.mode;
|
||||
@ -86,12 +91,12 @@ export function preparePlotConfigBuilder(
|
||||
|
||||
const opts: TimelineCoreOptions = {
|
||||
// should expose in panel config
|
||||
mode: coreOptions.mode!,
|
||||
mode: mode!,
|
||||
numSeries: frame.fields.length - 1,
|
||||
isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]),
|
||||
rowHeight: coreOptions.rowHeight!,
|
||||
colWidth: coreOptions.colWidth,
|
||||
showValue: coreOptions.showValue!,
|
||||
rowHeight: rowHeight!,
|
||||
colWidth: colWidth,
|
||||
showValue: showValue!,
|
||||
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
|
||||
fill: colorLookup,
|
||||
stroke: colorLookup,
|
||||
@ -136,7 +141,7 @@ export function preparePlotConfigBuilder(
|
||||
isTime: true,
|
||||
splits: coreConfig.xSplits!,
|
||||
placement: AxisPlacement.Bottom,
|
||||
timeZone: getTimeZone(),
|
||||
timeZone,
|
||||
theme,
|
||||
});
|
||||
|
||||
@ -192,7 +197,7 @@ export function preparePlotConfigBuilder(
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
};
|
||||
|
||||
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||||
const names = new Map<string, number>();
|
||||
|
@ -240,6 +240,7 @@ export * from './uPlot/geometries';
|
||||
export * from './uPlot/plugins';
|
||||
export { usePlotContext } from './uPlot/context';
|
||||
export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
|
||||
export { TimeSeries } from './TimeSeries/TimeSeries';
|
||||
export { useGraphNGContext } from './GraphNG/hooks';
|
||||
export { preparePlotFrame } from './GraphNG/utils';
|
||||
export { BarChart } from './BarChart/BarChart';
|
||||
|
@ -1,94 +1,110 @@
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import uPlot, { AlignedData, Options } from 'uplot';
|
||||
import { PlotContext } from './context';
|
||||
import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils';
|
||||
import React, { createRef } from 'react';
|
||||
import uPlot, { Options } from 'uplot';
|
||||
import { PlotContext, PlotContextType } from './context';
|
||||
import { DEFAULT_PLOT_CONFIG } from './utils';
|
||||
import { PlotProps } from './types';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
|
||||
function sameDims(prevProps: PlotProps, nextProps: PlotProps) {
|
||||
return nextProps.width === prevProps.width && nextProps.height === prevProps.height;
|
||||
}
|
||||
|
||||
function sameData(prevProps: PlotProps, nextProps: PlotProps) {
|
||||
return nextProps.data === prevProps.data;
|
||||
}
|
||||
|
||||
function sameConfig(prevProps: PlotProps, nextProps: PlotProps) {
|
||||
return nextProps.config === prevProps.config;
|
||||
}
|
||||
|
||||
type UPlotChartState = {
|
||||
ctx: PlotContextType;
|
||||
};
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* uPlot abstraction responsible for plot initialisation, setup and refresh
|
||||
* Receives a data frame that is x-axis aligned, as of https://github.com/leeoniya/uPlot/tree/master/docs#data-format
|
||||
* Exposes contexts for plugins registration and uPlot instance access
|
||||
* Exposes context for uPlot instance access
|
||||
*/
|
||||
export const UPlotChart: React.FC<PlotProps> = (props) => {
|
||||
const plotContainer = useRef<HTMLDivElement>(null);
|
||||
const plotInstance = useRef<uPlot>();
|
||||
const prevProps = usePrevious(props);
|
||||
export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
||||
plotContainer = createRef<HTMLDivElement>();
|
||||
|
||||
const config = useMemo(() => {
|
||||
return {
|
||||
...DEFAULT_PLOT_CONFIG,
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
ms: 1,
|
||||
...props.config.getConfig(),
|
||||
} as uPlot.Options;
|
||||
}, [props.config]);
|
||||
constructor(props: PlotProps) {
|
||||
super(props);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!plotInstance.current || props.width === 0 || props.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pluginLog('uPlot core', false, 'updating size');
|
||||
plotInstance.current.setSize({
|
||||
width: props.width,
|
||||
height: props.height,
|
||||
});
|
||||
}, [props.width, props.height]);
|
||||
|
||||
// Effect responsible for uPlot updates/initialization logic. It's performed whenever component's props have changed
|
||||
useLayoutEffect(() => {
|
||||
// 0. Exit early if the component is not ready to initialize uPlot
|
||||
if (!plotContainer.current || props.width === 0 || props.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. When config is ready and there is no uPlot instance, create new uPlot and return
|
||||
if (!plotInstance.current || !prevProps) {
|
||||
plotInstance.current = initializePlot(props.data, config, plotContainer.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Reinitialize uPlot if config changed
|
||||
if (props.config !== prevProps.config) {
|
||||
if (plotInstance.current) {
|
||||
pluginLog('uPlot core', false, 'destroying instance');
|
||||
plotInstance.current.destroy();
|
||||
}
|
||||
plotInstance.current = initializePlot(props.data, config, plotContainer.current);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Otherwise, assume only data has changed and update uPlot data
|
||||
if (props.data !== prevProps.data) {
|
||||
pluginLog('uPlot core', false, 'updating plot data(throttled log!)', props.data);
|
||||
plotInstance.current.setData(props.data);
|
||||
}
|
||||
}, [props, config]);
|
||||
|
||||
// When component unmounts, clean the existing uPlot instance
|
||||
useEffect(() => () => plotInstance.current?.destroy(), []);
|
||||
|
||||
// Memoize plot context
|
||||
const plotCtx = useMemo(() => {
|
||||
return {
|
||||
getPlot: () => plotInstance.current,
|
||||
this.state = {
|
||||
ctx: {
|
||||
plot: null,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlotContext.Provider value={plotCtx}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={plotContainer} data-testid="uplot-main-div" />
|
||||
{props.children}
|
||||
</div>
|
||||
</PlotContext.Provider>
|
||||
);
|
||||
};
|
||||
reinitPlot() {
|
||||
let { ctx } = this.state;
|
||||
|
||||
function initializePlot(data: AlignedData, config: Options, el: HTMLDivElement) {
|
||||
pluginLog('UPlotChart: init uPlot', false, 'initialized with', data, config);
|
||||
return new uPlot(config, data, el);
|
||||
ctx.plot?.destroy();
|
||||
|
||||
let { width, height } = this.props;
|
||||
|
||||
if (width === 0 && height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config: Options = {
|
||||
...DEFAULT_PLOT_CONFIG,
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
ms: 1 as 1,
|
||||
...this.props.config.getConfig(),
|
||||
};
|
||||
|
||||
this.setState({
|
||||
ctx: {
|
||||
plot: new uPlot(config, this.props.data, this.plotContainer!.current!),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.reinitPlot();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.state.ctx.plot?.destroy();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PlotProps, nextState: UPlotChartState) {
|
||||
return (
|
||||
nextState.ctx !== this.state.ctx ||
|
||||
!sameDims(this.props, nextProps) ||
|
||||
!sameData(this.props, nextProps) ||
|
||||
!sameConfig(this.props, nextProps)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: PlotProps, prevState: object) {
|
||||
let { ctx } = this.state;
|
||||
|
||||
if (!sameDims(prevProps, this.props)) {
|
||||
ctx.plot?.setSize({
|
||||
width: this.props.width,
|
||||
height: this.props.height,
|
||||
});
|
||||
} else if (!sameConfig(prevProps, this.props)) {
|
||||
this.reinitPlot();
|
||||
} else if (!sameData(prevProps, this.props)) {
|
||||
ctx.plot?.setData(this.props.data);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PlotContext.Provider value={this.state.ctx}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div ref={this.plotContainer} data-testid="uplot-main-div" />
|
||||
{this.props.children}
|
||||
</div>
|
||||
</PlotContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,9 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
||||
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||
import { AxisPlacement } from '../config';
|
||||
import uPlot, { Cursor, Band, Hooks, BBox } from 'uplot';
|
||||
import uPlot, { Cursor, Band, Hooks, Select } from 'uplot';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import { DefaultTimeZone, getTimeZoneInfo } from '@grafana/data';
|
||||
import { DefaultTimeZone, getTimeZoneInfo, TimeZone } from '@grafana/data';
|
||||
import { pluginLog } from '../utils';
|
||||
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
||||
|
||||
@ -16,8 +16,7 @@ export class UPlotConfigBuilder {
|
||||
private bands: Band[] = [];
|
||||
private cursor: Cursor | undefined;
|
||||
private isStacking = false;
|
||||
// uPlot types don't export the Select interface prior to 1.6.4
|
||||
private select: Partial<BBox> | undefined;
|
||||
private select: uPlot.Select | undefined;
|
||||
private hasLeftAxis = false;
|
||||
private hasBottomAxis = false;
|
||||
private hooks: Hooks.Arrays = {};
|
||||
@ -25,8 +24,8 @@ export class UPlotConfigBuilder {
|
||||
// to prevent more than one threshold per scale
|
||||
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
||||
|
||||
constructor(getTimeZone = () => DefaultTimeZone) {
|
||||
this.tz = getTimeZoneInfo(getTimeZone(), Date.now())?.ianaName;
|
||||
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
||||
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
||||
}
|
||||
|
||||
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
|
||||
@ -85,8 +84,7 @@ export class UPlotConfigBuilder {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
// uPlot types don't export the Select interface prior to 1.6.4
|
||||
setSelect(select: Partial<BBox>) {
|
||||
setSelect(select: Select) {
|
||||
this.select = select;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { useContext } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
interface PlotContextType {
|
||||
getPlot: () => uPlot | undefined;
|
||||
export interface PlotContextType {
|
||||
plot: uPlot | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,7 +27,7 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
||||
|
||||
const eventMarkers = useMemo(() => {
|
||||
const markers: React.ReactNode[] = [];
|
||||
const plotInstance = plotCtx.getPlot();
|
||||
const plotInstance = plotCtx.plot;
|
||||
if (!plotInstance || events.length === 0) {
|
||||
return markers;
|
||||
}
|
||||
@ -50,7 +50,7 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
||||
return <>{markers}</>;
|
||||
}, [events, renderEventMarker, renderToken, plotCtx]);
|
||||
|
||||
if (!plotCtx.getPlot()) {
|
||||
if (!plotCtx.plot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ interface XYCanvasProps {}
|
||||
*/
|
||||
export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
|
||||
const plotCtx = usePlotContext();
|
||||
const plotInstance = plotCtx.getPlot();
|
||||
const plotInstance = plotCtx.plot;
|
||||
|
||||
if (!plotInstance) {
|
||||
return null;
|
||||
|
@ -74,7 +74,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const plotInstance = plotCtx.getPlot();
|
||||
const plotInstance = plotCtx.plot;
|
||||
if (!plotInstance || focusedPointIdx === null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ import {
|
||||
import {
|
||||
Collapse,
|
||||
DrawStyle,
|
||||
GraphNG,
|
||||
GraphNGLegendEvent,
|
||||
Icon,
|
||||
LegendDisplayMode,
|
||||
@ -24,6 +23,7 @@ import {
|
||||
useTheme2,
|
||||
ZoomPlugin,
|
||||
TooltipDisplayMode,
|
||||
TimeSeries,
|
||||
} from '@grafana/ui';
|
||||
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
|
||||
import { hideSeriesConfigFactory } from 'app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory';
|
||||
@ -135,8 +135,8 @@ export function ExploreGraphNGPanel({
|
||||
)}
|
||||
|
||||
<Collapse label="Graph" loading={isLoading} isOpen>
|
||||
<GraphNG
|
||||
data={seriesToShow}
|
||||
<TimeSeries
|
||||
frames={seriesToShow}
|
||||
structureRev={structureRev}
|
||||
width={width}
|
||||
height={400}
|
||||
@ -167,7 +167,7 @@ export function ExploreGraphNGPanel({
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</GraphNG>
|
||||
</TimeSeries>
|
||||
</Collapse>
|
||||
</>
|
||||
);
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
NavModelItem,
|
||||
PanelData,
|
||||
} from '@grafana/data';
|
||||
import { GraphNG, LegendDisplayMode, Table } from '@grafana/ui';
|
||||
import { LegendDisplayMode, Table, TimeSeries } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
@ -65,10 +65,10 @@ export const TestStuffPage: FC = () => {
|
||||
{({ width }) => {
|
||||
return (
|
||||
<div>
|
||||
<GraphNG
|
||||
<TimeSeries
|
||||
width={width}
|
||||
height={300}
|
||||
data={data.series}
|
||||
frames={data.series}
|
||||
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
|
||||
timeRange={data.timeRange}
|
||||
timeZone="browser"
|
||||
|
@ -35,7 +35,7 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
|
||||
return (
|
||||
<TimelineChart
|
||||
data={data.series}
|
||||
frames={data.series}
|
||||
structureRev={data.structureRev}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Field, PanelProps } from '@grafana/data';
|
||||
import { GraphNG, GraphNGLegendEvent, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
|
||||
import { TimeSeries, GraphNGLegendEvent, TooltipPlugin, ZoomPlugin } from '@grafana/ui';
|
||||
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||
import React, { useCallback } from 'react';
|
||||
import { hideSeriesConfigFactory } from './overrides/hideSeriesConfigFactory';
|
||||
@ -42,8 +42,8 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphNG
|
||||
data={data.series}
|
||||
<TimeSeries
|
||||
frames={data.series}
|
||||
structureRev={data.structureRev}
|
||||
timeRange={timeRange}
|
||||
timeZone={timeZone}
|
||||
@ -83,6 +83,6 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</GraphNG>
|
||||
</TimeSeries>
|
||||
);
|
||||
};
|
||||
|
@ -68,7 +68,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
(frame: DataFrame, index: number) => {
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(index);
|
||||
const plotInstance = plotCtx.getPlot();
|
||||
const plotInstance = plotCtx.plot;
|
||||
if (!annotation.time || !plotInstance) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
||||
|
||||
const mapExemplarToXYCoords = useCallback(
|
||||
(dataFrame: DataFrame, index: number) => {
|
||||
const plotInstance = plotCtx.getPlot();
|
||||
const plotInstance = plotCtx.plot;
|
||||
const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Button, GraphNG, GraphNGLegendEvent, TooltipPlugin } from '@grafana/ui';
|
||||
import { Button, GraphNGLegendEvent, TimeSeries, TooltipPlugin } from '@grafana/ui';
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
||||
@ -43,8 +43,8 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphNG
|
||||
data={frames}
|
||||
<TimeSeries
|
||||
frames={frames}
|
||||
structureRev={data.structureRev}
|
||||
fields={dims.fields}
|
||||
timeRange={timeRange}
|
||||
@ -64,6 +64,6 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</GraphNG>
|
||||
</TimeSeries>
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user