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