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 { Quadtree, Rect, pointWithin } from './quadtree';
|
||||||
import { distribute, SPACE_BETWEEN } from './distribute';
|
import { distribute, SPACE_BETWEEN } from './distribute';
|
||||||
|
|
||||||
@ -212,7 +212,7 @@ export function getConfig(opts: BarsOptions) {
|
|||||||
|
|
||||||
// disable selection
|
// disable selection
|
||||||
// uPlot types do not export the Select interface prior to 1.6.4
|
// uPlot types do not export the Select interface prior to 1.6.4
|
||||||
const select: Partial<BBox> = {
|
const select: Partial<Select> = {
|
||||||
show: false,
|
show: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import { BarChartFieldConfig, BarChartOptions, BarValueVisibility, defaultBarCha
|
|||||||
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config';
|
import { AxisPlacement, ScaleDirection, ScaleDistribution, ScaleOrientation } from '../uPlot/config';
|
||||||
import { BarsOptions, getConfig } from './bars';
|
import { BarsOptions, getConfig } from './bars';
|
||||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||||
|
import { Select } from 'uplot';
|
||||||
|
|
||||||
/** @alpha */
|
/** @alpha */
|
||||||
export function preparePlotConfigBuilder(
|
export function preparePlotConfigBuilder(
|
||||||
@ -68,7 +69,7 @@ export function preparePlotConfigBuilder(
|
|||||||
builder.addHook('setCursor', config.setCursor);
|
builder.addHook('setCursor', config.setCursor);
|
||||||
|
|
||||||
builder.setCursor(config.cursor);
|
builder.setCursor(config.cursor);
|
||||||
builder.setSelect(config.select);
|
builder.setSelect(config.select as Select);
|
||||||
|
|
||||||
builder.addScale({
|
builder.addScale({
|
||||||
scaleKey: 'x',
|
scaleKey: 'x',
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { FieldColorModeId, toDataFrame, dateTime } from '@grafana/data';
|
import { FieldColorModeId, toDataFrame, dateTime } from '@grafana/data';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { GraphNG, GraphNGProps } from './GraphNG';
|
import { GraphNGProps } from './GraphNG';
|
||||||
import { LegendDisplayMode, LegendPlacement } from '../VizLegend/models.gen';
|
import { LegendDisplayMode, LegendPlacement } from '../VizLegend/models.gen';
|
||||||
import { prepDataForStorybook } from '../../utils/storybook/data';
|
import { prepDataForStorybook } from '../../utils/storybook/data';
|
||||||
import { useTheme2 } from '../../themes';
|
import { useTheme2 } from '../../themes';
|
||||||
import { Story } from '@storybook/react';
|
import { Story } from '@storybook/react';
|
||||||
|
import { TimeSeries } from '../TimeSeries/TimeSeries';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Visualizations/GraphNG',
|
title: 'Visualizations/GraphNG',
|
||||||
component: GraphNG,
|
component: TimeSeries,
|
||||||
decorators: [withCenteredStory],
|
decorators: [withCenteredStory],
|
||||||
parameters: {
|
parameters: {
|
||||||
knobs: {
|
knobs: {
|
||||||
@ -51,9 +52,9 @@ export const Lines: Story<StoryProps> = ({ placement, unit, legendDisplayMode, .
|
|||||||
const data = prepDataForStorybook([seriesA], theme);
|
const data = prepDataForStorybook([seriesA], theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GraphNG
|
<TimeSeries
|
||||||
{...args}
|
{...args}
|
||||||
data={data}
|
frames={data}
|
||||||
legend={{
|
legend={{
|
||||||
displayMode:
|
displayMode:
|
||||||
legendDisplayMode === 'hidden'
|
legendDisplayMode === 'hidden'
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AlignedData } from 'uplot';
|
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 { Themeable2 } from '../../types';
|
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||||
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
||||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils';
|
import { preparePlotFrame } from './utils';
|
||||||
import { pluginLog, preparePlotData } from '../uPlot/utils';
|
import { preparePlotData } from '../uPlot/utils';
|
||||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
|
||||||
import { UPlotChart } from '../uPlot/Plot';
|
import { UPlotChart } from '../uPlot/Plot';
|
||||||
import { LegendDisplayMode, VizLegendOptions } from '../VizLegend/models.gen';
|
import { VizLegendOptions } from '../VizLegend/models.gen';
|
||||||
import { VizLayout } from '../VizLayout/VizLayout';
|
import { VizLayout } from '../VizLayout/VizLayout';
|
||||||
import { withTheme2 } from '../../themes/ThemeContext';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal -- not a public API
|
* @internal -- not a public API
|
||||||
@ -20,141 +18,124 @@ export const FIXED_UNIT = '__fixed';
|
|||||||
export interface GraphNGProps extends Themeable2 {
|
export interface GraphNGProps extends Themeable2 {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
data: DataFrame[];
|
frames: DataFrame[];
|
||||||
structureRev?: number; // a number that will change when the data[] structure changes
|
structureRev?: number; // a number that will change when the frames[] structure changes
|
||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
legend: VizLegendOptions;
|
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
|
legend: VizLegendOptions;
|
||||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
fields?: XYFieldMatchers; // default will assume timeseries data
|
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
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
|
* @internal -- not a public API
|
||||||
*/
|
*/
|
||||||
export interface GraphNGState {
|
export interface GraphNGState {
|
||||||
alignedDataFrame: DataFrame;
|
alignedFrame: DataFrame;
|
||||||
data: AlignedData;
|
alignedData: AlignedData;
|
||||||
config?: UPlotConfigBuilder;
|
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) {
|
constructor(props: GraphNGProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = this.prepState(props);
|
||||||
|
}
|
||||||
|
|
||||||
pluginLog('GraphNG', false, 'constructor, data aligment');
|
getTimeRange = () => this.props.timeRange;
|
||||||
const alignedData = preparePlotFrame(props.data, {
|
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!alignedData) {
|
prepState(props: GraphNGProps, withConfig = true) {
|
||||||
return;
|
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 = {
|
return state;
|
||||||
alignedDataFrame: alignedData,
|
|
||||||
data: preparePlotData(alignedData),
|
|
||||||
config: preparePlotConfigBuilder(alignedData, props.theme, this.getTimeRange, this.getTimeZone),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphNGProps) {
|
componentDidUpdate(prevProps: GraphNGProps) {
|
||||||
const { theme, structureRev, data } = this.props;
|
const { frames, structureRev, timeZone, propsToDiff } = this.props;
|
||||||
let shouldConfigUpdate = false;
|
|
||||||
let stateUpdate = {} as GraphNGState;
|
|
||||||
|
|
||||||
if (this.state.config === undefined || this.props.timeZone !== prevProps.timeZone) {
|
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
|
||||||
shouldConfigUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data !== prevProps.data) {
|
if (frames !== prevProps.frames || propsChanged) {
|
||||||
pluginLog('GraphNG', false, 'data changed');
|
let newState = this.prepState(this.props, false);
|
||||||
const hasStructureChanged = structureRev !== prevProps.structureRev || !structureRev;
|
|
||||||
|
|
||||||
if (hasStructureChanged) {
|
if (newState) {
|
||||||
pluginLog('GraphNG', false, 'schema changed');
|
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');
|
newState && this.setState(newState);
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
render() {
|
||||||
const { width, height, children, timeRange } = this.props;
|
const { width, height, children, timeRange, renderLegend } = this.props;
|
||||||
const { config, alignedDataFrame } = this.state;
|
const { config, alignedFrame } = this.state;
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout width={width} height={height} legend={this.renderLegend()}>
|
<VizLayout width={width} height={height} legend={renderLegend(config)}>
|
||||||
{(vizWidth: number, vizHeight: number) => (
|
{(vizWidth: number, vizHeight: number) => (
|
||||||
<UPlotChart
|
<UPlotChart
|
||||||
config={this.state.config!}
|
config={this.state.config!}
|
||||||
data={this.state.data}
|
data={this.state.alignedData}
|
||||||
width={vizWidth}
|
width={vizWidth}
|
||||||
height={vizHeight}
|
height={vizHeight}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
>
|
>
|
||||||
{children ? children(config, alignedDataFrame) : null}
|
{children ? children(config, alignedFrame) : null}
|
||||||
</UPlotChart>
|
</UPlotChart>
|
||||||
)}
|
)}
|
||||||
</VizLayout>
|
</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 {
|
import {
|
||||||
createTheme,
|
createTheme,
|
||||||
DefaultTimeZone,
|
DefaultTimeZone,
|
||||||
@ -188,12 +189,12 @@ jest.mock('@grafana/data', () => ({
|
|||||||
describe('GraphNG utils', () => {
|
describe('GraphNG utils', () => {
|
||||||
test('preparePlotConfigBuilder', () => {
|
test('preparePlotConfigBuilder', () => {
|
||||||
const frame = mockDataFrame();
|
const frame = mockDataFrame();
|
||||||
const result = preparePlotConfigBuilder(
|
const result = preparePlotConfigBuilder({
|
||||||
frame!,
|
frame: frame!,
|
||||||
createTheme(),
|
theme: createTheme(),
|
||||||
getDefaultTimeRange,
|
timeZone: DefaultTimeZone,
|
||||||
() => DefaultTimeZone
|
getTimeRange: getDefaultTimeRange,
|
||||||
).getConfig();
|
}).getConfig();
|
||||||
expect(result).toMatchSnapshot();
|
expect(result).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,41 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
import { GraphNGLegendEventMode, XYFieldMatchers } from './types';
|
import { GraphNGLegendEventMode, XYFieldMatchers } from './types';
|
||||||
import {
|
import {
|
||||||
ArrayVector,
|
ArrayVector,
|
||||||
DataFrame,
|
DataFrame,
|
||||||
FieldConfig,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
formattedValueToString,
|
|
||||||
getFieldColorModeForField,
|
|
||||||
getFieldDisplayName,
|
|
||||||
getFieldSeriesColor,
|
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
outerJoinDataFrames,
|
outerJoinDataFrames,
|
||||||
TimeRange,
|
TimeRange,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { nullToUndefThreshold } from './nullToUndefThreshold';
|
import { nullToUndefThreshold } from './nullToUndefThreshold';
|
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
export interface PrepConfigOpts {
|
||||||
import { FIXED_UNIT } from './GraphNG';
|
frame: DataFrame;
|
||||||
import {
|
theme: GrafanaTheme2;
|
||||||
AxisPlacement,
|
timeZone: TimeZone;
|
||||||
DrawStyle,
|
getTimeRange: () => TimeRange;
|
||||||
GraphFieldConfig,
|
[prop: string]: any;
|
||||||
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 function mapMouseEventToMode(event: React.MouseEvent): GraphNGLegendEventMode {
|
export function mapMouseEventToMode(event: React.MouseEvent): GraphNGLegendEventMode {
|
||||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||||
@ -72,192 +53,10 @@ function applySpanNullsThresholds(frames: DataFrame[]) {
|
|||||||
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) {
|
export function preparePlotFrame(frames: DataFrame[], dimFields: XYFieldMatchers) {
|
||||||
applySpanNullsThresholds(frames);
|
applySpanNullsThresholds(frames);
|
||||||
|
|
||||||
let joined = outerJoinDataFrames({
|
return outerJoinDataFrames({
|
||||||
frames: frames,
|
frames: frames,
|
||||||
joinBy: dimFields.x,
|
joinBy: dimFields.x,
|
||||||
keep: dimFields.y,
|
keep: dimFields.y,
|
||||||
keepOriginIndices: true,
|
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 React from 'react';
|
||||||
import { FieldMatcherID, fieldMatchers } from '@grafana/data';
|
|
||||||
import { withTheme2 } from '../../themes/ThemeContext';
|
import { withTheme2 } from '../../themes/ThemeContext';
|
||||||
import { GraphNGState } from '../GraphNG/GraphNG';
|
import { DataFrame, TimeRange } from '@grafana/data';
|
||||||
import { preparePlotConfigBuilder, preparePlotFrame } from './utils'; // << preparePlotConfigBuilder is really the only change vs GraphNG
|
import { GraphNG, GraphNGProps } from '../GraphNG/GraphNG';
|
||||||
import { pluginLog, preparePlotData } from '../uPlot/utils';
|
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||||
import { PlotLegend } from '../uPlot/PlotLegend';
|
import { preparePlotConfigBuilder } from './utils';
|
||||||
import { UPlotChart } from '../uPlot/Plot';
|
import { BarValueVisibility, TimelineMode } from './types';
|
||||||
import { LegendDisplayMode } from '../VizLegend/models.gen';
|
|
||||||
import { VizLayout } from '../VizLayout/VizLayout';
|
|
||||||
import { TimelineProps } from './types';
|
|
||||||
|
|
||||||
class UnthemedTimelineChart extends React.Component<TimelineProps, GraphNGState> {
|
/**
|
||||||
constructor(props: TimelineProps) {
|
* @alpha
|
||||||
super(props);
|
*/
|
||||||
const { theme, mode, rowHeight, colWidth, showValue } = props;
|
export interface TimelineProps extends Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'> {
|
||||||
|
mode: TimelineMode;
|
||||||
|
rowHeight: number;
|
||||||
|
showValue: BarValueVisibility;
|
||||||
|
colWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
pluginLog('TimelineChart', false, 'constructor, data aligment');
|
const propsToDiff = ['mode', 'rowHeight', 'colWidth', 'showValue'];
|
||||||
const alignedData = preparePlotFrame(
|
|
||||||
props.data,
|
|
||||||
props.fields || {
|
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!alignedData) {
|
export class UnthemedTimelineChart extends React.Component<TimelineProps> {
|
||||||
return;
|
prepConfig = (alignedFrame: DataFrame, getTimeRange: () => TimeRange) => {
|
||||||
}
|
return preparePlotConfigBuilder({
|
||||||
|
frame: alignedFrame,
|
||||||
this.state = {
|
getTimeRange,
|
||||||
alignedDataFrame: alignedData,
|
...this.props,
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getTimeZone = () => {
|
renderLegend = (config: UPlotConfigBuilder) => {
|
||||||
return this.props.timeZone;
|
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() {
|
render() {
|
||||||
const { width, height, children, timeRange } = this.props;
|
|
||||||
const { config, alignedDataFrame } = this.state;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VizLayout width={width} height={height}>
|
<GraphNG
|
||||||
{(vizWidth: number, vizHeight: number) => (
|
{...this.props}
|
||||||
<UPlotChart
|
prepConfig={this.prepConfig}
|
||||||
config={this.state.config!}
|
propsToDiff={propsToDiff}
|
||||||
data={this.state.data}
|
renderLegend={this.renderLegend as any}
|
||||||
width={vizWidth}
|
/>
|
||||||
height={vizHeight}
|
|
||||||
timeRange={timeRange}
|
|
||||||
>
|
|
||||||
{children ? children(config, alignedDataFrame) : null}
|
|
||||||
</UPlotChart>
|
|
||||||
)}
|
|
||||||
</VizLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { GraphNGProps } from '../GraphNG/GraphNG';
|
|
||||||
import { GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
|
import { GraphGradientMode, HideableFieldConfig } from '../uPlot/config';
|
||||||
import { VizLegendOptions } from '../VizLegend/models.gen';
|
import { VizLegendOptions } from '../VizLegend/models.gen';
|
||||||
|
|
||||||
@ -47,13 +46,3 @@ export enum TimelineMode {
|
|||||||
Spans = 'spans',
|
Spans = 'spans',
|
||||||
Grid = 'grid',
|
Grid = 'grid',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @alpha
|
|
||||||
*/
|
|
||||||
export interface TimelineProps extends GraphNGProps {
|
|
||||||
mode: TimelineMode;
|
|
||||||
rowHeight: number;
|
|
||||||
showValue: BarValueVisibility;
|
|
||||||
colWidth?: number;
|
|
||||||
}
|
|
||||||
|
@ -7,19 +7,18 @@ import {
|
|||||||
formattedValueToString,
|
formattedValueToString,
|
||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
outerJoinDataFrames,
|
outerJoinDataFrames,
|
||||||
TimeRange,
|
|
||||||
TimeZone,
|
|
||||||
classicColors,
|
classicColors,
|
||||||
Field,
|
Field,
|
||||||
GrafanaTheme2,
|
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||||
import { TimelineCoreOptions, getConfig } from './timeline';
|
import { TimelineCoreOptions, getConfig } from './timeline';
|
||||||
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
import { FIXED_UNIT } from '../GraphNG/GraphNG';
|
||||||
import { AxisPlacement, GraphGradientMode, ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
import { AxisPlacement, GraphGradientMode, ScaleDirection, ScaleOrientation } from '../uPlot/config';
|
||||||
import { measureText } from '../../utils/measureText';
|
import { measureText } from '../../utils/measureText';
|
||||||
|
import { PrepConfigOpts } from '../GraphNG/utils';
|
||||||
|
|
||||||
import { TimelineFieldConfig } from '../..';
|
import { TimelineFieldConfig } from '../..';
|
||||||
|
import { BarValueVisibility, TimelineMode } from './types';
|
||||||
|
|
||||||
const defaultConfig: TimelineFieldConfig = {
|
const defaultConfig: TimelineFieldConfig = {
|
||||||
lineWidth: 0,
|
lineWidth: 0,
|
||||||
@ -43,21 +42,27 @@ export function preparePlotFrame(data: DataFrame[], dimFields: XYFieldMatchers)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type uPlotConfigBuilderSupplier = (
|
interface PrepConfigOptsTimeline extends PrepConfigOpts {
|
||||||
frame: DataFrame,
|
mode: TimelineMode;
|
||||||
theme: GrafanaTheme2,
|
rowHeight: number;
|
||||||
getTimeRange: () => TimeRange,
|
colWidth?: number;
|
||||||
getTimeZone: () => TimeZone
|
showValue: BarValueVisibility;
|
||||||
) => UPlotConfigBuilder;
|
}
|
||||||
|
|
||||||
export function preparePlotConfigBuilder(
|
type PrepConfig = (opts: PrepConfigOptsTimeline) => UPlotConfigBuilder;
|
||||||
frame: DataFrame,
|
|
||||||
theme: GrafanaTheme2,
|
export const preparePlotConfigBuilder: PrepConfig = ({
|
||||||
getTimeRange: () => TimeRange,
|
frame,
|
||||||
getTimeZone: () => TimeZone,
|
theme,
|
||||||
coreOptions: Partial<TimelineCoreOptions>
|
timeZone,
|
||||||
): UPlotConfigBuilder {
|
getTimeRange,
|
||||||
const builder = new UPlotConfigBuilder(getTimeZone);
|
|
||||||
|
mode,
|
||||||
|
rowHeight,
|
||||||
|
colWidth,
|
||||||
|
showValue,
|
||||||
|
}) => {
|
||||||
|
const builder = new UPlotConfigBuilder(timeZone);
|
||||||
|
|
||||||
const isDiscrete = (field: Field) => {
|
const isDiscrete = (field: Field) => {
|
||||||
const mode = field.config?.color?.mode;
|
const mode = field.config?.color?.mode;
|
||||||
@ -86,12 +91,12 @@ export function preparePlotConfigBuilder(
|
|||||||
|
|
||||||
const opts: TimelineCoreOptions = {
|
const opts: TimelineCoreOptions = {
|
||||||
// should expose in panel config
|
// should expose in panel config
|
||||||
mode: coreOptions.mode!,
|
mode: mode!,
|
||||||
numSeries: frame.fields.length - 1,
|
numSeries: frame.fields.length - 1,
|
||||||
isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]),
|
isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]),
|
||||||
rowHeight: coreOptions.rowHeight!,
|
rowHeight: rowHeight!,
|
||||||
colWidth: coreOptions.colWidth,
|
colWidth: colWidth,
|
||||||
showValue: coreOptions.showValue!,
|
showValue: showValue!,
|
||||||
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
|
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
|
||||||
fill: colorLookup,
|
fill: colorLookup,
|
||||||
stroke: colorLookup,
|
stroke: colorLookup,
|
||||||
@ -136,7 +141,7 @@ export function preparePlotConfigBuilder(
|
|||||||
isTime: true,
|
isTime: true,
|
||||||
splits: coreConfig.xSplits!,
|
splits: coreConfig.xSplits!,
|
||||||
placement: AxisPlacement.Bottom,
|
placement: AxisPlacement.Bottom,
|
||||||
timeZone: getTimeZone(),
|
timeZone,
|
||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,7 +197,7 @@ export function preparePlotConfigBuilder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||||||
const names = new Map<string, number>();
|
const names = new Map<string, number>();
|
||||||
|
@ -240,6 +240,7 @@ export * from './uPlot/geometries';
|
|||||||
export * from './uPlot/plugins';
|
export * from './uPlot/plugins';
|
||||||
export { usePlotContext } from './uPlot/context';
|
export { usePlotContext } from './uPlot/context';
|
||||||
export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
|
export { GraphNG, FIXED_UNIT } from './GraphNG/GraphNG';
|
||||||
|
export { TimeSeries } from './TimeSeries/TimeSeries';
|
||||||
export { useGraphNGContext } from './GraphNG/hooks';
|
export { useGraphNGContext } from './GraphNG/hooks';
|
||||||
export { preparePlotFrame } from './GraphNG/utils';
|
export { preparePlotFrame } from './GraphNG/utils';
|
||||||
export { BarChart } from './BarChart/BarChart';
|
export { BarChart } from './BarChart/BarChart';
|
||||||
|
@ -1,94 +1,110 @@
|
|||||||
import React, { useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import uPlot, { AlignedData, Options } from 'uplot';
|
import uPlot, { Options } from 'uplot';
|
||||||
import { PlotContext } from './context';
|
import { PlotContext, PlotContextType } from './context';
|
||||||
import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils';
|
import { DEFAULT_PLOT_CONFIG } from './utils';
|
||||||
import { PlotProps } from './types';
|
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
|
* @internal
|
||||||
* uPlot abstraction responsible for plot initialisation, setup and refresh
|
* 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
|
* 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) => {
|
export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
||||||
const plotContainer = useRef<HTMLDivElement>(null);
|
plotContainer = createRef<HTMLDivElement>();
|
||||||
const plotInstance = useRef<uPlot>();
|
|
||||||
const prevProps = usePrevious(props);
|
|
||||||
|
|
||||||
const config = useMemo(() => {
|
constructor(props: PlotProps) {
|
||||||
return {
|
super(props);
|
||||||
...DEFAULT_PLOT_CONFIG,
|
|
||||||
width: props.width,
|
|
||||||
height: props.height,
|
|
||||||
ms: 1,
|
|
||||||
...props.config.getConfig(),
|
|
||||||
} as uPlot.Options;
|
|
||||||
}, [props.config]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
this.state = {
|
||||||
if (!plotInstance.current || props.width === 0 || props.height === 0) {
|
ctx: {
|
||||||
return;
|
plot: null,
|
||||||
}
|
},
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
return (
|
reinitPlot() {
|
||||||
<PlotContext.Provider value={plotCtx}>
|
let { ctx } = this.state;
|
||||||
<div style={{ position: 'relative' }}>
|
|
||||||
<div ref={plotContainer} data-testid="uplot-main-div" />
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
</PlotContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function initializePlot(data: AlignedData, config: Options, el: HTMLDivElement) {
|
ctx.plot?.destroy();
|
||||||
pluginLog('UPlotChart: init uPlot', false, 'initialized with', data, config);
|
|
||||||
return new uPlot(config, data, el);
|
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 { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||||
import { AxisPlacement } from '../config';
|
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 { defaultsDeep } from 'lodash';
|
||||||
import { DefaultTimeZone, getTimeZoneInfo } from '@grafana/data';
|
import { DefaultTimeZone, getTimeZoneInfo, TimeZone } from '@grafana/data';
|
||||||
import { pluginLog } from '../utils';
|
import { pluginLog } from '../utils';
|
||||||
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
||||||
|
|
||||||
@ -16,8 +16,7 @@ export class UPlotConfigBuilder {
|
|||||||
private bands: Band[] = [];
|
private bands: Band[] = [];
|
||||||
private cursor: Cursor | undefined;
|
private cursor: Cursor | undefined;
|
||||||
private isStacking = false;
|
private isStacking = false;
|
||||||
// uPlot types don't export the Select interface prior to 1.6.4
|
private select: uPlot.Select | undefined;
|
||||||
private select: Partial<BBox> | undefined;
|
|
||||||
private hasLeftAxis = false;
|
private hasLeftAxis = false;
|
||||||
private hasBottomAxis = false;
|
private hasBottomAxis = false;
|
||||||
private hooks: Hooks.Arrays = {};
|
private hooks: Hooks.Arrays = {};
|
||||||
@ -25,8 +24,8 @@ export class UPlotConfigBuilder {
|
|||||||
// to prevent more than one threshold per scale
|
// to prevent more than one threshold per scale
|
||||||
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
||||||
|
|
||||||
constructor(getTimeZone = () => DefaultTimeZone) {
|
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
||||||
this.tz = getTimeZoneInfo(getTimeZone(), Date.now())?.ianaName;
|
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
||||||
}
|
}
|
||||||
|
|
||||||
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
|
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
|
||||||
@ -85,8 +84,7 @@ export class UPlotConfigBuilder {
|
|||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
// uPlot types don't export the Select interface prior to 1.6.4
|
setSelect(select: Select) {
|
||||||
setSelect(select: Partial<BBox>) {
|
|
||||||
this.select = select;
|
this.select = select;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import uPlot from 'uplot';
|
import uPlot from 'uplot';
|
||||||
|
|
||||||
interface PlotContextType {
|
export interface PlotContextType {
|
||||||
getPlot: () => uPlot | undefined;
|
plot: uPlot | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,7 +27,7 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
|||||||
|
|
||||||
const eventMarkers = useMemo(() => {
|
const eventMarkers = useMemo(() => {
|
||||||
const markers: React.ReactNode[] = [];
|
const markers: React.ReactNode[] = [];
|
||||||
const plotInstance = plotCtx.getPlot();
|
const plotInstance = plotCtx.plot;
|
||||||
if (!plotInstance || events.length === 0) {
|
if (!plotInstance || events.length === 0) {
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
|||||||
return <>{markers}</>;
|
return <>{markers}</>;
|
||||||
}, [events, renderEventMarker, renderToken, plotCtx]);
|
}, [events, renderEventMarker, renderToken, plotCtx]);
|
||||||
|
|
||||||
if (!plotCtx.getPlot()) {
|
if (!plotCtx.plot) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ interface XYCanvasProps {}
|
|||||||
*/
|
*/
|
||||||
export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
|
export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
|
||||||
const plotCtx = usePlotContext();
|
const plotCtx = usePlotContext();
|
||||||
const plotInstance = plotCtx.getPlot();
|
const plotInstance = plotCtx.plot;
|
||||||
|
|
||||||
if (!plotInstance) {
|
if (!plotInstance) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -74,7 +74,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
});
|
});
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const plotInstance = plotCtx.getPlot();
|
const plotInstance = plotCtx.plot;
|
||||||
if (!plotInstance || focusedPointIdx === null) {
|
if (!plotInstance || focusedPointIdx === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Collapse,
|
Collapse,
|
||||||
DrawStyle,
|
DrawStyle,
|
||||||
GraphNG,
|
|
||||||
GraphNGLegendEvent,
|
GraphNGLegendEvent,
|
||||||
Icon,
|
Icon,
|
||||||
LegendDisplayMode,
|
LegendDisplayMode,
|
||||||
@ -24,6 +23,7 @@ import {
|
|||||||
useTheme2,
|
useTheme2,
|
||||||
ZoomPlugin,
|
ZoomPlugin,
|
||||||
TooltipDisplayMode,
|
TooltipDisplayMode,
|
||||||
|
TimeSeries,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
|
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
|
||||||
import { hideSeriesConfigFactory } from 'app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory';
|
import { hideSeriesConfigFactory } from 'app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory';
|
||||||
@ -135,8 +135,8 @@ export function ExploreGraphNGPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Collapse label="Graph" loading={isLoading} isOpen>
|
<Collapse label="Graph" loading={isLoading} isOpen>
|
||||||
<GraphNG
|
<TimeSeries
|
||||||
data={seriesToShow}
|
frames={seriesToShow}
|
||||||
structureRev={structureRev}
|
structureRev={structureRev}
|
||||||
width={width}
|
width={width}
|
||||||
height={400}
|
height={400}
|
||||||
@ -167,7 +167,7 @@ export function ExploreGraphNGPanel({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</GraphNG>
|
</TimeSeries>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
NavModelItem,
|
NavModelItem,
|
||||||
PanelData,
|
PanelData,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { GraphNG, LegendDisplayMode, Table } from '@grafana/ui';
|
import { LegendDisplayMode, Table, TimeSeries } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import React, { FC, useMemo, useState } from 'react';
|
import React, { FC, useMemo, useState } from 'react';
|
||||||
import { useObservable } from 'react-use';
|
import { useObservable } from 'react-use';
|
||||||
@ -65,10 +65,10 @@ export const TestStuffPage: FC = () => {
|
|||||||
{({ width }) => {
|
{({ width }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<GraphNG
|
<TimeSeries
|
||||||
width={width}
|
width={width}
|
||||||
height={300}
|
height={300}
|
||||||
data={data.series}
|
frames={data.series}
|
||||||
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
|
legend={{ displayMode: LegendDisplayMode.List, placement: 'bottom', calcs: [] }}
|
||||||
timeRange={data.timeRange}
|
timeRange={data.timeRange}
|
||||||
timeZone="browser"
|
timeZone="browser"
|
||||||
|
@ -35,7 +35,7 @@ export const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TimelineChart
|
<TimelineChart
|
||||||
data={data.series}
|
frames={data.series}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Field, PanelProps } from '@grafana/data';
|
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 { getFieldLinksForExplore } from 'app/features/explore/utils/links';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { hideSeriesConfigFactory } from './overrides/hideSeriesConfigFactory';
|
import { hideSeriesConfigFactory } from './overrides/hideSeriesConfigFactory';
|
||||||
@ -42,8 +42,8 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GraphNG
|
<TimeSeries
|
||||||
data={data.series}
|
frames={data.series}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
timeZone={timeZone}
|
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) => {
|
(frame: DataFrame, index: number) => {
|
||||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||||
const annotation = view.get(index);
|
const annotation = view.get(index);
|
||||||
const plotInstance = plotCtx.getPlot();
|
const plotInstance = plotCtx.plot;
|
||||||
if (!annotation.time || !plotInstance) {
|
if (!annotation.time || !plotInstance) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, tim
|
|||||||
|
|
||||||
const mapExemplarToXYCoords = useCallback(
|
const mapExemplarToXYCoords = useCallback(
|
||||||
(dataFrame: DataFrame, index: number) => {
|
(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 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);
|
const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
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 { PanelProps } from '@grafana/data';
|
||||||
import { Options } from './types';
|
import { Options } from './types';
|
||||||
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory';
|
||||||
@ -43,8 +43,8 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GraphNG
|
<TimeSeries
|
||||||
data={frames}
|
frames={frames}
|
||||||
structureRev={data.structureRev}
|
structureRev={data.structureRev}
|
||||||
fields={dims.fields}
|
fields={dims.fields}
|
||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
@ -64,6 +64,6 @@ export const XYChartPanel: React.FC<XYChartPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</GraphNG>
|
</TimeSeries>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user