mirror of https://github.com/grafana/grafana.git
Chore: Remove components from the graveyard folder in grafana/ui (#83545)
This commit is contained in:
parent
528ce96118
commit
6517431165
|
|
@ -1024,29 +1024,6 @@ exports[`better eslint`] = {
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||||
],
|
],
|
||||||
"packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/graveyard/Graph/utils.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "6"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "7"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "10"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "11"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/graveyard/GraphNG/hooks.ts:5381": [
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [
|
"packages/grafana-ui/src/graveyard/GraphNG/nullInsertThreshold.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
|
@ -1058,11 +1035,6 @@ exports[`better eslint`] = {
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||||
],
|
],
|
||||||
"packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
|
||||||
],
|
|
||||||
"packages/grafana-ui/src/options/builder/axis.tsx:5381": [
|
"packages/grafana-ui/src/options/builder/axis.tsx:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||||
|
|
|
||||||
|
|
@ -332,9 +332,7 @@
|
||||||
/packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad
|
/packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad
|
||||||
/packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad
|
/packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad
|
||||||
/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations
|
/packages/grafana-ui/src/components/Sparkline/ @grafana/grafana-frontend-platform @grafana/app-o11y-visualizations
|
||||||
/packages/grafana-ui/src/graveyard/Graph/ @grafana/dataviz-squad
|
|
||||||
/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad
|
/packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad
|
||||||
/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad
|
|
||||||
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
|
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
|
||||||
/packages/grafana-data/src/transformations/ @grafana/dataviz-squad
|
/packages/grafana-data/src/transformations/ @grafana/dataviz-squad
|
||||||
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
|
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Meta } from '@storybook/react';
|
import { Meta } from '@storybook/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GraphContextMenuHeader } from '..';
|
|
||||||
import { StoryExample } from '../../utils/storybook/StoryExample';
|
import { StoryExample } from '../../utils/storybook/StoryExample';
|
||||||
import { VerticalGroup } from '../Layout/Layout';
|
import { VerticalGroup } from '../Layout/Layout';
|
||||||
|
|
||||||
|
|
@ -110,30 +109,6 @@ export function Examples() {
|
||||||
<Menu.Item label="Disabled destructive action" icon="trash-alt" destructive disabled />
|
<Menu.Item label="Disabled destructive action" icon="trash-alt" destructive disabled />
|
||||||
</Menu>
|
</Menu>
|
||||||
</StoryExample>
|
</StoryExample>
|
||||||
<StoryExample name="With header & groups">
|
|
||||||
<Menu
|
|
||||||
header={
|
|
||||||
<GraphContextMenuHeader
|
|
||||||
timestamp="2020-11-25 19:04:25"
|
|
||||||
seriesColor="#00ff00"
|
|
||||||
displayName="A-series"
|
|
||||||
displayValue={{
|
|
||||||
text: '128',
|
|
||||||
suffix: 'km/h',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
ariaLabel="Menu header"
|
|
||||||
>
|
|
||||||
<Menu.Group label="Group 1">
|
|
||||||
<Menu.Item label="item1" icon="history" />
|
|
||||||
<Menu.Item label="item2" icon="filter" />
|
|
||||||
</Menu.Group>
|
|
||||||
<Menu.Group label="Group 2">
|
|
||||||
<Menu.Item label="item1" icon="history" />
|
|
||||||
</Menu.Group>
|
|
||||||
</Menu>
|
|
||||||
</StoryExample>
|
|
||||||
<StoryExample name="With submenu and shortcuts">
|
<StoryExample name="With submenu and shortcuts">
|
||||||
<Menu>
|
<Menu>
|
||||||
<Menu.Item label="item1" icon="history" shortcut="q p" />
|
<Menu.Item label="item1" icon="history" shortcut="q p" />
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,6 @@ export { VizLegend } from './VizLegend/VizLegend';
|
||||||
export { VizLegendListItem } from './VizLegend/VizLegendListItem';
|
export { VizLegendListItem } from './VizLegend/VizLegendListItem';
|
||||||
|
|
||||||
export { Alert, type AlertVariant } from './Alert/Alert';
|
export { Alert, type AlertVariant } from './Alert/Alert';
|
||||||
export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from '../graveyard/Graph/GraphSeriesToggler';
|
|
||||||
export { Collapse, ControlledCollapse } from './Collapse/Collapse';
|
export { Collapse, ControlledCollapse } from './Collapse/Collapse';
|
||||||
export { CollapsableSection } from './Collapse/CollapsableSection';
|
export { CollapsableSection } from './Collapse/CollapsableSection';
|
||||||
export { DataLinkButton } from './DataLinks/DataLinkButton';
|
export { DataLinkButton } from './DataLinks/DataLinkButton';
|
||||||
|
|
@ -296,19 +295,3 @@ export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder';
|
||||||
export * from './PanelChrome/types';
|
export * from './PanelChrome/types';
|
||||||
export { Label as BrowserLabel } from './BrowserLabel/Label';
|
export { Label as BrowserLabel } from './BrowserLabel/Label';
|
||||||
export { PanelContainer } from './PanelContainer/PanelContainer';
|
export { PanelContainer } from './PanelContainer/PanelContainer';
|
||||||
|
|
||||||
// -----------------------------------------------------
|
|
||||||
// Graveyard: exported, but no longer used internally
|
|
||||||
// These will be removed in the future
|
|
||||||
// -----------------------------------------------------
|
|
||||||
|
|
||||||
export { Graph } from '../graveyard/Graph/Graph';
|
|
||||||
export { GraphWithLegend } from '../graveyard/Graph/GraphWithLegend';
|
|
||||||
export { GraphContextMenu, GraphContextMenuHeader } from '../graveyard/Graph/GraphContextMenu';
|
|
||||||
export { graphTimeFormat, graphTickFormatter } from '../graveyard/Graph/utils';
|
|
||||||
|
|
||||||
export { GraphNG, type GraphNGProps } from '../graveyard/GraphNG/GraphNG';
|
|
||||||
export { TimeSeries } from '../graveyard/TimeSeries/TimeSeries';
|
|
||||||
export { useGraphNGContext } from '../graveyard/GraphNG/hooks';
|
|
||||||
export { preparePlotFrame, buildScaleKey } from '../graveyard/GraphNG/utils';
|
|
||||||
export { type GraphNGLegendEvent } from '../graveyard/GraphNG/types';
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data';
|
import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data';
|
||||||
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
||||||
|
|
||||||
import { preparePlotFrame } from '..';
|
// required for tests... but we actually have a duplicate copy that is used in the timeseries panel
|
||||||
|
// https://github.com/grafana/grafana/blob/v10.3.3/public/app/core/components/GraphNG/utils.test.ts
|
||||||
|
import { preparePlotFrame } from '../../graveyard/GraphNG/utils';
|
||||||
|
|
||||||
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
|
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
import { act, render, screen } from '@testing-library/react';
|
|
||||||
import $ from 'jquery';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId, DisplayProcessor } from '@grafana/data';
|
|
||||||
import { TooltipDisplayMode } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { VizTooltip } from '../../components/VizTooltip';
|
|
||||||
|
|
||||||
import Graph from './Graph';
|
|
||||||
|
|
||||||
const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' });
|
|
||||||
|
|
||||||
const series: GraphSeriesXY[] = [
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
[1546372800000, 10],
|
|
||||||
[1546376400000, 20],
|
|
||||||
[1546380000000, 10],
|
|
||||||
],
|
|
||||||
color: 'red',
|
|
||||||
isVisible: true,
|
|
||||||
label: 'A-series',
|
|
||||||
seriesIndex: 0,
|
|
||||||
timeField: {
|
|
||||||
type: FieldType.time,
|
|
||||||
name: 'time',
|
|
||||||
values: [1546372800000, 1546376400000, 1546380000000],
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
valueField: {
|
|
||||||
type: FieldType.number,
|
|
||||||
name: 'a-series',
|
|
||||||
values: [10, 20, 10],
|
|
||||||
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } },
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
timeStep: 3600000,
|
|
||||||
yAxis: {
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
[1546372800000, 20],
|
|
||||||
[1546376400000, 30],
|
|
||||||
[1546380000000, 40],
|
|
||||||
],
|
|
||||||
color: 'blue',
|
|
||||||
isVisible: true,
|
|
||||||
label: 'B-series',
|
|
||||||
seriesIndex: 0,
|
|
||||||
timeField: {
|
|
||||||
type: FieldType.time,
|
|
||||||
name: 'time',
|
|
||||||
values: [1546372800000, 1546376400000, 1546380000000],
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
valueField: {
|
|
||||||
type: FieldType.number,
|
|
||||||
name: 'b-series',
|
|
||||||
values: [20, 30, 40],
|
|
||||||
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } },
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
timeStep: 3600000,
|
|
||||||
yAxis: {
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockTimeRange = {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
raw: {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGraphProps = (multiSeries = false) => {
|
|
||||||
return {
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
series,
|
|
||||||
timeRange: mockTimeRange,
|
|
||||||
timeZone: 'browser',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
window.ResizeObserver = class ResizeObserver {
|
|
||||||
constructor() {}
|
|
||||||
observe() {}
|
|
||||||
unobserve() {}
|
|
||||||
disconnect() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Graph', () => {
|
|
||||||
describe('with tooltip', () => {
|
|
||||||
describe('in single mode', () => {
|
|
||||||
it("doesn't render tooltip when not hovering over a datapoint", () => {
|
|
||||||
const graphWithTooltip = (
|
|
||||||
<Graph {...mockGraphProps()}>
|
|
||||||
<VizTooltip mode={TooltipDisplayMode.Single} />
|
|
||||||
</Graph>
|
|
||||||
);
|
|
||||||
render(graphWithTooltip);
|
|
||||||
|
|
||||||
const timestamp = screen.queryByLabelText('Timestamp');
|
|
||||||
const tableRow = screen.queryByTestId('SeriesTableRow');
|
|
||||||
const seriesIcon = screen.queryByTestId('series-icon');
|
|
||||||
|
|
||||||
expect(timestamp).toBeFalsy();
|
|
||||||
expect(timestamp?.parentElement).toBeFalsy();
|
|
||||||
expect(tableRow?.parentElement).toBeFalsy();
|
|
||||||
expect(seriesIcon).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders tooltip when hovering over a datapoint', () => {
|
|
||||||
// Given
|
|
||||||
const graphWithTooltip = (
|
|
||||||
<Graph {...mockGraphProps()}>
|
|
||||||
<VizTooltip mode={TooltipDisplayMode.Single} />
|
|
||||||
</Graph>
|
|
||||||
);
|
|
||||||
render(graphWithTooltip);
|
|
||||||
const eventArgs = {
|
|
||||||
pos: {
|
|
||||||
x: 120,
|
|
||||||
y: 50,
|
|
||||||
},
|
|
||||||
activeItem: {
|
|
||||||
seriesIndex: 0,
|
|
||||||
dataIndex: 1,
|
|
||||||
series: { seriesIndex: 0 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
act(() => {
|
|
||||||
$('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]);
|
|
||||||
});
|
|
||||||
const timestamp = screen.getByLabelText('Timestamp');
|
|
||||||
const tooltip = screen.getByTestId('SeriesTableRow').parentElement;
|
|
||||||
|
|
||||||
expect(timestamp.parentElement?.isEqualNode(tooltip)).toBe(true);
|
|
||||||
expect(screen.getAllByTestId('series-icon')).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('in All Series mode', () => {
|
|
||||||
it('it renders all series summary regardless of mouse position', () => {
|
|
||||||
// Given
|
|
||||||
const graphWithTooltip = (
|
|
||||||
<Graph {...mockGraphProps(true)}>
|
|
||||||
<VizTooltip mode={TooltipDisplayMode.Multi} />
|
|
||||||
</Graph>
|
|
||||||
);
|
|
||||||
render(graphWithTooltip);
|
|
||||||
|
|
||||||
// When
|
|
||||||
const eventArgs = {
|
|
||||||
// This "is" more or less between first and middle point. Flot would not have picked any point as active one at this position
|
|
||||||
pos: {
|
|
||||||
x: 80,
|
|
||||||
y: 50,
|
|
||||||
},
|
|
||||||
activeItem: null,
|
|
||||||
};
|
|
||||||
// Then
|
|
||||||
act(() => {
|
|
||||||
$('div.graph-panel__chart').trigger('plothover', [eventArgs.pos, eventArgs.activeItem]);
|
|
||||||
});
|
|
||||||
const timestamp = screen.getByLabelText('Timestamp');
|
|
||||||
|
|
||||||
const tableRows = screen.getAllByTestId('SeriesTableRow');
|
|
||||||
expect(tableRows).toHaveLength(2);
|
|
||||||
expect(timestamp.parentElement?.isEqualNode(tableRows[0].parentElement)).toBe(true);
|
|
||||||
expect(timestamp.parentElement?.isEqualNode(tableRows[1].parentElement)).toBe(true);
|
|
||||||
|
|
||||||
const seriesIcon = screen.getAllByTestId('series-icon');
|
|
||||||
expect(seriesIcon).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,400 +0,0 @@
|
||||||
// Libraries
|
|
||||||
import $ from 'jquery';
|
|
||||||
import { uniqBy } from 'lodash';
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data';
|
|
||||||
import { TooltipDisplayMode } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../../components/VizTooltip';
|
|
||||||
import { FlotPosition } from '../../components/VizTooltip/VizTooltip';
|
|
||||||
|
|
||||||
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
|
|
||||||
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
|
|
||||||
import { GraphDimensions } from './GraphTooltip/types';
|
|
||||||
import { FlotItem } from './types';
|
|
||||||
import { graphTimeFormat, graphTickFormatter } from './utils';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphProps {
|
|
||||||
ariaLabel?: string;
|
|
||||||
children?: JSX.Element | JSX.Element[];
|
|
||||||
series: GraphSeriesXY[];
|
|
||||||
timeRange: TimeRange; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs
|
|
||||||
timeZone?: TimeZone; // NOTE: we should aim to make `time` a property of the axis, not force it for all graphs
|
|
||||||
showLines?: boolean;
|
|
||||||
showPoints?: boolean;
|
|
||||||
showBars?: boolean;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
isStacked?: boolean;
|
|
||||||
lineWidth?: number;
|
|
||||||
onHorizontalRegionSelected?: (from: number, to: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
interface GraphState {
|
|
||||||
pos?: FlotPosition;
|
|
||||||
contextPos?: FlotPosition;
|
|
||||||
isTooltipVisible: boolean;
|
|
||||||
isContextVisible: boolean;
|
|
||||||
activeItem?: FlotItem<GraphSeriesXY>;
|
|
||||||
contextItem?: FlotItem<GraphSeriesXY>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a react wrapper for the angular, flot based graph visualization.
|
|
||||||
* Rather than using this component, you should use the `<PanelRender .../> with
|
|
||||||
* timeseries panel configs.
|
|
||||||
*
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export class Graph extends PureComponent<GraphProps, GraphState> {
|
|
||||||
static defaultProps = {
|
|
||||||
showLines: true,
|
|
||||||
showPoints: false,
|
|
||||||
showBars: false,
|
|
||||||
isStacked: false,
|
|
||||||
lineWidth: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
state: GraphState = {
|
|
||||||
isTooltipVisible: false,
|
|
||||||
isContextVisible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
element: HTMLElement | null = null;
|
|
||||||
$element: JQuery<HTMLElement> | null = null;
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphProps, prevState: GraphState) {
|
|
||||||
if (prevProps !== this.props) {
|
|
||||||
this.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.draw();
|
|
||||||
if (this.element) {
|
|
||||||
this.$element = $(this.element);
|
|
||||||
this.$element.bind('plotselected', this.onPlotSelected);
|
|
||||||
this.$element.bind('plothover', this.onPlotHover);
|
|
||||||
this.$element.bind('plotclick', this.onPlotClick);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.$element) {
|
|
||||||
this.$element.unbind('plotselected', this.onPlotSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPlotSelected = (event: JQuery.Event, ranges: { xaxis: { from: number; to: number } }) => {
|
|
||||||
const { onHorizontalRegionSelected } = this.props;
|
|
||||||
if (onHorizontalRegionSelected) {
|
|
||||||
onHorizontalRegionSelected(ranges.xaxis.from, ranges.xaxis.to);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onPlotHover = (event: JQuery.Event, pos: FlotPosition, item?: FlotItem<GraphSeriesXY>) => {
|
|
||||||
this.setState({
|
|
||||||
isTooltipVisible: true,
|
|
||||||
activeItem: item,
|
|
||||||
pos,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onPlotClick = (event: JQuery.Event, contextPos: FlotPosition, item?: FlotItem<GraphSeriesXY>) => {
|
|
||||||
this.setState({
|
|
||||||
isContextVisible: true,
|
|
||||||
isTooltipVisible: false,
|
|
||||||
contextItem: item,
|
|
||||||
contextPos,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
getYAxes(series: GraphSeriesXY[]) {
|
|
||||||
if (series.length === 0) {
|
|
||||||
return [{ show: true, min: -1, max: 1 }];
|
|
||||||
}
|
|
||||||
return uniqBy(
|
|
||||||
series.map((s) => {
|
|
||||||
const index = s.yAxis ? s.yAxis.index : 1;
|
|
||||||
const min = s.yAxis && s.yAxis.min && !isNaN(s.yAxis.min) ? s.yAxis.min : null;
|
|
||||||
const tickDecimals =
|
|
||||||
s.yAxis && s.yAxis.tickDecimals && !isNaN(s.yAxis.tickDecimals) ? s.yAxis.tickDecimals : null;
|
|
||||||
return {
|
|
||||||
show: true,
|
|
||||||
index,
|
|
||||||
position: index === 1 ? 'left' : 'right',
|
|
||||||
min,
|
|
||||||
tickDecimals,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
(yAxisConfig) => yAxisConfig.index
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderTooltip = () => {
|
|
||||||
const { children, series, timeZone } = this.props;
|
|
||||||
const { pos, activeItem, isTooltipVisible } = this.state;
|
|
||||||
let tooltipElement: React.ReactElement<VizTooltipProps> | undefined;
|
|
||||||
|
|
||||||
if (!isTooltipVisible || !pos || series.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find children that indicate tooltip to be rendered
|
|
||||||
React.Children.forEach(children, (c) => {
|
|
||||||
// We have already found tooltip
|
|
||||||
if (tooltipElement) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const childType = c && c.type && (c.type.displayName || c.type.name);
|
|
||||||
|
|
||||||
if (childType === VizTooltip.displayName) {
|
|
||||||
tooltipElement = c;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// If no tooltip provided, skip rendering
|
|
||||||
if (!tooltipElement) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const tooltipElementProps = tooltipElement.props;
|
|
||||||
|
|
||||||
const tooltipMode = tooltipElementProps.mode || 'single';
|
|
||||||
|
|
||||||
// If mode is single series and user is not hovering over item, skip rendering
|
|
||||||
if (!activeItem && tooltipMode === 'single') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if tooltip needs to be rendered with custom tooltip component, otherwise default to GraphTooltip
|
|
||||||
const tooltipContentRenderer = tooltipElementProps.tooltipComponent || GraphTooltip;
|
|
||||||
// Indicates column(field) index in y-axis dimension
|
|
||||||
const seriesIndex = activeItem ? activeItem.series.seriesIndex : 0;
|
|
||||||
// Indicates row index in active field values
|
|
||||||
const rowIndex = activeItem ? activeItem.dataIndex : undefined;
|
|
||||||
|
|
||||||
const activeDimensions: ActiveDimensions<GraphDimensions> = {
|
|
||||||
// Described x-axis active item
|
|
||||||
// When hovering over an item - let's take it's dataIndex, otherwise undefined
|
|
||||||
// Tooltip itself needs to figure out correct datapoint display information based on pos passed to it
|
|
||||||
xAxis: [seriesIndex, rowIndex],
|
|
||||||
// Describes y-axis active item
|
|
||||||
yAxis: activeItem ? [activeItem.series.seriesIndex, activeItem.dataIndex] : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tooltipContentProps: VizTooltipContentProps<GraphDimensions> = {
|
|
||||||
dimensions: {
|
|
||||||
// time/value dimension columns are index-aligned - see getGraphSeriesModel
|
|
||||||
xAxis: createDimension(
|
|
||||||
'xAxis',
|
|
||||||
series.map((s) => s.timeField)
|
|
||||||
),
|
|
||||||
yAxis: createDimension(
|
|
||||||
'yAxis',
|
|
||||||
series.map((s) => s.valueField)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
activeDimensions,
|
|
||||||
pos,
|
|
||||||
mode: tooltipElementProps.mode || TooltipDisplayMode.Single,
|
|
||||||
timeZone,
|
|
||||||
};
|
|
||||||
|
|
||||||
const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps });
|
|
||||||
|
|
||||||
return React.cloneElement(tooltipElement, {
|
|
||||||
content: tooltipContent,
|
|
||||||
position: { x: pos.pageX, y: pos.pageY },
|
|
||||||
offset: { x: 10, y: 10 },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderContextMenu = () => {
|
|
||||||
const { series } = this.props;
|
|
||||||
const { contextPos, contextItem, isContextVisible } = this.state;
|
|
||||||
|
|
||||||
if (!isContextVisible || !contextPos || !contextItem || series.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indicates column(field) index in y-axis dimension
|
|
||||||
const seriesIndex = contextItem ? contextItem.series.seriesIndex : 0;
|
|
||||||
// Indicates row index in context field values
|
|
||||||
const rowIndex = contextItem ? contextItem.dataIndex : undefined;
|
|
||||||
|
|
||||||
const contextDimensions: ContextDimensions<GraphDimensions> = {
|
|
||||||
// Described x-axis context item
|
|
||||||
xAxis: [seriesIndex, rowIndex],
|
|
||||||
// Describes y-axis context item
|
|
||||||
yAxis: contextItem ? [contextItem.series.seriesIndex, contextItem.dataIndex] : null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dimensions: GraphDimensions = {
|
|
||||||
// time/value dimension columns are index-aligned - see getGraphSeriesModel
|
|
||||||
xAxis: createDimension(
|
|
||||||
'xAxis',
|
|
||||||
series.map((s) => s.timeField)
|
|
||||||
),
|
|
||||||
yAxis: createDimension(
|
|
||||||
'yAxis',
|
|
||||||
series.map((s) => s.valueField)
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeContext = () => this.setState({ isContextVisible: false });
|
|
||||||
|
|
||||||
const getContextMenuSource = () => {
|
|
||||||
return {
|
|
||||||
datapoint: contextItem.datapoint,
|
|
||||||
dataIndex: contextItem.dataIndex,
|
|
||||||
series: contextItem.series,
|
|
||||||
seriesIndex: contextItem.series.seriesIndex,
|
|
||||||
pageX: contextPos.pageX,
|
|
||||||
pageY: contextPos.pageY,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const contextContentProps: GraphContextMenuProps = {
|
|
||||||
x: contextPos.pageX,
|
|
||||||
y: contextPos.pageY,
|
|
||||||
onClose: closeContext,
|
|
||||||
getContextMenuSource: getContextMenuSource,
|
|
||||||
timeZone: this.props.timeZone,
|
|
||||||
dimensions,
|
|
||||||
contextDimensions,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <GraphContextMenu {...contextContentProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
getBarWidth = () => {
|
|
||||||
const { series } = this.props;
|
|
||||||
return Math.min(...series.map((s) => s.timeStep));
|
|
||||||
};
|
|
||||||
|
|
||||||
draw() {
|
|
||||||
if (this.element === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
series,
|
|
||||||
timeRange,
|
|
||||||
showLines,
|
|
||||||
showBars,
|
|
||||||
showPoints,
|
|
||||||
isStacked,
|
|
||||||
lineWidth,
|
|
||||||
timeZone,
|
|
||||||
onHorizontalRegionSelected,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!width) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticks = width / 100;
|
|
||||||
const min = timeRange.from.valueOf();
|
|
||||||
const max = timeRange.to.valueOf();
|
|
||||||
const yaxes = this.getYAxes(series);
|
|
||||||
|
|
||||||
const flotOptions = {
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
series: {
|
|
||||||
stack: isStacked,
|
|
||||||
lines: {
|
|
||||||
show: showLines,
|
|
||||||
lineWidth: lineWidth,
|
|
||||||
zero: false,
|
|
||||||
},
|
|
||||||
points: {
|
|
||||||
show: showPoints,
|
|
||||||
fill: 1,
|
|
||||||
fillColor: false,
|
|
||||||
radius: 2,
|
|
||||||
},
|
|
||||||
bars: {
|
|
||||||
show: showBars,
|
|
||||||
fill: 1,
|
|
||||||
// Dividig the width by 1.5 to make the bars not touch each other
|
|
||||||
barWidth: showBars ? this.getBarWidth() / 1.5 : 1,
|
|
||||||
zero: false,
|
|
||||||
lineWidth: lineWidth,
|
|
||||||
},
|
|
||||||
shadowSize: 0,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
timezone: timeZone,
|
|
||||||
show: true,
|
|
||||||
mode: 'time',
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
label: 'Datetime',
|
|
||||||
ticks: ticks,
|
|
||||||
timeformat: graphTimeFormat(ticks, min, max),
|
|
||||||
tickFormatter: graphTickFormatter,
|
|
||||||
},
|
|
||||||
yaxes,
|
|
||||||
grid: {
|
|
||||||
minBorderMargin: 0,
|
|
||||||
markings: [],
|
|
||||||
backgroundColor: null,
|
|
||||||
borderWidth: 0,
|
|
||||||
hoverable: true,
|
|
||||||
clickable: true,
|
|
||||||
color: '#a1a1a1',
|
|
||||||
margin: { left: 0, right: 0 },
|
|
||||||
labelMarginX: 0,
|
|
||||||
mouseActiveRadius: 30,
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
mode: onHorizontalRegionSelected ? 'x' : null,
|
|
||||||
color: '#666',
|
|
||||||
},
|
|
||||||
crosshair: {
|
|
||||||
mode: 'x',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
$.plot(
|
|
||||||
this.element,
|
|
||||||
series.filter((s) => s.isVisible),
|
|
||||||
flotOptions
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Graph rendering error', err, flotOptions, series);
|
|
||||||
throw new Error('Error rendering panel');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { ariaLabel, height, width, series } = this.props;
|
|
||||||
const noDataToBeDisplayed = series.length === 0;
|
|
||||||
const tooltip = this.renderTooltip();
|
|
||||||
const context = this.renderContextMenu();
|
|
||||||
return (
|
|
||||||
<div className="graph-panel" aria-label={ariaLabel}>
|
|
||||||
<div
|
|
||||||
className="graph-panel__chart"
|
|
||||||
ref={(e) => (this.element = e)}
|
|
||||||
style={{ height, width }}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
this.setState({ isTooltipVisible: false });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{noDataToBeDisplayed && <div className="datapoints-warning">No data</div>}
|
|
||||||
{tooltip}
|
|
||||||
{context}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Graph;
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
import { difference, isEqual } from 'lodash';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { GraphSeriesXY } from '@grafana/data';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphSeriesTogglerAPI {
|
|
||||||
onSeriesToggle: (label: string, event: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
toggledSeries: GraphSeriesXY[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphSeriesTogglerProps {
|
|
||||||
children: (api: GraphSeriesTogglerAPI) => JSX.Element;
|
|
||||||
series: GraphSeriesXY[];
|
|
||||||
onHiddenSeriesChanged?: (hiddenSeries: string[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphSeriesTogglerState {
|
|
||||||
hiddenSeries: string[];
|
|
||||||
toggledSeries: GraphSeriesXY[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export class GraphSeriesToggler extends Component<GraphSeriesTogglerProps, GraphSeriesTogglerState> {
|
|
||||||
constructor(props: GraphSeriesTogglerProps) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.onSeriesToggle = this.onSeriesToggle.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
hiddenSeries: [],
|
|
||||||
toggledSeries: props.series,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<GraphSeriesTogglerProps>) {
|
|
||||||
const { series } = this.props;
|
|
||||||
if (!isEqual(prevProps.series, series)) {
|
|
||||||
this.setState({ hiddenSeries: [], toggledSeries: series });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSeriesToggle(label: string, event: React.MouseEvent<HTMLElement>) {
|
|
||||||
const { series, onHiddenSeriesChanged } = this.props;
|
|
||||||
const { hiddenSeries } = this.state;
|
|
||||||
|
|
||||||
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
|
||||||
// Toggling series with key makes the series itself to toggle
|
|
||||||
const newHiddenSeries =
|
|
||||||
hiddenSeries.indexOf(label) > -1
|
|
||||||
? hiddenSeries.filter((series) => series !== label)
|
|
||||||
: hiddenSeries.concat([label]);
|
|
||||||
|
|
||||||
const toggledSeries = series.map((series) => ({
|
|
||||||
...series,
|
|
||||||
isVisible: newHiddenSeries.indexOf(series.label) === -1,
|
|
||||||
}));
|
|
||||||
this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
|
|
||||||
onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggling series with out key toggles all the series but the clicked one
|
|
||||||
const allSeriesLabels = series.map((series) => series.label);
|
|
||||||
const newHiddenSeries =
|
|
||||||
hiddenSeries.length + 1 === allSeriesLabels.length ? [] : difference(allSeriesLabels, [label]);
|
|
||||||
const toggledSeries = series.map((series) => ({
|
|
||||||
...series,
|
|
||||||
isVisible: newHiddenSeries.indexOf(series.label) === -1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.setState({ hiddenSeries: newHiddenSeries, toggledSeries }, () =>
|
|
||||||
onHiddenSeriesChanged ? onHiddenSeriesChanged(newHiddenSeries) : undefined
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children } = this.props;
|
|
||||||
const { toggledSeries } = this.state;
|
|
||||||
|
|
||||||
return children({
|
|
||||||
onSeriesToggle: this.onSeriesToggle,
|
|
||||||
toggledSeries,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { TooltipDisplayMode } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { VizTooltipContentProps } from '../../../components/VizTooltip';
|
|
||||||
|
|
||||||
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
|
|
||||||
import { SingleModeGraphTooltip } from './SingleModeGraphTooltip';
|
|
||||||
import { GraphDimensions } from './types';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const GraphTooltip = ({
|
|
||||||
mode = TooltipDisplayMode.Single,
|
|
||||||
dimensions,
|
|
||||||
activeDimensions,
|
|
||||||
pos,
|
|
||||||
timeZone,
|
|
||||||
}: VizTooltipContentProps<GraphDimensions>) => {
|
|
||||||
// When
|
|
||||||
// [1] no active dimension or
|
|
||||||
// [2] no xAxis position
|
|
||||||
// we assume no tooltip should be rendered
|
|
||||||
if (!activeDimensions || !activeDimensions.xAxis) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'single') {
|
|
||||||
return <SingleModeGraphTooltip dimensions={dimensions} activeDimensions={activeDimensions} timeZone={timeZone} />;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<MultiModeGraphTooltip
|
|
||||||
dimensions={dimensions}
|
|
||||||
activeDimensions={activeDimensions}
|
|
||||||
pos={pos}
|
|
||||||
timeZone={timeZone}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
GraphTooltip.displayName = 'GraphTooltip';
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { createDimension, createTheme, FieldType, DisplayProcessor } from '@grafana/data';
|
|
||||||
|
|
||||||
import { ActiveDimensions } from '../../../components/VizTooltip';
|
|
||||||
|
|
||||||
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
|
|
||||||
import { GraphDimensions } from './types';
|
|
||||||
|
|
||||||
let dimensions: GraphDimensions;
|
|
||||||
|
|
||||||
describe('MultiModeGraphTooltip', () => {
|
|
||||||
const display: DisplayProcessor = (v) => ({ numeric: Number(v), text: String(v), color: 'red' });
|
|
||||||
const theme = createTheme();
|
|
||||||
|
|
||||||
describe('when shown when hovering over a datapoint', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
dimensions = {
|
|
||||||
xAxis: createDimension('xAxis', [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [0, 100, 200],
|
|
||||||
name: 'A-series time',
|
|
||||||
type: FieldType.time,
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [0, 100, 200],
|
|
||||||
name: 'B-series time',
|
|
||||||
type: FieldType.time,
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
yAxis: createDimension('yAxis', [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [10, 20, 10],
|
|
||||||
name: 'A-series values',
|
|
||||||
type: FieldType.number,
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [20, 30, 40],
|
|
||||||
name: 'B-series values',
|
|
||||||
type: FieldType.number,
|
|
||||||
display,
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('highlights series of the datapoint', () => {
|
|
||||||
// We are simulating hover over A-series, middle point
|
|
||||||
const activeDimensions: ActiveDimensions<GraphDimensions> = {
|
|
||||||
xAxis: [0, 1], // column, row
|
|
||||||
yAxis: [0, 1], // column, row
|
|
||||||
};
|
|
||||||
render(
|
|
||||||
<MultiModeGraphTooltip
|
|
||||||
dimensions={dimensions}
|
|
||||||
activeDimensions={activeDimensions}
|
|
||||||
// pos is not relevant in this test
|
|
||||||
pos={{ x: 0, y: 0, pageX: 0, pageY: 0, x1: 0, y1: 0 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// We rendered two series rows
|
|
||||||
const rows = screen.getAllByTestId('SeriesTableRow');
|
|
||||||
expect(rows.length).toEqual(2);
|
|
||||||
|
|
||||||
// We expect A-series(1st row) not to be highlighted
|
|
||||||
expect(rows[0]).toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`);
|
|
||||||
// We expect B-series(2nd row) not to be highlighted
|
|
||||||
expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("doesn't highlight series when not hovering over datapoint", () => {
|
|
||||||
// We are simulating hover over graph, but not datapoint
|
|
||||||
const activeDimensions: ActiveDimensions<GraphDimensions> = {
|
|
||||||
xAxis: [0, undefined], // no active point in time
|
|
||||||
yAxis: null, // no active series
|
|
||||||
};
|
|
||||||
|
|
||||||
render(
|
|
||||||
<MultiModeGraphTooltip
|
|
||||||
dimensions={dimensions}
|
|
||||||
activeDimensions={activeDimensions}
|
|
||||||
// pos is not relevant in this test
|
|
||||||
pos={{ x: 0, y: 0, pageX: 0, pageY: 0, x1: 0, y1: 0 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// We rendered two series rows
|
|
||||||
const rows = screen.getAllByTestId('SeriesTableRow');
|
|
||||||
expect(rows.length).toEqual(2);
|
|
||||||
|
|
||||||
// We expect A-series(1st row) not to be highlighted
|
|
||||||
expect(rows[0]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`);
|
|
||||||
// We expect B-series(2nd row) not to be highlighted
|
|
||||||
expect(rows[1]).not.toHaveStyle(`font-weight: ${theme.typography.fontWeightMedium}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getValueFromDimension } from '@grafana/data';
|
|
||||||
|
|
||||||
import { SeriesTable } from '../../../components/VizTooltip';
|
|
||||||
import { FlotPosition } from '../../../components/VizTooltip/VizTooltip';
|
|
||||||
import { getMultiSeriesGraphHoverInfo } from '../utils';
|
|
||||||
|
|
||||||
import { GraphTooltipContentProps } from './types';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
type Props = GraphTooltipContentProps & {
|
|
||||||
// We expect position to figure out correct values when not hovering over a datapoint
|
|
||||||
pos: FlotPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const MultiModeGraphTooltip = ({ dimensions, activeDimensions, pos, timeZone }: Props) => {
|
|
||||||
let activeSeriesIndex: number | null = null;
|
|
||||||
// when no x-axis provided, skip rendering
|
|
||||||
if (activeDimensions.xAxis === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeDimensions.yAxis) {
|
|
||||||
activeSeriesIndex = activeDimensions.yAxis[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// when not hovering over a point, time is undefined, and we use pos.x as time
|
|
||||||
const time = activeDimensions.xAxis[1]
|
|
||||||
? getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1])
|
|
||||||
: pos.x;
|
|
||||||
|
|
||||||
const hoverInfo = getMultiSeriesGraphHoverInfo(dimensions.yAxis.columns, dimensions.xAxis.columns, time, timeZone);
|
|
||||||
const timestamp = hoverInfo.time;
|
|
||||||
|
|
||||||
const series = hoverInfo.results.map((s, i) => {
|
|
||||||
return {
|
|
||||||
color: s.color,
|
|
||||||
label: s.label,
|
|
||||||
value: s.value,
|
|
||||||
isActive: activeSeriesIndex === i,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return <SeriesTable series={series} timestamp={timestamp} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
MultiModeGraphTooltip.displayName = 'MultiModeGraphTooltip';
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getValueFromDimension,
|
|
||||||
getColumnFromDimension,
|
|
||||||
formattedValueToString,
|
|
||||||
getFieldDisplayName,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
import { SeriesTable } from '../../../components/VizTooltip';
|
|
||||||
|
|
||||||
import { GraphTooltipContentProps } from './types';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const SingleModeGraphTooltip = ({ dimensions, activeDimensions, timeZone }: GraphTooltipContentProps) => {
|
|
||||||
// not hovering over a point, skip rendering
|
|
||||||
if (
|
|
||||||
activeDimensions.yAxis === null ||
|
|
||||||
activeDimensions.yAxis[1] === undefined ||
|
|
||||||
activeDimensions.xAxis === null ||
|
|
||||||
activeDimensions.xAxis[1] === undefined
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const time = getValueFromDimension(dimensions.xAxis, activeDimensions.xAxis[0], activeDimensions.xAxis[1]);
|
|
||||||
const timeField = getColumnFromDimension(dimensions.xAxis, activeDimensions.xAxis[0]);
|
|
||||||
const processedTime = timeField.display ? formattedValueToString(timeField.display(time)) : time;
|
|
||||||
|
|
||||||
const valueField = getColumnFromDimension(dimensions.yAxis, activeDimensions.yAxis[0]);
|
|
||||||
const value = getValueFromDimension(dimensions.yAxis, activeDimensions.yAxis[0], activeDimensions.yAxis[1]);
|
|
||||||
const display = valueField.display!;
|
|
||||||
const disp = display(value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SeriesTable
|
|
||||||
series={[
|
|
||||||
{
|
|
||||||
color: disp.color,
|
|
||||||
label: getFieldDisplayName(valueField),
|
|
||||||
value: formattedValueToString(disp),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
timestamp={processedTime}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
SingleModeGraphTooltip.displayName = 'SingleModeGraphTooltip';
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { Dimension, Dimensions, TimeZone } from '@grafana/data';
|
|
||||||
|
|
||||||
import { ActiveDimensions } from '../../../components/VizTooltip';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphDimensions extends Dimensions {
|
|
||||||
xAxis: Dimension<number>;
|
|
||||||
yAxis: Dimension<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphTooltipContentProps {
|
|
||||||
dimensions: GraphDimensions; // Dimension[]
|
|
||||||
activeDimensions: ActiveDimensions<GraphDimensions>;
|
|
||||||
timeZone?: TimeZone;
|
|
||||||
}
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import { Story } from '@storybook/react';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GraphSeriesXY, FieldType, dateTime, FieldColorModeId } from '@grafana/data';
|
|
||||||
import { LegendDisplayMode } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { GraphWithLegend, GraphWithLegendProps } from './GraphWithLegend';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
title: 'Visualizations/Graph/GraphWithLegend',
|
|
||||||
component: GraphWithLegend,
|
|
||||||
parameters: {
|
|
||||||
controls: {
|
|
||||||
exclude: ['className', 'ariaLabel', 'legendDisplayMode', 'series'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
displayMode: { control: { type: 'radio' }, options: ['table', 'list', 'hidden'] },
|
|
||||||
placement: { control: { type: 'radio' }, options: ['bottom', 'right'] },
|
|
||||||
rightAxisSeries: { name: 'Right y-axis series, i.e. A,C' },
|
|
||||||
timeZone: { control: { type: 'radio' }, options: ['browser', 'utc'] },
|
|
||||||
width: { control: { type: 'range', min: 200, max: 800 } },
|
|
||||||
height: { control: { type: 'range', min: 1700, step: 300 } },
|
|
||||||
lineWidth: { control: { type: 'range', min: 1, max: 10 } },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const series: GraphSeriesXY[] = [
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
[1546372800000, 10],
|
|
||||||
[1546376400000, 20],
|
|
||||||
[1546380000000, 10],
|
|
||||||
],
|
|
||||||
color: 'red',
|
|
||||||
isVisible: true,
|
|
||||||
label: 'A-series',
|
|
||||||
seriesIndex: 0,
|
|
||||||
timeField: {
|
|
||||||
type: FieldType.time,
|
|
||||||
name: 'time',
|
|
||||||
values: [1546372800000, 1546376400000, 1546380000000],
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
valueField: {
|
|
||||||
type: FieldType.number,
|
|
||||||
name: 'a-series',
|
|
||||||
values: [10, 20, 10],
|
|
||||||
config: {
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
fixedColor: 'red',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timeStep: 3600000,
|
|
||||||
yAxis: {
|
|
||||||
index: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: [
|
|
||||||
[1546372800000, 20],
|
|
||||||
[1546376400000, 30],
|
|
||||||
[1546380000000, 40],
|
|
||||||
],
|
|
||||||
color: 'blue',
|
|
||||||
isVisible: true,
|
|
||||||
label: 'B-series',
|
|
||||||
seriesIndex: 1,
|
|
||||||
timeField: {
|
|
||||||
type: FieldType.time,
|
|
||||||
name: 'time',
|
|
||||||
values: [1546372800000, 1546376400000, 1546380000000],
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
valueField: {
|
|
||||||
type: FieldType.number,
|
|
||||||
name: 'b-series',
|
|
||||||
values: [20, 30, 40],
|
|
||||||
config: {
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
fixedColor: 'blue',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timeStep: 3600000,
|
|
||||||
yAxis: {
|
|
||||||
index: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface StoryProps extends GraphWithLegendProps {
|
|
||||||
rightAxisSeries: string;
|
|
||||||
displayMode: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WithLegend: Story<StoryProps> = ({ rightAxisSeries, displayMode, legendDisplayMode, ...args }) => {
|
|
||||||
const props: Partial<GraphWithLegendProps> = {
|
|
||||||
series: series.map((s) => {
|
|
||||||
if (
|
|
||||||
rightAxisSeries
|
|
||||||
.split(',')
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.indexOf(s.label.split('-')[0]) > -1
|
|
||||||
) {
|
|
||||||
s.yAxis = { index: 2 };
|
|
||||||
} else {
|
|
||||||
s.yAxis = { index: 1 };
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GraphWithLegend
|
|
||||||
legendDisplayMode={displayMode === 'table' ? LegendDisplayMode.Table : LegendDisplayMode.List}
|
|
||||||
{...args}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
WithLegend.args = {
|
|
||||||
rightAxisSeries: '',
|
|
||||||
displayMode: 'list',
|
|
||||||
onToggleSort: () => {},
|
|
||||||
timeRange: {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
raw: {
|
|
||||||
from: dateTime(1546372800000),
|
|
||||||
to: dateTime(1546380000000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
timeZone: 'browser',
|
|
||||||
width: 600,
|
|
||||||
height: 300,
|
|
||||||
placement: 'bottom',
|
|
||||||
};
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
// Libraries
|
|
||||||
|
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, GraphSeriesValue } from '@grafana/data';
|
|
||||||
import { LegendDisplayMode, LegendPlacement } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { CustomScrollbar } from '../../components/CustomScrollbar/CustomScrollbar';
|
|
||||||
import { VizLegend } from '../../components/VizLegend/VizLegend';
|
|
||||||
import { VizLegendItem } from '../../components/VizLegend/types';
|
|
||||||
import { useStyles2 } from '../../themes';
|
|
||||||
|
|
||||||
import { Graph, GraphProps } from './Graph';
|
|
||||||
|
|
||||||
export interface GraphWithLegendProps extends GraphProps {
|
|
||||||
legendDisplayMode: LegendDisplayMode;
|
|
||||||
legendVisibility: boolean;
|
|
||||||
placement: LegendPlacement;
|
|
||||||
hideEmpty?: boolean;
|
|
||||||
hideZero?: boolean;
|
|
||||||
sortLegendBy?: string;
|
|
||||||
sortLegendDesc?: boolean;
|
|
||||||
onSeriesToggle?: (label: string, event: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
onToggleSort: (sortBy: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => {
|
|
||||||
const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0;
|
|
||||||
const isNullOnlySeries = !data.reduce((acc, current) => acc && current[1] !== null, true);
|
|
||||||
|
|
||||||
return (hideEmpty && isNullOnlySeries) || (hideZero && isZeroOnlySeries);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GraphWithLegend = (props: GraphWithLegendProps) => {
|
|
||||||
const {
|
|
||||||
series,
|
|
||||||
timeRange,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
showBars,
|
|
||||||
showLines,
|
|
||||||
showPoints,
|
|
||||||
sortLegendBy,
|
|
||||||
sortLegendDesc,
|
|
||||||
legendDisplayMode,
|
|
||||||
legendVisibility,
|
|
||||||
placement,
|
|
||||||
onSeriesToggle,
|
|
||||||
onToggleSort,
|
|
||||||
hideEmpty,
|
|
||||||
hideZero,
|
|
||||||
isStacked,
|
|
||||||
lineWidth,
|
|
||||||
onHorizontalRegionSelected,
|
|
||||||
timeZone,
|
|
||||||
children,
|
|
||||||
ariaLabel,
|
|
||||||
} = props;
|
|
||||||
const { graphContainer, wrapper, legendContainer } = useStyles2(getGraphWithLegendStyles, props.placement);
|
|
||||||
|
|
||||||
const legendItems = series.reduce<VizLegendItem[]>((acc, s) => {
|
|
||||||
return shouldHideLegendItem(s.data, hideEmpty, hideZero)
|
|
||||||
? acc
|
|
||||||
: acc.concat([
|
|
||||||
{
|
|
||||||
label: s.label,
|
|
||||||
color: s.color || '',
|
|
||||||
disabled: !s.isVisible,
|
|
||||||
yAxis: s.yAxis.index,
|
|
||||||
getDisplayValues: () => s.info || [],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={wrapper} aria-label={ariaLabel}>
|
|
||||||
<div className={graphContainer}>
|
|
||||||
<Graph
|
|
||||||
series={series}
|
|
||||||
timeRange={timeRange}
|
|
||||||
timeZone={timeZone}
|
|
||||||
showLines={showLines}
|
|
||||||
showPoints={showPoints}
|
|
||||||
showBars={showBars}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
isStacked={isStacked}
|
|
||||||
lineWidth={lineWidth}
|
|
||||||
onHorizontalRegionSelected={onHorizontalRegionSelected}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Graph>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{legendVisibility && (
|
|
||||||
<div className={legendContainer}>
|
|
||||||
<CustomScrollbar hideHorizontalTrack>
|
|
||||||
<VizLegend
|
|
||||||
items={legendItems}
|
|
||||||
displayMode={legendDisplayMode}
|
|
||||||
placement={placement}
|
|
||||||
sortBy={sortLegendBy}
|
|
||||||
sortDesc={sortLegendDesc}
|
|
||||||
onLabelClick={(item, event) => {
|
|
||||||
if (onSeriesToggle) {
|
|
||||||
onSeriesToggle(item.label, event);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onToggleSort={onToggleSort}
|
|
||||||
/>
|
|
||||||
</CustomScrollbar>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getGraphWithLegendStyles = (_theme: GrafanaTheme2, placement: LegendPlacement) => ({
|
|
||||||
wrapper: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: placement === 'bottom' ? 'column' : 'row',
|
|
||||||
}),
|
|
||||||
graphContainer: css({
|
|
||||||
minHeight: '65%',
|
|
||||||
flexGrow: 1,
|
|
||||||
}),
|
|
||||||
legendContainer: css({
|
|
||||||
padding: '10px 0',
|
|
||||||
maxHeight: placement === 'bottom' ? '35%' : 'none',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
/** @deprecated */
|
|
||||||
export interface FlotItem<T> {
|
|
||||||
datapoint: [number, number];
|
|
||||||
dataIndex: number;
|
|
||||||
series: T;
|
|
||||||
seriesIndex: number;
|
|
||||||
pageX: number;
|
|
||||||
pageY: number;
|
|
||||||
}
|
|
||||||
|
|
@ -1,233 +0,0 @@
|
||||||
import {
|
|
||||||
toDataFrame,
|
|
||||||
FieldType,
|
|
||||||
FieldCache,
|
|
||||||
FieldColorModeId,
|
|
||||||
Field,
|
|
||||||
applyFieldOverrides,
|
|
||||||
createTheme,
|
|
||||||
DataFrame,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
import { getTheme } from '../../themes';
|
|
||||||
|
|
||||||
import { getMultiSeriesGraphHoverInfo, findHoverIndexFromData, graphTimeFormat } from './utils';
|
|
||||||
|
|
||||||
const mockResult = (
|
|
||||||
value: string,
|
|
||||||
datapointIndex: number,
|
|
||||||
seriesIndex: number,
|
|
||||||
color?: string,
|
|
||||||
label?: string,
|
|
||||||
time?: string
|
|
||||||
) => ({
|
|
||||||
value,
|
|
||||||
datapointIndex,
|
|
||||||
seriesIndex,
|
|
||||||
color,
|
|
||||||
label,
|
|
||||||
time,
|
|
||||||
});
|
|
||||||
|
|
||||||
function passThroughFieldOverrides(frame: DataFrame) {
|
|
||||||
return applyFieldOverrides({
|
|
||||||
data: [frame],
|
|
||||||
fieldConfig: {
|
|
||||||
defaults: {},
|
|
||||||
overrides: [],
|
|
||||||
},
|
|
||||||
replaceVariables: (val: string) => val,
|
|
||||||
timeZone: 'utc',
|
|
||||||
theme: createTheme(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// A and B series have the same x-axis range and the datapoints are x-axis aligned
|
|
||||||
const aSeries = passThroughFieldOverrides(
|
|
||||||
toDataFrame({
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] },
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: [10, 20, 10, 25],
|
|
||||||
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'red' } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
const bSeries = passThroughFieldOverrides(
|
|
||||||
toDataFrame({
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [10000, 20000, 30000, 80000] },
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: [30, 60, 30, 40],
|
|
||||||
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'blue' } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
// C-series has the same x-axis range as A and B but is missing the middle point
|
|
||||||
const cSeries = passThroughFieldOverrides(
|
|
||||||
toDataFrame({
|
|
||||||
fields: [
|
|
||||||
{ name: 'time', type: FieldType.time, values: [10000, 30000, 80000] },
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
values: [30, 30, 30],
|
|
||||||
config: { color: { mode: FieldColorModeId.Fixed, fixedColor: 'yellow' } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
function getFixedThemedColor(field: Field): string {
|
|
||||||
return getTheme().visualization.getColorByName(field.config.color!.fixedColor!);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Graph utils', () => {
|
|
||||||
describe('getMultiSeriesGraphHoverInfo', () => {
|
|
||||||
describe('when series datapoints are x-axis aligned', () => {
|
|
||||||
it('returns a datapoints that user hovers over', () => {
|
|
||||||
const aCache = new FieldCache(aSeries);
|
|
||||||
const aValueField = aCache.getFieldByName('value');
|
|
||||||
const aTimeField = aCache.getFieldByName('time');
|
|
||||||
const bCache = new FieldCache(bSeries);
|
|
||||||
const bValueField = bCache.getFieldByName('value');
|
|
||||||
const bTimeField = bCache.getFieldByName('time');
|
|
||||||
|
|
||||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 0);
|
|
||||||
expect(result.time).toBe('1970-01-01 00:00:10');
|
|
||||||
expect(result.results[0]).toEqual(
|
|
||||||
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
expect(result.results[1]).toEqual(
|
|
||||||
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('returns the closest datapoints before the hover position', () => {
|
|
||||||
it('when hovering right before a datapoint', () => {
|
|
||||||
const aCache = new FieldCache(aSeries);
|
|
||||||
const aValueField = aCache.getFieldByName('value');
|
|
||||||
const aTimeField = aCache.getFieldByName('time');
|
|
||||||
const bCache = new FieldCache(bSeries);
|
|
||||||
const bValueField = bCache.getFieldByName('value');
|
|
||||||
const bTimeField = bCache.getFieldByName('time');
|
|
||||||
|
|
||||||
// hovering right before middle point
|
|
||||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 19900);
|
|
||||||
expect(result.time).toBe('1970-01-01 00:00:10');
|
|
||||||
expect(result.results[0]).toEqual(
|
|
||||||
mockResult('10', 0, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
expect(result.results[1]).toEqual(
|
|
||||||
mockResult('30', 0, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('when hovering right after a datapoint', () => {
|
|
||||||
const aCache = new FieldCache(aSeries);
|
|
||||||
const aValueField = aCache.getFieldByName('value');
|
|
||||||
const aTimeField = aCache.getFieldByName('time');
|
|
||||||
const bCache = new FieldCache(bSeries);
|
|
||||||
const bValueField = bCache.getFieldByName('value');
|
|
||||||
const bTimeField = bCache.getFieldByName('time');
|
|
||||||
|
|
||||||
// hovering right after middle point
|
|
||||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, bValueField!], [aTimeField!, bTimeField!], 20100);
|
|
||||||
expect(result.time).toBe('1970-01-01 00:00:20');
|
|
||||||
expect(result.results[0]).toEqual(
|
|
||||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20')
|
|
||||||
);
|
|
||||||
expect(result.results[1]).toEqual(
|
|
||||||
mockResult('60', 1, 1, getFixedThemedColor(bValueField!), bValueField!.name, '1970-01-01 00:00:20')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when series x-axes are not aligned', () => {
|
|
||||||
// aSeries and cSeries are not aligned
|
|
||||||
// cSeries is missing a middle point
|
|
||||||
it('hovering over a middle point', () => {
|
|
||||||
const aCache = new FieldCache(aSeries);
|
|
||||||
const aValueField = aCache.getFieldByName('value');
|
|
||||||
const aTimeField = aCache.getFieldByName('time');
|
|
||||||
const cCache = new FieldCache(cSeries);
|
|
||||||
const cValueField = cCache.getFieldByName('value');
|
|
||||||
const cTimeField = cCache.getFieldByName('time');
|
|
||||||
|
|
||||||
// hovering on a middle point
|
|
||||||
// aSeries has point at that time, cSeries doesn't
|
|
||||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20000);
|
|
||||||
|
|
||||||
// we expect a time of the hovered point
|
|
||||||
expect(result.time).toBe('1970-01-01 00:00:20');
|
|
||||||
// we expect middle point from aSeries (the one we are hovering over)
|
|
||||||
expect(result.results[0]).toEqual(
|
|
||||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20')
|
|
||||||
);
|
|
||||||
// we expect closest point before hovered point from cSeries (1st point)
|
|
||||||
expect(result.results[1]).toEqual(
|
|
||||||
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hovering right after over the middle point', () => {
|
|
||||||
const aCache = new FieldCache(aSeries);
|
|
||||||
const aValueField = aCache.getFieldByName('value');
|
|
||||||
const aTimeField = aCache.getFieldByName('time');
|
|
||||||
const cCache = new FieldCache(cSeries);
|
|
||||||
const cValueField = cCache.getFieldByName('value');
|
|
||||||
const cTimeField = cCache.getFieldByName('time');
|
|
||||||
|
|
||||||
// aSeries has point at that time, cSeries doesn't
|
|
||||||
const result = getMultiSeriesGraphHoverInfo([aValueField!, cValueField!], [aTimeField!, cTimeField!], 20100);
|
|
||||||
|
|
||||||
// we expect the time of the closest point before hover
|
|
||||||
expect(result.time).toBe('1970-01-01 00:00:20');
|
|
||||||
// we expect the closest datapoint before hover from aSeries
|
|
||||||
expect(result.results[0]).toEqual(
|
|
||||||
mockResult('20', 1, 0, getFixedThemedColor(aValueField!), aValueField!.name, '1970-01-01 00:00:20')
|
|
||||||
);
|
|
||||||
// we expect the closest datapoint before hover from cSeries (1st point)
|
|
||||||
expect(result.results[1]).toEqual(
|
|
||||||
mockResult('30', 0, 1, getFixedThemedColor(cValueField!), cValueField!.name, '1970-01-01 00:00:10')
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('findHoverIndexFromData', () => {
|
|
||||||
it('returns index of the closest datapoint before hover position', () => {
|
|
||||||
const cache = new FieldCache(aSeries);
|
|
||||||
const timeField = cache.getFieldByName('time');
|
|
||||||
// hovering over 1st datapoint
|
|
||||||
expect(findHoverIndexFromData(timeField!, 0)).toBe(0);
|
|
||||||
// hovering over right before 2nd datapoint
|
|
||||||
expect(findHoverIndexFromData(timeField!, 19900)).toBe(0);
|
|
||||||
// hovering over 2nd datapoint
|
|
||||||
expect(findHoverIndexFromData(timeField!, 20000)).toBe(1);
|
|
||||||
// hovering over right before 3rd datapoint
|
|
||||||
expect(findHoverIndexFromData(timeField!, 29900)).toBe(1);
|
|
||||||
// hovering over 3rd datapoint
|
|
||||||
expect(findHoverIndexFromData(timeField!, 30000)).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('graphTimeFormat', () => {
|
|
||||||
it('graphTimeFormat', () => {
|
|
||||||
expect(graphTimeFormat(5, 1, 45 * 5 * 1000)).toBe('HH:mm:ss');
|
|
||||||
expect(graphTimeFormat(5, 1, 7200 * 5 * 1000)).toBe('HH:mm');
|
|
||||||
expect(graphTimeFormat(5, 1, 80000 * 5 * 1000)).toBe('MM/DD HH:mm');
|
|
||||||
expect(graphTimeFormat(5, 1, 2419200 * 5 * 1000)).toBe('MM/DD');
|
|
||||||
expect(graphTimeFormat(5, 1, 12419200 * 5 * 1000)).toBe('YYYY-MM');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
import {
|
|
||||||
GraphSeriesValue,
|
|
||||||
Field,
|
|
||||||
formattedValueToString,
|
|
||||||
getFieldDisplayName,
|
|
||||||
TimeZone,
|
|
||||||
dateTimeFormat,
|
|
||||||
systemDateFormats,
|
|
||||||
} from '@grafana/data';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns index of the closest datapoint BEFORE hover position
|
|
||||||
*
|
|
||||||
* @param posX
|
|
||||||
* @param series
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export const findHoverIndexFromData = (xAxisDimension: Field, xPos: number) => {
|
|
||||||
let lower = 0;
|
|
||||||
let upper = xAxisDimension.values.length - 1;
|
|
||||||
let middle;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
if (lower > upper) {
|
|
||||||
return Math.max(upper, 0);
|
|
||||||
}
|
|
||||||
middle = Math.floor((lower + upper) / 2);
|
|
||||||
const xPosition = xAxisDimension.values[middle];
|
|
||||||
|
|
||||||
if (xPosition === xPos) {
|
|
||||||
return middle;
|
|
||||||
} else if (xPosition && xPosition < xPos) {
|
|
||||||
lower = middle + 1;
|
|
||||||
} else {
|
|
||||||
upper = middle - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MultiSeriesHoverInfo {
|
|
||||||
value: string;
|
|
||||||
time: string;
|
|
||||||
datapointIndex: number;
|
|
||||||
seriesIndex: number;
|
|
||||||
label?: string;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns information about closest datapoints when hovering over a Graph
|
|
||||||
*
|
|
||||||
* @param seriesList list of series visible on the Graph
|
|
||||||
* @param pos mouse cursor position, based on jQuery.flot position
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export const getMultiSeriesGraphHoverInfo = (
|
|
||||||
// x and y axis dimensions order is aligned
|
|
||||||
yAxisDimensions: Field[],
|
|
||||||
xAxisDimensions: Field[],
|
|
||||||
/** Well, time basically */
|
|
||||||
xAxisPosition: number,
|
|
||||||
timeZone?: TimeZone
|
|
||||||
): {
|
|
||||||
results: MultiSeriesHoverInfo[];
|
|
||||||
time?: GraphSeriesValue;
|
|
||||||
} => {
|
|
||||||
let i, field, hoverIndex, hoverDistance, pointTime;
|
|
||||||
|
|
||||||
const results: MultiSeriesHoverInfo[] = [];
|
|
||||||
|
|
||||||
let minDistance, minTime;
|
|
||||||
|
|
||||||
for (i = 0; i < yAxisDimensions.length; i++) {
|
|
||||||
field = yAxisDimensions[i];
|
|
||||||
const time = xAxisDimensions[i];
|
|
||||||
hoverIndex = findHoverIndexFromData(time, xAxisPosition);
|
|
||||||
hoverDistance = xAxisPosition - time.values[hoverIndex];
|
|
||||||
pointTime = time.values[hoverIndex];
|
|
||||||
// Take the closest point before the cursor, or if it does not exist, the closest after
|
|
||||||
if (
|
|
||||||
minDistance === undefined ||
|
|
||||||
(hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
|
|
||||||
(hoverDistance < 0 && hoverDistance > minDistance)
|
|
||||||
) {
|
|
||||||
minDistance = hoverDistance;
|
|
||||||
minTime = time.display ? formattedValueToString(time.display(pointTime)) : pointTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disp = field.display!(field.values[hoverIndex]);
|
|
||||||
|
|
||||||
results.push({
|
|
||||||
value: formattedValueToString(disp),
|
|
||||||
datapointIndex: hoverIndex,
|
|
||||||
seriesIndex: i,
|
|
||||||
color: disp.color,
|
|
||||||
label: getFieldDisplayName(field),
|
|
||||||
time: time.display ? formattedValueToString(time.display(pointTime)) : pointTime,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
results,
|
|
||||||
time: minTime,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const graphTickFormatter = (epoch: number, axis: any) => {
|
|
||||||
return dateTimeFormat(epoch, {
|
|
||||||
format: axis?.options?.timeformat,
|
|
||||||
timeZone: axis?.options?.timezone,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const graphTimeFormat = (ticks: number | null, min: number | null, max: number | null): string => {
|
|
||||||
if (min && max && ticks) {
|
|
||||||
const range = max - min;
|
|
||||||
const secPerTick = range / ticks / 1000;
|
|
||||||
// Need have 10 millisecond margin on the day range
|
|
||||||
// As sometimes last 24 hour dashboard evaluates to more than 86400000
|
|
||||||
const oneDay = 86400010;
|
|
||||||
const oneYear = 31536000000;
|
|
||||||
|
|
||||||
if (secPerTick <= 10) {
|
|
||||||
return systemDateFormats.interval.millisecond;
|
|
||||||
}
|
|
||||||
if (secPerTick <= 45) {
|
|
||||||
return systemDateFormats.interval.second;
|
|
||||||
}
|
|
||||||
if (range <= oneDay) {
|
|
||||||
return systemDateFormats.interval.minute;
|
|
||||||
}
|
|
||||||
if (secPerTick <= 80000) {
|
|
||||||
return systemDateFormats.interval.hour;
|
|
||||||
}
|
|
||||||
if (range <= oneYear) {
|
|
||||||
return systemDateFormats.interval.day;
|
|
||||||
}
|
|
||||||
if (secPerTick <= 31536000) {
|
|
||||||
return systemDateFormats.interval.month;
|
|
||||||
}
|
|
||||||
return systemDateFormats.interval.year;
|
|
||||||
}
|
|
||||||
|
|
||||||
return systemDateFormats.interval.minute;
|
|
||||||
};
|
|
||||||
|
|
@ -1,275 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Subscription } from 'rxjs';
|
|
||||||
import { throttleTime } from 'rxjs/operators';
|
|
||||||
import uPlot, { AlignedData } from 'uplot';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DataFrame,
|
|
||||||
DataHoverClearEvent,
|
|
||||||
DataHoverEvent,
|
|
||||||
Field,
|
|
||||||
FieldMatcherID,
|
|
||||||
fieldMatchers,
|
|
||||||
FieldType,
|
|
||||||
LegacyGraphHoverEvent,
|
|
||||||
TimeRange,
|
|
||||||
TimeZone,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { VizLegendOptions } from '@grafana/schema';
|
|
||||||
|
|
||||||
import { PanelContext, PanelContextRoot } from '../../components/PanelChrome/PanelContext';
|
|
||||||
import { VizLayout } from '../../components/VizLayout/VizLayout';
|
|
||||||
import { UPlotChart } from '../../components/uPlot/Plot';
|
|
||||||
import { AxisProps } from '../../components/uPlot/config/UPlotAxisBuilder';
|
|
||||||
import { Renderers, UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
|
|
||||||
import { ScaleProps } from '../../components/uPlot/config/UPlotScaleBuilder';
|
|
||||||
import { findMidPointYPosition, pluginLog } from '../../components/uPlot/utils';
|
|
||||||
import { Themeable2 } from '../../types';
|
|
||||||
|
|
||||||
import { GraphNGLegendEvent, XYFieldMatchers } from './types';
|
|
||||||
import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated
|
|
||||||
* @internal -- not a public API
|
|
||||||
*/
|
|
||||||
export type PropDiffFn<T extends any = any> = (prev: T, next: T) => boolean;
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export interface GraphNGProps extends Themeable2 {
|
|
||||||
frames: DataFrame[];
|
|
||||||
structureRev?: number; // a number that will change when the frames[] structure changes
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
timeRange: TimeRange;
|
|
||||||
timeZone: TimeZone[] | TimeZone;
|
|
||||||
legend: VizLegendOptions;
|
|
||||||
fields?: XYFieldMatchers; // default will assume timeseries data
|
|
||||||
renderers?: Renderers;
|
|
||||||
tweakScale?: (opts: ScaleProps, forField: Field) => ScaleProps;
|
|
||||||
tweakAxis?: (opts: AxisProps, forField: Field) => AxisProps;
|
|
||||||
onLegendClick?: (event: GraphNGLegendEvent) => void;
|
|
||||||
children?: (builder: UPlotConfigBuilder, alignedFrame: DataFrame) => React.ReactNode;
|
|
||||||
prepConfig: (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => UPlotConfigBuilder;
|
|
||||||
propsToDiff?: Array<string | PropDiffFn>;
|
|
||||||
preparePlotFrame?: (frames: DataFrame[], dimFields: XYFieldMatchers) => DataFrame | null;
|
|
||||||
renderLegend: (config: UPlotConfigBuilder) => React.ReactElement | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* needed for propsToDiff to re-init the plot & config
|
|
||||||
* this is a generic approach to plot re-init, without having to specify which panel-level options
|
|
||||||
* should cause invalidation. we can drop this in favor of something like panelOptionsRev that gets passed in
|
|
||||||
* similar to structureRev. then we can drop propsToDiff entirely.
|
|
||||||
*/
|
|
||||||
options?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | PropDiffFn> = []) {
|
|
||||||
for (const propName of propsToDiff) {
|
|
||||||
if (typeof propName === 'function') {
|
|
||||||
if (!propName(prevProps, nextProps)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (nextProps[propName] !== prevProps[propName]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal -- not a public API
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export interface GraphNGState {
|
|
||||||
alignedFrame: DataFrame;
|
|
||||||
alignedData?: AlignedData;
|
|
||||||
config?: UPlotConfigBuilder;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Time as X" core component, expects ascending x
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
export class GraphNG extends Component<GraphNGProps, GraphNGState> {
|
|
||||||
static contextType = PanelContextRoot;
|
|
||||||
panelContext: PanelContext = {} as PanelContext;
|
|
||||||
private plotInstance: React.RefObject<uPlot>;
|
|
||||||
|
|
||||||
private subscription = new Subscription();
|
|
||||||
|
|
||||||
constructor(props: GraphNGProps) {
|
|
||||||
super(props);
|
|
||||||
let state = this.prepState(props);
|
|
||||||
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
|
|
||||||
this.state = state;
|
|
||||||
this.plotInstance = React.createRef();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTimeRange = () => this.props.timeRange;
|
|
||||||
|
|
||||||
prepState(props: GraphNGProps, withConfig = true) {
|
|
||||||
let state: GraphNGState = null as any;
|
|
||||||
|
|
||||||
const { frames, fields, preparePlotFrame } = props;
|
|
||||||
|
|
||||||
const preparePlotFrameFn = preparePlotFrame || defaultPreparePlotFrame;
|
|
||||||
|
|
||||||
const alignedFrame = preparePlotFrameFn(
|
|
||||||
frames,
|
|
||||||
fields || {
|
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
y: fieldMatchers.get(FieldMatcherID.byTypes).get(new Set([FieldType.number, FieldType.enum])),
|
|
||||||
},
|
|
||||||
props.timeRange
|
|
||||||
);
|
|
||||||
pluginLog('GraphNG', false, 'data aligned', alignedFrame);
|
|
||||||
|
|
||||||
if (alignedFrame) {
|
|
||||||
let config = this.state?.config;
|
|
||||||
|
|
||||||
if (withConfig) {
|
|
||||||
config = props.prepConfig(alignedFrame, this.props.frames, this.getTimeRange);
|
|
||||||
pluginLog('GraphNG', false, 'config prepared', config);
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
alignedFrame,
|
|
||||||
config,
|
|
||||||
};
|
|
||||||
|
|
||||||
pluginLog('GraphNG', false, 'data prepared', state.alignedData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCursorUpdate(evt: DataHoverEvent | LegacyGraphHoverEvent) {
|
|
||||||
const time = evt.payload?.point?.time;
|
|
||||||
const u = this.plotInstance.current;
|
|
||||||
if (u && time) {
|
|
||||||
// Try finding left position on time axis
|
|
||||||
const left = u.valToPos(time, 'x');
|
|
||||||
let top;
|
|
||||||
if (left) {
|
|
||||||
// find midpoint between points at current idx
|
|
||||||
top = findMidPointYPosition(u, u.posToIdx(left));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!top || !left) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
u.setCursor({
|
|
||||||
left,
|
|
||||||
top,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.panelContext = this.context as PanelContext;
|
|
||||||
const { eventBus } = this.panelContext;
|
|
||||||
|
|
||||||
this.subscription.add(
|
|
||||||
eventBus
|
|
||||||
.getStream(DataHoverEvent)
|
|
||||||
.pipe(throttleTime(50))
|
|
||||||
.subscribe({
|
|
||||||
next: (evt) => {
|
|
||||||
if (eventBus === evt.origin) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleCursorUpdate(evt);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Legacy events (from flot graph)
|
|
||||||
this.subscription.add(
|
|
||||||
eventBus
|
|
||||||
.getStream(LegacyGraphHoverEvent)
|
|
||||||
.pipe(throttleTime(50))
|
|
||||||
.subscribe({
|
|
||||||
next: (evt) => this.handleCursorUpdate(evt),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.subscription.add(
|
|
||||||
eventBus
|
|
||||||
.getStream(DataHoverClearEvent)
|
|
||||||
.pipe(throttleTime(50))
|
|
||||||
.subscribe({
|
|
||||||
next: () => {
|
|
||||||
const u = this.plotInstance?.current;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
if (u && !u.cursor._lock) {
|
|
||||||
u.setCursor({
|
|
||||||
left: -10,
|
|
||||||
top: -10,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: GraphNGProps) {
|
|
||||||
const { frames, structureRev, timeZone, propsToDiff } = this.props;
|
|
||||||
|
|
||||||
const propsChanged = !sameProps(prevProps, this.props, propsToDiff);
|
|
||||||
|
|
||||||
if (frames !== prevProps.frames || propsChanged || timeZone !== prevProps.timeZone) {
|
|
||||||
let newState = this.prepState(this.props, false);
|
|
||||||
|
|
||||||
if (newState) {
|
|
||||||
const shouldReconfig =
|
|
||||||
this.state.config === undefined ||
|
|
||||||
timeZone !== prevProps.timeZone ||
|
|
||||||
structureRev !== prevProps.structureRev ||
|
|
||||||
!structureRev ||
|
|
||||||
propsChanged;
|
|
||||||
|
|
||||||
if (shouldReconfig) {
|
|
||||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
|
||||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
|
||||||
}
|
|
||||||
|
|
||||||
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { width, height, children, renderLegend } = this.props;
|
|
||||||
const { config, alignedFrame, alignedData } = this.state;
|
|
||||||
|
|
||||||
if (!config) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VizLayout width={width} height={height} legend={renderLegend(config)}>
|
|
||||||
{(vizWidth: number, vizHeight: number) => (
|
|
||||||
<UPlotChart
|
|
||||||
config={config}
|
|
||||||
data={alignedData!}
|
|
||||||
width={vizWidth}
|
|
||||||
height={vizHeight}
|
|
||||||
plotRef={(u) => ((this.plotInstance as React.MutableRefObject<uPlot>).current = u)}
|
|
||||||
>
|
|
||||||
{children ? children(config, alignedFrame) : null}
|
|
||||||
</UPlotChart>
|
|
||||||
)}
|
|
||||||
</VizLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`GraphNG utils preparePlotConfigBuilder 1`] = `
|
|
||||||
{
|
|
||||||
"axes": [
|
|
||||||
{
|
|
||||||
"filter": undefined,
|
|
||||||
"font": "12px "Inter", "Helvetica", "Arial", sans-serif",
|
|
||||||
"gap": 5,
|
|
||||||
"grid": {
|
|
||||||
"show": true,
|
|
||||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
|
||||||
"width": 1,
|
|
||||||
},
|
|
||||||
"incrs": undefined,
|
|
||||||
"labelGap": 0,
|
|
||||||
"rotate": undefined,
|
|
||||||
"scale": "x",
|
|
||||||
"show": true,
|
|
||||||
"side": 2,
|
|
||||||
"size": [Function],
|
|
||||||
"space": [Function],
|
|
||||||
"splits": undefined,
|
|
||||||
"stroke": "rgb(204, 204, 220)",
|
|
||||||
"ticks": {
|
|
||||||
"show": true,
|
|
||||||
"size": 4,
|
|
||||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
|
||||||
"width": 1,
|
|
||||||
},
|
|
||||||
"timeZone": "utc",
|
|
||||||
"values": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"filter": undefined,
|
|
||||||
"font": "12px "Inter", "Helvetica", "Arial", sans-serif",
|
|
||||||
"gap": 5,
|
|
||||||
"grid": {
|
|
||||||
"show": true,
|
|
||||||
"stroke": "rgba(240, 250, 255, 0.09)",
|
|
||||||
"width": 1,
|
|
||||||
},
|
|
||||||
"incrs": undefined,
|
|
||||||
"labelGap": 0,
|
|
||||||
"rotate": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"side": 3,
|
|
||||||
"size": [Function],
|
|
||||||
"space": [Function],
|
|
||||||
"splits": undefined,
|
|
||||||
"stroke": "rgb(204, 204, 220)",
|
|
||||||
"ticks": {
|
|
||||||
"show": false,
|
|
||||||
"size": 4,
|
|
||||||
"stroke": "rgb(204, 204, 220)",
|
|
||||||
"width": 1,
|
|
||||||
},
|
|
||||||
"timeZone": undefined,
|
|
||||||
"values": [Function],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"cursor": {
|
|
||||||
"dataIdx": [Function],
|
|
||||||
"drag": {
|
|
||||||
"setScale": false,
|
|
||||||
},
|
|
||||||
"focus": {
|
|
||||||
"prox": 30,
|
|
||||||
},
|
|
||||||
"points": {
|
|
||||||
"fill": [Function],
|
|
||||||
"size": [Function],
|
|
||||||
"stroke": [Function],
|
|
||||||
"width": [Function],
|
|
||||||
},
|
|
||||||
"sync": {
|
|
||||||
"filters": {
|
|
||||||
"pub": [Function],
|
|
||||||
},
|
|
||||||
"key": "__global_",
|
|
||||||
"scales": [
|
|
||||||
"x",
|
|
||||||
"__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"focus": {
|
|
||||||
"alpha": 1,
|
|
||||||
},
|
|
||||||
"hooks": {},
|
|
||||||
"legend": {
|
|
||||||
"show": false,
|
|
||||||
},
|
|
||||||
"mode": 1,
|
|
||||||
"ms": 1,
|
|
||||||
"padding": [
|
|
||||||
[Function],
|
|
||||||
[Function],
|
|
||||||
[Function],
|
|
||||||
[Function],
|
|
||||||
],
|
|
||||||
"scales": {
|
|
||||||
"__fixed/na-na/na-na/auto/linear/na/number": {
|
|
||||||
"asinh": undefined,
|
|
||||||
"auto": true,
|
|
||||||
"dir": 1,
|
|
||||||
"distr": 1,
|
|
||||||
"log": undefined,
|
|
||||||
"ori": 1,
|
|
||||||
"range": [Function],
|
|
||||||
"time": undefined,
|
|
||||||
},
|
|
||||||
"x": {
|
|
||||||
"auto": false,
|
|
||||||
"dir": 1,
|
|
||||||
"ori": 0,
|
|
||||||
"range": [Function],
|
|
||||||
"time": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"select": undefined,
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"value": [Function],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dash": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
"facets": undefined,
|
|
||||||
"fill": [Function],
|
|
||||||
"paths": [Function],
|
|
||||||
"points": {
|
|
||||||
"fill": "#ff0000",
|
|
||||||
"filter": [Function],
|
|
||||||
"show": true,
|
|
||||||
"size": undefined,
|
|
||||||
"stroke": "#ff0000",
|
|
||||||
},
|
|
||||||
"pxAlign": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"spanGaps": false,
|
|
||||||
"stroke": "#ff0000",
|
|
||||||
"value": [Function],
|
|
||||||
"width": 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dash": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
"facets": undefined,
|
|
||||||
"fill": [Function],
|
|
||||||
"paths": [Function],
|
|
||||||
"points": {
|
|
||||||
"fill": [Function],
|
|
||||||
"filter": [Function],
|
|
||||||
"show": true,
|
|
||||||
"size": undefined,
|
|
||||||
"stroke": [Function],
|
|
||||||
},
|
|
||||||
"pxAlign": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"spanGaps": false,
|
|
||||||
"stroke": [Function],
|
|
||||||
"value": [Function],
|
|
||||||
"width": 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dash": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
"facets": undefined,
|
|
||||||
"fill": [Function],
|
|
||||||
"paths": [Function],
|
|
||||||
"points": {
|
|
||||||
"fill": "#ff0000",
|
|
||||||
"filter": [Function],
|
|
||||||
"show": true,
|
|
||||||
"size": undefined,
|
|
||||||
"stroke": "#ff0000",
|
|
||||||
},
|
|
||||||
"pxAlign": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"spanGaps": false,
|
|
||||||
"stroke": "#ff0000",
|
|
||||||
"value": [Function],
|
|
||||||
"width": 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dash": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
"facets": undefined,
|
|
||||||
"fill": [Function],
|
|
||||||
"paths": [Function],
|
|
||||||
"points": {
|
|
||||||
"fill": [Function],
|
|
||||||
"filter": [Function],
|
|
||||||
"show": true,
|
|
||||||
"size": undefined,
|
|
||||||
"stroke": [Function],
|
|
||||||
},
|
|
||||||
"pxAlign": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"spanGaps": false,
|
|
||||||
"stroke": [Function],
|
|
||||||
"value": [Function],
|
|
||||||
"width": 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dash": [
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
],
|
|
||||||
"facets": undefined,
|
|
||||||
"fill": [Function],
|
|
||||||
"paths": [Function],
|
|
||||||
"points": {
|
|
||||||
"fill": [Function],
|
|
||||||
"filter": [Function],
|
|
||||||
"show": true,
|
|
||||||
"size": undefined,
|
|
||||||
"stroke": [Function],
|
|
||||||
},
|
|
||||||
"pxAlign": undefined,
|
|
||||||
"scale": "__fixed/na-na/na-na/auto/linear/na/number",
|
|
||||||
"show": true,
|
|
||||||
"spanGaps": false,
|
|
||||||
"stroke": [Function],
|
|
||||||
"value": [Function],
|
|
||||||
"width": 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"tzDate": [Function],
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import React, { useCallback, useContext } from 'react';
|
|
||||||
|
|
||||||
import { DataFrame, DataFrameFieldIndex, Field } from '@grafana/data';
|
|
||||||
|
|
||||||
import { XYFieldMatchers } from './types';
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
interface GraphNGContextType {
|
|
||||||
mapSeriesIndexToDataFrameFieldIndex: (index: number) => DataFrameFieldIndex;
|
|
||||||
dimFields: XYFieldMatchers;
|
|
||||||
data: DataFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const GraphNGContext = React.createContext<GraphNGContextType>({} as GraphNGContextType);
|
|
||||||
|
|
||||||
/** @deprecated */
|
|
||||||
export const useGraphNGContext = () => {
|
|
||||||
const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext<GraphNGContextType>(GraphNGContext);
|
|
||||||
|
|
||||||
const getXAxisField = useCallback(() => {
|
|
||||||
const xFieldMatcher = dimFields.x;
|
|
||||||
let xField: Field | null = null;
|
|
||||||
|
|
||||||
for (let j = 0; j < data.fields.length; j++) {
|
|
||||||
if (xFieldMatcher(data.fields[j], data, [data])) {
|
|
||||||
xField = data.fields[j];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return xField;
|
|
||||||
}, [data, dimFields]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
dimFields,
|
|
||||||
mapSeriesIndexToDataFrameFieldIndex,
|
|
||||||
getXAxisField,
|
|
||||||
alignedData: data,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
@ -1,522 +0,0 @@
|
||||||
import {
|
|
||||||
createTheme,
|
|
||||||
DashboardCursorSync,
|
|
||||||
DataFrame,
|
|
||||||
DefaultTimeZone,
|
|
||||||
EventBusSrv,
|
|
||||||
FieldColorModeId,
|
|
||||||
FieldConfig,
|
|
||||||
FieldMatcherID,
|
|
||||||
fieldMatchers,
|
|
||||||
FieldType,
|
|
||||||
getDefaultTimeRange,
|
|
||||||
MutableDataFrame,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import {
|
|
||||||
BarAlignment,
|
|
||||||
GraphDrawStyle,
|
|
||||||
GraphFieldConfig,
|
|
||||||
GraphGradientMode,
|
|
||||||
LineInterpolation,
|
|
||||||
VisibilityMode,
|
|
||||||
StackingMode,
|
|
||||||
} from '@grafana/schema';
|
|
||||||
|
|
||||||
import { preparePlotConfigBuilder } from '../TimeSeries/utils';
|
|
||||||
|
|
||||||
import { preparePlotFrame } from './utils';
|
|
||||||
|
|
||||||
function mockDataFrame() {
|
|
||||||
const df1 = new MutableDataFrame({
|
|
||||||
refId: 'A',
|
|
||||||
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 3] }],
|
|
||||||
});
|
|
||||||
const df2 = new MutableDataFrame({
|
|
||||||
refId: 'B',
|
|
||||||
fields: [{ name: 'ts', type: FieldType.time, values: [1, 2, 4] }],
|
|
||||||
});
|
|
||||||
|
|
||||||
const f1Config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
displayName: 'Metric 1',
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
},
|
|
||||||
decimals: 2,
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Line,
|
|
||||||
gradientMode: GraphGradientMode.Opacity,
|
|
||||||
lineColor: '#ff0000',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineInterpolation: LineInterpolation.Linear,
|
|
||||||
lineStyle: {
|
|
||||||
fill: 'dash',
|
|
||||||
dash: [1, 2],
|
|
||||||
},
|
|
||||||
spanNulls: false,
|
|
||||||
fillColor: '#ff0000',
|
|
||||||
fillOpacity: 0.1,
|
|
||||||
showPoints: VisibilityMode.Always,
|
|
||||||
stacking: {
|
|
||||||
group: 'A',
|
|
||||||
mode: StackingMode.Normal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const f2Config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
displayName: 'Metric 2',
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
},
|
|
||||||
decimals: 2,
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
gradientMode: GraphGradientMode.Hue,
|
|
||||||
lineColor: '#ff0000',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineInterpolation: LineInterpolation.Linear,
|
|
||||||
lineStyle: {
|
|
||||||
fill: 'dash',
|
|
||||||
dash: [1, 2],
|
|
||||||
},
|
|
||||||
barAlignment: BarAlignment.Before,
|
|
||||||
fillColor: '#ff0000',
|
|
||||||
fillOpacity: 0.1,
|
|
||||||
showPoints: VisibilityMode.Always,
|
|
||||||
stacking: {
|
|
||||||
group: 'A',
|
|
||||||
mode: StackingMode.Normal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const f3Config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
displayName: 'Metric 3',
|
|
||||||
decimals: 2,
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
},
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Line,
|
|
||||||
gradientMode: GraphGradientMode.Opacity,
|
|
||||||
lineColor: '#ff0000',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineInterpolation: LineInterpolation.Linear,
|
|
||||||
lineStyle: {
|
|
||||||
fill: 'dash',
|
|
||||||
dash: [1, 2],
|
|
||||||
},
|
|
||||||
spanNulls: false,
|
|
||||||
fillColor: '#ff0000',
|
|
||||||
fillOpacity: 0.1,
|
|
||||||
showPoints: VisibilityMode.Always,
|
|
||||||
stacking: {
|
|
||||||
group: 'B',
|
|
||||||
mode: StackingMode.Normal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const f4Config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
displayName: 'Metric 4',
|
|
||||||
decimals: 2,
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
},
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
gradientMode: GraphGradientMode.Hue,
|
|
||||||
lineColor: '#ff0000',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineInterpolation: LineInterpolation.Linear,
|
|
||||||
lineStyle: {
|
|
||||||
fill: 'dash',
|
|
||||||
dash: [1, 2],
|
|
||||||
},
|
|
||||||
barAlignment: BarAlignment.Before,
|
|
||||||
fillColor: '#ff0000',
|
|
||||||
fillOpacity: 0.1,
|
|
||||||
showPoints: VisibilityMode.Always,
|
|
||||||
stacking: {
|
|
||||||
group: 'B',
|
|
||||||
mode: StackingMode.Normal,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const f5Config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
displayName: 'Metric 4',
|
|
||||||
decimals: 2,
|
|
||||||
color: {
|
|
||||||
mode: FieldColorModeId.Fixed,
|
|
||||||
},
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
gradientMode: GraphGradientMode.Hue,
|
|
||||||
lineColor: '#ff0000',
|
|
||||||
lineWidth: 2,
|
|
||||||
lineInterpolation: LineInterpolation.Linear,
|
|
||||||
lineStyle: {
|
|
||||||
fill: 'dash',
|
|
||||||
dash: [1, 2],
|
|
||||||
},
|
|
||||||
barAlignment: BarAlignment.Before,
|
|
||||||
fillColor: '#ff0000',
|
|
||||||
fillOpacity: 0.1,
|
|
||||||
showPoints: VisibilityMode.Always,
|
|
||||||
stacking: {
|
|
||||||
group: 'B',
|
|
||||||
mode: StackingMode.None,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
df1.addField({
|
|
||||||
name: 'metric1',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: f1Config,
|
|
||||||
});
|
|
||||||
|
|
||||||
df2.addField({
|
|
||||||
name: 'metric2',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: f2Config,
|
|
||||||
});
|
|
||||||
df2.addField({
|
|
||||||
name: 'metric3',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: f3Config,
|
|
||||||
});
|
|
||||||
df2.addField({
|
|
||||||
name: 'metric4',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: f4Config,
|
|
||||||
});
|
|
||||||
df2.addField({
|
|
||||||
name: 'metric5',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: f5Config,
|
|
||||||
});
|
|
||||||
|
|
||||||
return preparePlotFrame([df1, df2], {
|
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
jest.mock('@grafana/data', () => ({
|
|
||||||
...jest.requireActual('@grafana/data'),
|
|
||||||
DefaultTimeZone: 'utc',
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('GraphNG utils', () => {
|
|
||||||
test('preparePlotConfigBuilder', () => {
|
|
||||||
const frame = mockDataFrame();
|
|
||||||
const result = preparePlotConfigBuilder({
|
|
||||||
frame: frame!,
|
|
||||||
theme: createTheme(),
|
|
||||||
timeZones: [DefaultTimeZone],
|
|
||||||
getTimeRange: getDefaultTimeRange,
|
|
||||||
eventBus: new EventBusSrv(),
|
|
||||||
sync: () => DashboardCursorSync.Tooltip,
|
|
||||||
allFrames: [frame!],
|
|
||||||
}).getConfig();
|
|
||||||
expect(result).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('preparePlotFrame appends min bar spaced nulls when > 1 bar series', () => {
|
|
||||||
const df1: DataFrame = {
|
|
||||||
name: 'A',
|
|
||||||
length: 5,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'time',
|
|
||||||
type: FieldType.time,
|
|
||||||
config: {},
|
|
||||||
values: [1, 2, 4, 6, 100], // should find smallest delta === 1 from here
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: [1, 1, 1, 1, 1],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const df2: DataFrame = {
|
|
||||||
name: 'B',
|
|
||||||
length: 5,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'time',
|
|
||||||
type: FieldType.time,
|
|
||||||
config: {},
|
|
||||||
values: [30, 40, 50, 90, 100], // should be appended with two smallest-delta increments
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: [2, 2, 2, 2, 2], // bar series should be appended with nulls
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Line,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: [3, 3, 3, 3, 3], // line series should be appended with undefineds
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const df3: DataFrame = {
|
|
||||||
name: 'C',
|
|
||||||
length: 2,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'time',
|
|
||||||
type: FieldType.time,
|
|
||||||
config: {},
|
|
||||||
values: [1, 1.1], // should not trip up on smaller deltas of non-bars
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Line,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: [4, 4],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'value',
|
|
||||||
type: FieldType.number,
|
|
||||||
config: {
|
|
||||||
custom: {
|
|
||||||
drawStyle: GraphDrawStyle.Bars,
|
|
||||||
hideFrom: {
|
|
||||||
viz: true, // should ignore hidden bar series
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
values: [4, 4],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let aligndFrame = preparePlotFrame([df1, df2, df3], {
|
|
||||||
x: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}),
|
|
||||||
y: fieldMatchers.get(FieldMatcherID.numeric).get({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(aligndFrame).toMatchInlineSnapshot(`
|
|
||||||
{
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"config": {},
|
|
||||||
"name": "time",
|
|
||||||
"state": {
|
|
||||||
"nullThresholdApplied": true,
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 0,
|
|
||||||
"frameIndex": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "time",
|
|
||||||
"values": [
|
|
||||||
1,
|
|
||||||
1.1,
|
|
||||||
2,
|
|
||||||
4,
|
|
||||||
6,
|
|
||||||
30,
|
|
||||||
40,
|
|
||||||
50,
|
|
||||||
90,
|
|
||||||
100,
|
|
||||||
101,
|
|
||||||
102,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"custom": {
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"spanNulls": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"name": "A",
|
|
||||||
},
|
|
||||||
"name": "value",
|
|
||||||
"state": {
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 1,
|
|
||||||
"frameIndex": 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "number",
|
|
||||||
"values": [
|
|
||||||
1,
|
|
||||||
undefined,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
1,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
1,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"custom": {
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"spanNulls": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"name": "B",
|
|
||||||
},
|
|
||||||
"name": "value",
|
|
||||||
"state": {
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 1,
|
|
||||||
"frameIndex": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "number",
|
|
||||||
"values": [
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
2,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"custom": {
|
|
||||||
"drawStyle": "line",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"name": "B",
|
|
||||||
},
|
|
||||||
"name": "value",
|
|
||||||
"state": {
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 2,
|
|
||||||
"frameIndex": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "number",
|
|
||||||
"values": [
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
3,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"custom": {
|
|
||||||
"drawStyle": "line",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"name": "C",
|
|
||||||
},
|
|
||||||
"name": "value",
|
|
||||||
"state": {
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 1,
|
|
||||||
"frameIndex": 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "number",
|
|
||||||
"values": [
|
|
||||||
4,
|
|
||||||
4,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"config": {
|
|
||||||
"custom": {
|
|
||||||
"drawStyle": "bars",
|
|
||||||
"hideFrom": {
|
|
||||||
"viz": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"labels": {
|
|
||||||
"name": "C",
|
|
||||||
},
|
|
||||||
"name": "value",
|
|
||||||
"state": {
|
|
||||||
"origin": {
|
|
||||||
"fieldIndex": 2,
|
|
||||||
"frameIndex": 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "number",
|
|
||||||
"values": [
|
|
||||||
4,
|
|
||||||
4,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"length": 12,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1 +1,4 @@
|
||||||
Items in this folder are all deprecated and will be removed in the future
|
Items in this folder are all deprecated and will be removed in the future
|
||||||
|
|
||||||
|
NOTE: GraphNG is include, but not exported. It contains some complex function that are
|
||||||
|
used in the uPlot helper bundles, but also duplicated in grafana core
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
import { DataFrame, TimeRange } from '@grafana/data';
|
|
||||||
|
|
||||||
import { PanelContextRoot } from '../../components/PanelChrome/PanelContext';
|
|
||||||
import { hasVisibleLegendSeries, PlotLegend } from '../../components/uPlot/PlotLegend';
|
|
||||||
import { UPlotConfigBuilder } from '../../components/uPlot/config/UPlotConfigBuilder';
|
|
||||||
import { withTheme2 } from '../../themes/ThemeContext';
|
|
||||||
import { GraphNG, GraphNGProps, PropDiffFn } from '../GraphNG/GraphNG';
|
|
||||||
|
|
||||||
import { preparePlotConfigBuilder } from './utils';
|
|
||||||
|
|
||||||
const propsToDiff: Array<string | PropDiffFn> = ['legend', 'options', 'theme'];
|
|
||||||
|
|
||||||
type TimeSeriesProps = Omit<GraphNGProps, 'prepConfig' | 'propsToDiff' | 'renderLegend'>;
|
|
||||||
|
|
||||||
export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
|
|
||||||
static contextType = PanelContextRoot;
|
|
||||||
declare context: React.ContextType<typeof PanelContextRoot>;
|
|
||||||
|
|
||||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
|
||||||
const { eventBus, eventsScope, sync } = this.context;
|
|
||||||
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
|
|
||||||
|
|
||||||
return preparePlotConfigBuilder({
|
|
||||||
frame: alignedFrame,
|
|
||||||
theme,
|
|
||||||
timeZones: Array.isArray(timeZone) ? timeZone : [timeZone],
|
|
||||||
getTimeRange,
|
|
||||||
eventBus,
|
|
||||||
sync,
|
|
||||||
allFrames,
|
|
||||||
renderers,
|
|
||||||
tweakScale,
|
|
||||||
tweakAxis,
|
|
||||||
eventsScope,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderLegend = (config: UPlotConfigBuilder) => {
|
|
||||||
const { legend, frames } = this.props;
|
|
||||||
|
|
||||||
if (!config || (legend && !legend.showLegend) || !hasVisibleLegendSeries(config, frames)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PlotLegend data={frames} config={config} {...legend} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<GraphNG
|
|
||||||
{...this.props}
|
|
||||||
prepConfig={this.prepConfig}
|
|
||||||
propsToDiff={propsToDiff}
|
|
||||||
renderLegend={this.renderLegend}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TimeSeries = withTheme2(UnthemedTimeSeries);
|
|
||||||
TimeSeries.displayName = 'TimeSeries';
|
|
||||||
|
|
@ -1,274 +0,0 @@
|
||||||
import { EventBus, FieldType } from '@grafana/data';
|
|
||||||
import { getTheme } from '@grafana/ui';
|
|
||||||
|
|
||||||
import { preparePlotConfigBuilder } from './utils';
|
|
||||||
|
|
||||||
describe('when fill below to option is used', () => {
|
|
||||||
let eventBus: EventBus;
|
|
||||||
// eslint-disable-next-line
|
|
||||||
let renderers: any[];
|
|
||||||
// eslint-disable-next-line
|
|
||||||
let tests: any;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
eventBus = {
|
|
||||||
publish: jest.fn(),
|
|
||||||
getStream: jest.fn(),
|
|
||||||
subscribe: jest.fn(),
|
|
||||||
removeAllListeners: jest.fn(),
|
|
||||||
newScopedBus: jest.fn(),
|
|
||||||
};
|
|
||||||
renderers = [];
|
|
||||||
|
|
||||||
tests = [
|
|
||||||
{
|
|
||||||
alignedFrame: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'Time',
|
|
||||||
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 },
|
|
||||||
values: [1, 2, 3],
|
|
||||||
name: 'Value',
|
|
||||||
state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { displayNameFromDS: 'Test2', min: 0, max: 100 },
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'Value',
|
|
||||||
state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 3,
|
|
||||||
},
|
|
||||||
allFrames: [
|
|
||||||
{
|
|
||||||
name: 'Test1',
|
|
||||||
refId: 'A',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'Time',
|
|
||||||
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 0 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { displayNameFromDS: 'Test1', custom: { fillBelowTo: 'Test2' }, min: 0, max: 100 },
|
|
||||||
values: [1, 2, 3],
|
|
||||||
name: 'Value',
|
|
||||||
state: { multipleFrames: true, displayName: 'Test1', origin: { fieldIndex: 1, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Test2',
|
|
||||||
refId: 'B',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'Time',
|
|
||||||
state: { multipleFrames: true, displayName: 'Time', origin: { fieldIndex: 0, frameIndex: 1 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { displayNameFromDS: 'Test2', min: 0, max: 100 },
|
|
||||||
values: [1, 2, 3],
|
|
||||||
name: 'Value',
|
|
||||||
state: { multipleFrames: true, displayName: 'Test2', origin: { fieldIndex: 1, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expectedResult: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
alignedFrame: {
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'time',
|
|
||||||
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { custom: { fillBelowTo: 'below_value1' } },
|
|
||||||
values: [1, 2, 3],
|
|
||||||
name: 'value1',
|
|
||||||
state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { custom: { fillBelowTo: 'below_value2' } },
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'value2',
|
|
||||||
state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'below_value1',
|
|
||||||
state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'below_value2',
|
|
||||||
state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 5,
|
|
||||||
},
|
|
||||||
allFrames: [
|
|
||||||
{
|
|
||||||
refId: 'A',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'time',
|
|
||||||
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 0 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { custom: { fillBelowTo: 'below_value1' } },
|
|
||||||
values: [1, 2, 3],
|
|
||||||
name: 'value1',
|
|
||||||
state: { multipleFrames: true, displayName: 'value1', origin: { fieldIndex: 1, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: { custom: { fillBelowTo: 'below_value2' } },
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'value2',
|
|
||||||
state: { multipleFrames: true, displayName: 'value2', origin: { fieldIndex: 2, frameIndex: 0 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refId: 'B',
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [1667406900000, 1667407170000, 1667407185000],
|
|
||||||
name: 'time',
|
|
||||||
state: { multipleFrames: true, displayName: 'time', origin: { fieldIndex: 0, frameIndex: 1 } },
|
|
||||||
type: FieldType.time,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'below_value1',
|
|
||||||
state: { multipleFrames: true, displayName: 'below_value1', origin: { fieldIndex: 1, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
config: {},
|
|
||||||
values: [4, 5, 6],
|
|
||||||
name: 'below_value2',
|
|
||||||
state: { multipleFrames: true, displayName: 'below_value2', origin: { fieldIndex: 2, frameIndex: 1 } },
|
|
||||||
type: FieldType.number,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
length: 3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
expectedResult: 2,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should verify if fill below to is set then builder bands are set', () => {
|
|
||||||
for (const test of tests) {
|
|
||||||
const builder = preparePlotConfigBuilder({
|
|
||||||
frame: test.alignedFrame,
|
|
||||||
//@ts-ignore
|
|
||||||
theme: getTheme(),
|
|
||||||
timeZones: ['browser'],
|
|
||||||
getTimeRange: jest.fn(),
|
|
||||||
eventBus,
|
|
||||||
sync: jest.fn(),
|
|
||||||
allFrames: test.allFrames,
|
|
||||||
renderers,
|
|
||||||
});
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
expect(builder.bands.length).toBe(test.expectedResult);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should verify if fill below to is not set then builder bands are empty', () => {
|
|
||||||
tests[0].alignedFrame.fields[1].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[0].allFrames[0].fields[1].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[1].alignedFrame.fields[1].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[1].alignedFrame.fields[2].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[1].allFrames[0].fields[1].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[1].allFrames[0].fields[2].config.custom.fillBelowTo = undefined;
|
|
||||||
tests[0].expectedResult = 0;
|
|
||||||
tests[1].expectedResult = 0;
|
|
||||||
|
|
||||||
for (const test of tests) {
|
|
||||||
const builder = preparePlotConfigBuilder({
|
|
||||||
frame: test.alignedFrame,
|
|
||||||
//@ts-ignore
|
|
||||||
theme: getTheme(),
|
|
||||||
timeZones: ['browser'],
|
|
||||||
getTimeRange: jest.fn(),
|
|
||||||
eventBus,
|
|
||||||
sync: jest.fn(),
|
|
||||||
allFrames: test.allFrames,
|
|
||||||
renderers,
|
|
||||||
});
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
expect(builder.bands.length).toBe(test.expectedResult);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should verify if fill below to is set and field name is overriden then builder bands are set', () => {
|
|
||||||
tests[0].alignedFrame.fields[2].config.displayName = 'newName';
|
|
||||||
tests[0].alignedFrame.fields[2].state.displayName = 'newName';
|
|
||||||
tests[0].allFrames[1].fields[1].config.displayName = 'newName';
|
|
||||||
tests[0].allFrames[1].fields[1].state.displayName = 'newName';
|
|
||||||
|
|
||||||
tests[1].alignedFrame.fields[3].config.displayName = 'newName';
|
|
||||||
tests[1].alignedFrame.fields[3].state.displayName = 'newName';
|
|
||||||
tests[1].allFrames[1].fields[1].config.displayName = 'newName';
|
|
||||||
tests[1].allFrames[1].fields[1].state.displayName = 'newName';
|
|
||||||
|
|
||||||
for (const test of tests) {
|
|
||||||
const builder = preparePlotConfigBuilder({
|
|
||||||
frame: test.alignedFrame,
|
|
||||||
//@ts-ignore
|
|
||||||
theme: getTheme(),
|
|
||||||
timeZones: ['browser'],
|
|
||||||
getTimeRange: jest.fn(),
|
|
||||||
eventBus,
|
|
||||||
sync: jest.fn(),
|
|
||||||
allFrames: test.allFrames,
|
|
||||||
renderers,
|
|
||||||
});
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
expect(builder.bands.length).toBe(test.expectedResult);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,668 +0,0 @@
|
||||||
import { isNumber } from 'lodash';
|
|
||||||
import uPlot from 'uplot';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DashboardCursorSync,
|
|
||||||
DataFrame,
|
|
||||||
DataHoverClearEvent,
|
|
||||||
DataHoverEvent,
|
|
||||||
DataHoverPayload,
|
|
||||||
FieldConfig,
|
|
||||||
FieldType,
|
|
||||||
formattedValueToString,
|
|
||||||
getFieldColorModeForField,
|
|
||||||
getFieldSeriesColor,
|
|
||||||
getFieldDisplayName,
|
|
||||||
getDisplayProcessor,
|
|
||||||
FieldColorModeId,
|
|
||||||
DecimalCount,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import {
|
|
||||||
AxisPlacement,
|
|
||||||
GraphDrawStyle,
|
|
||||||
GraphFieldConfig,
|
|
||||||
GraphThresholdsStyleMode,
|
|
||||||
VisibilityMode,
|
|
||||||
ScaleDirection,
|
|
||||||
ScaleOrientation,
|
|
||||||
StackingMode,
|
|
||||||
GraphTransform,
|
|
||||||
AxisColorMode,
|
|
||||||
GraphGradientMode,
|
|
||||||
} from '@grafana/schema';
|
|
||||||
|
|
||||||
// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks
|
|
||||||
// see categories.ts is @grafana/data
|
|
||||||
const IEC_UNITS = new Set([
|
|
||||||
'bytes',
|
|
||||||
'bits',
|
|
||||||
'kbytes',
|
|
||||||
'mbytes',
|
|
||||||
'gbytes',
|
|
||||||
'tbytes',
|
|
||||||
'pbytes',
|
|
||||||
'binBps',
|
|
||||||
'binbps',
|
|
||||||
'KiBs',
|
|
||||||
'Kibits',
|
|
||||||
'MiBs',
|
|
||||||
'Mibits',
|
|
||||||
'GiBs',
|
|
||||||
'Gibits',
|
|
||||||
'TiBs',
|
|
||||||
'Tibits',
|
|
||||||
'PiBs',
|
|
||||||
'Pibits',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const BIN_INCRS = Array(53);
|
|
||||||
|
|
||||||
for (let i = 0; i < BIN_INCRS.length; i++) {
|
|
||||||
BIN_INCRS[i] = 2 ** i;
|
|
||||||
}
|
|
||||||
|
|
||||||
import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../../components/uPlot/config/UPlotConfigBuilder';
|
|
||||||
import { getScaleGradientFn } from '../../components/uPlot/config/gradientFills';
|
|
||||||
import { getStackingGroups, preparePlotData2 } from '../../components/uPlot/utils';
|
|
||||||
import { buildScaleKey } from '../GraphNG/utils';
|
|
||||||
|
|
||||||
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
|
|
||||||
|
|
||||||
const defaultConfig: GraphFieldConfig = {
|
|
||||||
drawStyle: GraphDrawStyle.Line,
|
|
||||||
showPoints: VisibilityMode.Auto,
|
|
||||||
axisPlacement: AxisPlacement.Auto,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
|
||||||
sync?: () => DashboardCursorSync;
|
|
||||||
}> = ({
|
|
||||||
frame,
|
|
||||||
theme,
|
|
||||||
timeZones,
|
|
||||||
getTimeRange,
|
|
||||||
eventBus,
|
|
||||||
sync,
|
|
||||||
allFrames,
|
|
||||||
renderers,
|
|
||||||
tweakScale = (opts) => opts,
|
|
||||||
tweakAxis = (opts) => opts,
|
|
||||||
eventsScope = '__global_',
|
|
||||||
}) => {
|
|
||||||
const builder = new UPlotConfigBuilder(timeZones[0]);
|
|
||||||
|
|
||||||
let alignedFrame: DataFrame;
|
|
||||||
|
|
||||||
builder.setPrepData((frames) => {
|
|
||||||
// cache alignedFrame
|
|
||||||
alignedFrame = frames[0];
|
|
||||||
|
|
||||||
return preparePlotData2(frames[0], builder.getStackingGroups());
|
|
||||||
});
|
|
||||||
|
|
||||||
// X is the first field in the aligned frame
|
|
||||||
const xField = frame.fields[0];
|
|
||||||
if (!xField) {
|
|
||||||
return builder; // empty frame with no options
|
|
||||||
}
|
|
||||||
|
|
||||||
const xScaleKey = 'x';
|
|
||||||
let xScaleUnit = '_x';
|
|
||||||
let yScaleKey = '';
|
|
||||||
|
|
||||||
const xFieldAxisPlacement =
|
|
||||||
xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden;
|
|
||||||
const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden;
|
|
||||||
|
|
||||||
if (xField.type === FieldType.time) {
|
|
||||||
xScaleUnit = 'time';
|
|
||||||
builder.addScale({
|
|
||||||
scaleKey: xScaleKey,
|
|
||||||
orientation: ScaleOrientation.Horizontal,
|
|
||||||
direction: ScaleDirection.Right,
|
|
||||||
isTime: true,
|
|
||||||
range: () => {
|
|
||||||
const r = getTimeRange();
|
|
||||||
return [r.from.valueOf(), r.to.valueOf()];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// filters first 2 ticks to make space for timezone labels
|
|
||||||
const filterTicks: uPlot.Axis.Filter | undefined =
|
|
||||||
timeZones.length > 1
|
|
||||||
? (u, splits) => {
|
|
||||||
return splits.map((v, i) => (i < 2 ? null : v));
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
for (let i = 0; i < timeZones.length; i++) {
|
|
||||||
const timeZone = timeZones[i];
|
|
||||||
builder.addAxis({
|
|
||||||
scaleKey: xScaleKey,
|
|
||||||
isTime: true,
|
|
||||||
placement: xFieldAxisPlacement,
|
|
||||||
show: xFieldAxisShow,
|
|
||||||
label: xField.config.custom?.axisLabel,
|
|
||||||
timeZone,
|
|
||||||
theme,
|
|
||||||
grid: { show: i === 0 && xField.config.custom?.axisGridShow },
|
|
||||||
filter: filterTicks,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// render timezone labels
|
|
||||||
if (timeZones.length > 1) {
|
|
||||||
builder.addHook('drawAxes', (u: uPlot) => {
|
|
||||||
u.ctx.save();
|
|
||||||
|
|
||||||
u.ctx.fillStyle = theme.colors.text.primary;
|
|
||||||
u.ctx.textAlign = 'left';
|
|
||||||
u.ctx.textBaseline = 'bottom';
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
u.axes.forEach((a) => {
|
|
||||||
if (a.side === 2) {
|
|
||||||
//@ts-ignore
|
|
||||||
let cssBaseline: number = a._pos + a._size;
|
|
||||||
u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
u.ctx.restore();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not time!
|
|
||||||
if (xField.config.unit) {
|
|
||||||
xScaleUnit = xField.config.unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.addScale({
|
|
||||||
scaleKey: xScaleKey,
|
|
||||||
orientation: ScaleOrientation.Horizontal,
|
|
||||||
direction: ScaleDirection.Right,
|
|
||||||
range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax],
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addAxis({
|
|
||||||
scaleKey: xScaleKey,
|
|
||||||
placement: xFieldAxisPlacement,
|
|
||||||
show: xFieldAxisShow,
|
|
||||||
label: xField.config.custom?.axisLabel,
|
|
||||||
theme,
|
|
||||||
grid: { show: xField.config.custom?.axisGridShow },
|
|
||||||
formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let customRenderedFields =
|
|
||||||
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
|
|
||||||
|
|
||||||
let indexByName: Map<string, number> | undefined;
|
|
||||||
|
|
||||||
for (let i = 1; i < frame.fields.length; i++) {
|
|
||||||
const field = frame.fields[i];
|
|
||||||
|
|
||||||
const config: FieldConfig<GraphFieldConfig> = {
|
|
||||||
...field.config,
|
|
||||||
custom: {
|
|
||||||
...defaultConfig,
|
|
||||||
...field.config.custom,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const customConfig: GraphFieldConfig = config.custom!;
|
|
||||||
|
|
||||||
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fmt = field.display ?? defaultFormatter;
|
|
||||||
if (field.config.custom?.stacking?.mode === StackingMode.Percent) {
|
|
||||||
fmt = getDisplayProcessor({
|
|
||||||
field: {
|
|
||||||
...field,
|
|
||||||
config: {
|
|
||||||
...field.config,
|
|
||||||
unit: 'percentunit',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
theme,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const scaleKey = buildScaleKey(config, field.type);
|
|
||||||
const colorMode = getFieldColorModeForField(field);
|
|
||||||
const scaleColor = getFieldSeriesColor(field, theme);
|
|
||||||
const seriesColor = scaleColor.color;
|
|
||||||
|
|
||||||
// The builder will manage unique scaleKeys and combine where appropriate
|
|
||||||
builder.addScale(
|
|
||||||
tweakScale(
|
|
||||||
{
|
|
||||||
scaleKey,
|
|
||||||
orientation: ScaleOrientation.Vertical,
|
|
||||||
direction: ScaleDirection.Up,
|
|
||||||
distribution: customConfig.scaleDistribution?.type,
|
|
||||||
log: customConfig.scaleDistribution?.log,
|
|
||||||
linearThreshold: customConfig.scaleDistribution?.linearThreshold,
|
|
||||||
min: field.config.min,
|
|
||||||
max: field.config.max,
|
|
||||||
softMin: customConfig.axisSoftMin,
|
|
||||||
softMax: customConfig.axisSoftMax,
|
|
||||||
centeredZero: customConfig.axisCenteredZero,
|
|
||||||
range:
|
|
||||||
customConfig.stacking?.mode === StackingMode.Percent
|
|
||||||
? (u: uPlot, dataMin: number, dataMax: number) => {
|
|
||||||
dataMin = dataMin < 0 ? -1 : 0;
|
|
||||||
dataMax = dataMax > 0 ? 1 : 0;
|
|
||||||
return [dataMin, dataMax];
|
|
||||||
}
|
|
||||||
: field.type === FieldType.enum
|
|
||||||
? (u: uPlot, dataMin: number, dataMax: number) => {
|
|
||||||
// this is the exhaustive enum (stable)
|
|
||||||
let len = field.config.type!.enum!.text!.length;
|
|
||||||
|
|
||||||
return [-1, len];
|
|
||||||
|
|
||||||
// these are only values that are present
|
|
||||||
// return [dataMin - 1, dataMax + 1]
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
decimals: field.config.decimals,
|
|
||||||
},
|
|
||||||
field
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!yScaleKey) {
|
|
||||||
yScaleKey = scaleKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
|
||||||
let axisColor: uPlot.Axis.Stroke | undefined;
|
|
||||||
|
|
||||||
if (customConfig.axisColorMode === AxisColorMode.Series) {
|
|
||||||
if (
|
|
||||||
colorMode.isByValue &&
|
|
||||||
field.config.custom?.gradientMode === GraphGradientMode.Scheme &&
|
|
||||||
colorMode.id === FieldColorModeId.Thresholds
|
|
||||||
) {
|
|
||||||
axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds);
|
|
||||||
} else {
|
|
||||||
axisColor = seriesColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const axisDisplayOptions = {
|
|
||||||
border: {
|
|
||||||
show: customConfig.axisBorderShow || false,
|
|
||||||
width: 1 / devicePixelRatio,
|
|
||||||
stroke: axisColor || theme.colors.text.primary,
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
show: customConfig.axisBorderShow || false,
|
|
||||||
stroke: axisColor || theme.colors.text.primary,
|
|
||||||
},
|
|
||||||
color: axisColor || theme.colors.text.primary,
|
|
||||||
};
|
|
||||||
|
|
||||||
let incrs: uPlot.Axis.Incrs | undefined;
|
|
||||||
|
|
||||||
// TODO: these will be dynamic with frame updates, so need to accept getYTickLabels()
|
|
||||||
let values: uPlot.Axis.Values | undefined;
|
|
||||||
let splits: uPlot.Axis.Splits | undefined;
|
|
||||||
|
|
||||||
if (IEC_UNITS.has(config.unit!)) {
|
|
||||||
incrs = BIN_INCRS;
|
|
||||||
} else if (field.type === FieldType.enum) {
|
|
||||||
let text = field.config.type!.enum!.text!;
|
|
||||||
splits = text.map((v: string, i: number) => i);
|
|
||||||
values = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.addAxis(
|
|
||||||
tweakAxis(
|
|
||||||
{
|
|
||||||
scaleKey,
|
|
||||||
label: customConfig.axisLabel,
|
|
||||||
size: customConfig.axisWidth,
|
|
||||||
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
|
|
||||||
formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)),
|
|
||||||
theme,
|
|
||||||
grid: { show: customConfig.axisGridShow },
|
|
||||||
decimals: field.config.decimals,
|
|
||||||
distr: customConfig.scaleDistribution?.type,
|
|
||||||
splits,
|
|
||||||
values,
|
|
||||||
incrs,
|
|
||||||
...axisDisplayOptions,
|
|
||||||
},
|
|
||||||
field
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const showPoints =
|
|
||||||
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
|
|
||||||
|
|
||||||
let pointsFilter: uPlot.Series.Points.Filter = () => null;
|
|
||||||
|
|
||||||
if (customConfig.spanNulls !== true) {
|
|
||||||
pointsFilter = (u, seriesIdx, show, gaps) => {
|
|
||||||
let filtered = [];
|
|
||||||
|
|
||||||
let series = u.series[seriesIdx];
|
|
||||||
|
|
||||||
if (!show && gaps && gaps.length) {
|
|
||||||
const [firstIdx, lastIdx] = series.idxs!;
|
|
||||||
const xData = u.data[0];
|
|
||||||
const yData = u.data[seriesIdx];
|
|
||||||
const firstPos = Math.round(u.valToPos(xData[firstIdx], 'x', true));
|
|
||||||
const lastPos = Math.round(u.valToPos(xData[lastIdx], 'x', true));
|
|
||||||
|
|
||||||
if (gaps[0][0] === firstPos) {
|
|
||||||
filtered.push(firstIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// show single points between consecutive gaps that share end/start
|
|
||||||
for (let i = 0; i < gaps.length; i++) {
|
|
||||||
let thisGap = gaps[i];
|
|
||||||
let nextGap = gaps[i + 1];
|
|
||||||
|
|
||||||
if (nextGap && thisGap[1] === nextGap[0]) {
|
|
||||||
// approx when data density is > 1pt/px, since gap start/end pixels are rounded
|
|
||||||
let approxIdx = u.posToIdx(thisGap[1], true);
|
|
||||||
|
|
||||||
if (yData[approxIdx] == null) {
|
|
||||||
// scan left/right alternating to find closest index with non-null value
|
|
||||||
for (let j = 1; j < 100; j++) {
|
|
||||||
if (yData[approxIdx + j] != null) {
|
|
||||||
approxIdx += j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (yData[approxIdx - j] != null) {
|
|
||||||
approxIdx -= j;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered.push(approxIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gaps[gaps.length - 1][1] === lastPos) {
|
|
||||||
filtered.push(lastIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered.length ? filtered : null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let { fillOpacity } = customConfig;
|
|
||||||
|
|
||||||
let pathBuilder: uPlot.Series.PathBuilder | null = null;
|
|
||||||
let pointsBuilder: uPlot.Series.Points.Show | null = null;
|
|
||||||
|
|
||||||
if (field.state?.origin) {
|
|
||||||
if (!indexByName) {
|
|
||||||
indexByName = getNamesToFieldIndex(frame, allFrames);
|
|
||||||
}
|
|
||||||
|
|
||||||
const originFrame = allFrames[field.state.origin.frameIndex];
|
|
||||||
const originField = originFrame?.fields[field.state.origin.fieldIndex];
|
|
||||||
|
|
||||||
const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames);
|
|
||||||
|
|
||||||
// disable default renderers
|
|
||||||
if (customRenderedFields.indexOf(dispName) >= 0) {
|
|
||||||
pathBuilder = () => null;
|
|
||||||
pointsBuilder = () => undefined;
|
|
||||||
} else if (customConfig.transform === GraphTransform.Constant) {
|
|
||||||
// patch some monkeys!
|
|
||||||
const defaultBuilder = uPlot.paths!.linear!();
|
|
||||||
|
|
||||||
pathBuilder = (u, seriesIdx) => {
|
|
||||||
//eslint-disable-next-line
|
|
||||||
const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types
|
|
||||||
|
|
||||||
// the data we want the line renderer to pull is x at each plot edge with paired flat y values
|
|
||||||
|
|
||||||
const r = getTimeRange();
|
|
||||||
let xData = [r.from.valueOf(), r.to.valueOf()];
|
|
||||||
let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null);
|
|
||||||
let yData = [firstY, firstY];
|
|
||||||
let fauxData = _data.slice();
|
|
||||||
fauxData[0] = xData;
|
|
||||||
fauxData[seriesIdx] = yData;
|
|
||||||
|
|
||||||
//eslint-disable-next-line
|
|
||||||
return defaultBuilder(
|
|
||||||
{
|
|
||||||
...u,
|
|
||||||
_data: fauxData,
|
|
||||||
} as any,
|
|
||||||
seriesIdx,
|
|
||||||
0,
|
|
||||||
1
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customConfig.fillBelowTo) {
|
|
||||||
const fillBelowToField = frame.fields.find(
|
|
||||||
(f) =>
|
|
||||||
customConfig.fillBelowTo === f.name ||
|
|
||||||
customConfig.fillBelowTo === f.config?.displayNameFromDS ||
|
|
||||||
customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames)
|
|
||||||
);
|
|
||||||
|
|
||||||
const fillBelowDispName = fillBelowToField
|
|
||||||
? getFieldDisplayName(fillBelowToField, frame, allFrames)
|
|
||||||
: customConfig.fillBelowTo;
|
|
||||||
|
|
||||||
const t = indexByName.get(dispName);
|
|
||||||
const b = indexByName.get(fillBelowDispName);
|
|
||||||
if (isNumber(b) && isNumber(t)) {
|
|
||||||
builder.addBand({
|
|
||||||
series: [t, b],
|
|
||||||
fill: undefined, // using null will have the band use fill options from `t`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!fillOpacity) {
|
|
||||||
fillOpacity = 35; // default from flot
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fillOpacity = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined;
|
|
||||||
|
|
||||||
if (colorMode.id === FieldColorModeId.Thresholds) {
|
|
||||||
dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.addSeries({
|
|
||||||
pathBuilder,
|
|
||||||
pointsBuilder,
|
|
||||||
scaleKey,
|
|
||||||
showPoints,
|
|
||||||
pointsFilter,
|
|
||||||
colorMode,
|
|
||||||
fillOpacity,
|
|
||||||
theme,
|
|
||||||
dynamicSeriesColor,
|
|
||||||
drawStyle: customConfig.drawStyle!,
|
|
||||||
lineColor: customConfig.lineColor ?? seriesColor,
|
|
||||||
lineWidth: customConfig.lineWidth,
|
|
||||||
lineInterpolation: customConfig.lineInterpolation,
|
|
||||||
lineStyle: customConfig.lineStyle,
|
|
||||||
barAlignment: customConfig.barAlignment,
|
|
||||||
barWidthFactor: customConfig.barWidthFactor,
|
|
||||||
barMaxWidth: customConfig.barMaxWidth,
|
|
||||||
pointSize: customConfig.pointSize,
|
|
||||||
spanNulls: customConfig.spanNulls || false,
|
|
||||||
show: !customConfig.hideFrom?.viz,
|
|
||||||
gradientMode: customConfig.gradientMode,
|
|
||||||
thresholds: config.thresholds,
|
|
||||||
hardMin: field.config.min,
|
|
||||||
hardMax: field.config.max,
|
|
||||||
softMin: customConfig.axisSoftMin,
|
|
||||||
softMax: customConfig.axisSoftMax,
|
|
||||||
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
|
||||||
dataFrameFieldIndex: field.state?.origin,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render thresholds in graph
|
|
||||||
if (customConfig.thresholdsStyle && config.thresholds) {
|
|
||||||
const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphThresholdsStyleMode.Off;
|
|
||||||
if (thresholdDisplay !== GraphThresholdsStyleMode.Off) {
|
|
||||||
builder.addThresholds({
|
|
||||||
config: customConfig.thresholdsStyle,
|
|
||||||
thresholds: config.thresholds,
|
|
||||||
scaleKey,
|
|
||||||
theme,
|
|
||||||
hardMin: field.config.min,
|
|
||||||
hardMax: field.config.max,
|
|
||||||
softMin: customConfig.axisSoftMin,
|
|
||||||
softMax: customConfig.axisSoftMax,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let stackingGroups = getStackingGroups(frame);
|
|
||||||
|
|
||||||
builder.setStackingGroups(stackingGroups);
|
|
||||||
|
|
||||||
// hook up custom/composite renderers
|
|
||||||
renderers?.forEach((r) => {
|
|
||||||
if (!indexByName) {
|
|
||||||
indexByName = getNamesToFieldIndex(frame, allFrames);
|
|
||||||
}
|
|
||||||
let fieldIndices: Record<string, number> = {};
|
|
||||||
|
|
||||||
for (let key in r.fieldMap) {
|
|
||||||
let dispName = r.fieldMap[key];
|
|
||||||
fieldIndices[key] = indexByName.get(dispName)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
r.init(builder, fieldIndices);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.scaleKeys = [xScaleKey, yScaleKey];
|
|
||||||
|
|
||||||
// if hovered value is null, how far we may scan left/right to hover nearest non-null
|
|
||||||
const hoverProximityPx = 15;
|
|
||||||
|
|
||||||
let cursor: Partial<uPlot.Cursor> = {
|
|
||||||
// this scans left and right from cursor position to find nearest data index with value != null
|
|
||||||
// TODO: do we want to only scan past undefined values, but halt at explicit null values?
|
|
||||||
dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => {
|
|
||||||
let seriesData = self.data[seriesIdx];
|
|
||||||
|
|
||||||
if (seriesData[hoveredIdx] == null) {
|
|
||||||
let nonNullLft = null,
|
|
||||||
nonNullRgt = null,
|
|
||||||
i;
|
|
||||||
|
|
||||||
i = hoveredIdx;
|
|
||||||
while (nonNullLft == null && i-- > 0) {
|
|
||||||
if (seriesData[i] != null) {
|
|
||||||
nonNullLft = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i = hoveredIdx;
|
|
||||||
while (nonNullRgt == null && i++ < seriesData.length) {
|
|
||||||
if (seriesData[i] != null) {
|
|
||||||
nonNullRgt = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let xVals = self.data[0];
|
|
||||||
|
|
||||||
let curPos = self.valToPos(cursorXVal, 'x');
|
|
||||||
let rgtPos = nonNullRgt == null ? Infinity : self.valToPos(xVals[nonNullRgt], 'x');
|
|
||||||
let lftPos = nonNullLft == null ? -Infinity : self.valToPos(xVals[nonNullLft], 'x');
|
|
||||||
|
|
||||||
let lftDelta = curPos - lftPos;
|
|
||||||
let rgtDelta = rgtPos - curPos;
|
|
||||||
|
|
||||||
if (lftDelta <= rgtDelta) {
|
|
||||||
if (lftDelta <= hoverProximityPx) {
|
|
||||||
hoveredIdx = nonNullLft!;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (rgtDelta <= hoverProximityPx) {
|
|
||||||
hoveredIdx = nonNullRgt!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hoveredIdx;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sync && sync() !== DashboardCursorSync.Off) {
|
|
||||||
const payload: DataHoverPayload = {
|
|
||||||
point: {
|
|
||||||
[xScaleKey]: null,
|
|
||||||
[yScaleKey]: null,
|
|
||||||
},
|
|
||||||
data: frame,
|
|
||||||
};
|
|
||||||
|
|
||||||
const hoverEvent = new DataHoverEvent(payload);
|
|
||||||
cursor.sync = {
|
|
||||||
key: eventsScope,
|
|
||||||
filters: {
|
|
||||||
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
|
|
||||||
if (sync && sync() === DashboardCursorSync.Off) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
payload.rowIndex = dataIdx;
|
|
||||||
if (x < 0 && y < 0) {
|
|
||||||
payload.point[xScaleUnit] = null;
|
|
||||||
payload.point[yScaleKey] = null;
|
|
||||||
eventBus.publish(new DataHoverClearEvent());
|
|
||||||
} else {
|
|
||||||
// convert the points
|
|
||||||
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
|
||||||
payload.point[yScaleKey] = src.posToVal(y, yScaleKey);
|
|
||||||
payload.point.panelRelY = y > 0 ? y / h : 1; // used by old graph panel to position tooltip
|
|
||||||
eventBus.publish(hoverEvent);
|
|
||||||
hoverEvent.payload.down = undefined;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: [xScaleKey, yScaleKey],
|
|
||||||
// match: [() => true, (a, b) => a === b],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setSync();
|
|
||||||
builder.setCursor(cursor);
|
|
||||||
|
|
||||||
return builder;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
|
|
||||||
const originNames = new Map<string, number>();
|
|
||||||
frame.fields.forEach((field, i) => {
|
|
||||||
const origin = field.state?.origin;
|
|
||||||
if (origin) {
|
|
||||||
const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex];
|
|
||||||
if (origField) {
|
|
||||||
originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return originNames;
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import {
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
DataLinksInlineEditor,
|
DataLinksInlineEditor,
|
||||||
DataSourceHttpSettings,
|
DataSourceHttpSettings,
|
||||||
GraphContextMenu,
|
|
||||||
Icon,
|
Icon,
|
||||||
LegacyForms,
|
LegacyForms,
|
||||||
SeriesColorPickerPopoverWithTheme,
|
SeriesColorPickerPopoverWithTheme,
|
||||||
|
|
@ -22,6 +21,8 @@ import { MetricSelect } from '../core/components/Select/MetricSelect';
|
||||||
import { TagFilter } from '../core/components/TagFilter/TagFilter';
|
import { TagFilter } from '../core/components/TagFilter/TagFilter';
|
||||||
import { HelpModal } from '../core/components/help/HelpModal';
|
import { HelpModal } from '../core/components/help/HelpModal';
|
||||||
|
|
||||||
|
import { GraphContextMenu } from './components/legacy_graph_panel/GraphContextMenu';
|
||||||
|
|
||||||
const { SecretFormField } = LegacyForms;
|
const { SecretFormField } = LegacyForms;
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,29 @@ import {
|
||||||
TimeZone,
|
TimeZone,
|
||||||
FormattedValue,
|
FormattedValue,
|
||||||
GrafanaTheme2,
|
GrafanaTheme2,
|
||||||
|
Dimension,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
|
import {
|
||||||
import { ContextMenu, ContextMenuProps } from '../../components/ContextMenu/ContextMenu';
|
ContextMenu,
|
||||||
import { FormattedValueDisplay } from '../../components/FormattedValueDisplay/FormattedValueDisplay';
|
ContextMenuProps,
|
||||||
import { HorizontalGroup } from '../../components/Layout/Layout';
|
FormattedValueDisplay,
|
||||||
import { MenuGroup, MenuGroupProps } from '../../components/Menu/MenuGroup';
|
HorizontalGroup,
|
||||||
import { MenuItem } from '../../components/Menu/MenuItem';
|
MenuGroup,
|
||||||
import { SeriesIcon } from '../../components/VizLegend/SeriesIcon';
|
MenuGroupProps,
|
||||||
import { useStyles2 } from '../../themes';
|
MenuItem,
|
||||||
|
SeriesIcon,
|
||||||
import { GraphDimensions } from './GraphTooltip/types';
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
|
export type ContextDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
|
export interface GraphDimensions extends Dimensions {
|
||||||
|
xAxis: Dimension<number>;
|
||||||
|
yAxis: Dimension<number>;
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
export type GraphContextMenuProps = ContextMenuProps & {
|
export type GraphContextMenuProps = ContextMenuProps & {
|
||||||
getContextMenuSource: () => FlotDataPoint | null;
|
getContextMenuSource: () => FlotDataPoint | null;
|
||||||
|
|
@ -2,15 +2,8 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useClickAway } from 'react-use';
|
import { useClickAway } from 'react-use';
|
||||||
|
|
||||||
import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data';
|
import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data';
|
||||||
import {
|
import { ContextMenu, MenuItemProps, MenuItemsGroup, MenuGroup, MenuItem, UPlotConfigBuilder } from '@grafana/ui';
|
||||||
ContextMenu,
|
import { GraphContextMenuHeader } from 'app/angular/components/legacy_graph_panel/GraphContextMenu';
|
||||||
GraphContextMenuHeader,
|
|
||||||
MenuItemProps,
|
|
||||||
MenuItemsGroup,
|
|
||||||
MenuGroup,
|
|
||||||
MenuItem,
|
|
||||||
UPlotConfigBuilder,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
|
|
||||||
type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D };
|
type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D };
|
||||||
type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null };
|
type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue