diff --git a/.betterer.results b/.betterer.results index 2e2644ad9aa..c303a90fc4d 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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.", "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": [ [0, 0, 0, "Do not use any type assertions.", "0"], [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, "Do not use any type assertions.", "2"] ], - "packages/grafana-ui/src/graveyard/TimeSeries/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "packages/grafana-ui/src/options/builder/axis.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b18eeac0350..1139ac6950a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -332,9 +332,7 @@ /packages/grafana-ui/src/components/VizRepeater/ @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/graveyard/Graph/ @grafana/dataviz-squad /packages/grafana-ui/src/graveyard/GraphNG/ @grafana/dataviz-squad -/packages/grafana-ui/src/graveyard/TimeSeries/ @grafana/dataviz-squad /packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend /packages/grafana-data/src/transformations/ @grafana/dataviz-squad /packages/grafana-data/src/**/*logs* @grafana/observability-logs diff --git a/packages/grafana-ui/src/components/Menu/Menu.story.tsx b/packages/grafana-ui/src/components/Menu/Menu.story.tsx index 4a547fb84ae..a92e0755e7b 100644 --- a/packages/grafana-ui/src/components/Menu/Menu.story.tsx +++ b/packages/grafana-ui/src/components/Menu/Menu.story.tsx @@ -1,7 +1,6 @@ import { Meta } from '@storybook/react'; import React from 'react'; -import { GraphContextMenuHeader } from '..'; import { StoryExample } from '../../utils/storybook/StoryExample'; import { VerticalGroup } from '../Layout/Layout'; @@ -110,30 +109,6 @@ export function Examples() { - - - } - ariaLabel="Menu header" - > - - - - - - - - - diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ec094bea02e..cd3c980768a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -150,7 +150,6 @@ export { VizLegend } from './VizLegend/VizLegend'; export { VizLegendListItem } from './VizLegend/VizLegendListItem'; export { Alert, type AlertVariant } from './Alert/Alert'; -export { GraphSeriesToggler, type GraphSeriesTogglerAPI } from '../graveyard/Graph/GraphSeriesToggler'; export { Collapse, ControlledCollapse } from './Collapse/Collapse'; export { CollapsableSection } from './Collapse/CollapsableSection'; export { DataLinkButton } from './DataLinks/DataLinkButton'; @@ -296,19 +295,3 @@ export { type UPlotConfigPrepFn } from './uPlot/config/UPlotConfigBuilder'; export * from './PanelChrome/types'; export { Label as BrowserLabel } from './BrowserLabel/Label'; 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'; diff --git a/packages/grafana-ui/src/components/uPlot/utils.test.ts b/packages/grafana-ui/src/components/uPlot/utils.test.ts index 3ecbc180bd9..363c49e3d54 100644 --- a/packages/grafana-ui/src/components/uPlot/utils.test.ts +++ b/packages/grafana-ui/src/components/uPlot/utils.test.ts @@ -1,7 +1,9 @@ import { FieldMatcherID, fieldMatchers, FieldType, MutableDataFrame } from '@grafana/data'; 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'; diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx deleted file mode 100644 index 847d6e883c0..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/Graph.test.tsx +++ /dev/null @@ -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 = ( - - - - ); - 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 = ( - - - - ); - 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 = ( - - - - ); - 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); - }); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx b/packages/grafana-ui/src/graveyard/Graph/Graph.tsx deleted file mode 100644 index 624d19e29c6..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/Graph.tsx +++ /dev/null @@ -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; - contextItem?: FlotItem; -} - -/** - * This is a react wrapper for the angular, flot based graph visualization. - * Rather than using this component, you should use the ` with - * timeseries panel configs. - * - * @deprecated - */ -export class Graph extends PureComponent { - static defaultProps = { - showLines: true, - showPoints: false, - showBars: false, - isStacked: false, - lineWidth: 1, - }; - - state: GraphState = { - isTooltipVisible: false, - isContextVisible: false, - }; - - element: HTMLElement | null = null; - $element: JQuery | 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) => { - this.setState({ - isTooltipVisible: true, - activeItem: item, - pos, - }); - }; - - onPlotClick = (event: JQuery.Event, contextPos: FlotPosition, item?: FlotItem) => { - 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 | 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 = { - // 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 = { - 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 = { - // 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 ; - }; - - 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 ( -
-
(this.element = e)} - style={{ height, width }} - onMouseLeave={() => { - this.setState({ isTooltipVisible: false }); - }} - /> - {noDataToBeDisplayed &&
No data
} - {tooltip} - {context} -
- ); - } -} - -export default Graph; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx deleted file mode 100644 index fc960695a68..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphSeriesToggler.tsx +++ /dev/null @@ -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) => 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 { - constructor(props: GraphSeriesTogglerProps) { - super(props); - - this.onSeriesToggle = this.onSeriesToggle.bind(this); - - this.state = { - hiddenSeries: [], - toggledSeries: props.series, - }; - } - - componentDidUpdate(prevProps: Readonly) { - const { series } = this.props; - if (!isEqual(prevProps.series, series)) { - this.setState({ hiddenSeries: [], toggledSeries: series }); - } - } - - onSeriesToggle(label: string, event: React.MouseEvent) { - 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, - }); - } -} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx deleted file mode 100644 index d58c0816dfe..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/GraphTooltip.tsx +++ /dev/null @@ -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) => { - // 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 ; - } else { - return ( - - ); - } -}; - -GraphTooltip.displayName = 'GraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx deleted file mode 100644 index 1af501183a6..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.test.tsx +++ /dev/null @@ -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 = { - xAxis: [0, 1], // column, row - yAxis: [0, 1], // column, row - }; - render( - - ); - - // 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 = { - xAxis: [0, undefined], // no active point in time - yAxis: null, // no active series - }; - - render( - - ); - - // 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}`); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx deleted file mode 100644 index bfe2856c700..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/MultiModeGraphTooltip.tsx +++ /dev/null @@ -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 ; -}; - -MultiModeGraphTooltip.displayName = 'MultiModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx deleted file mode 100644 index 7eb4d61872c..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/SingleModeGraphTooltip.tsx +++ /dev/null @@ -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 ( - - ); -}; - -SingleModeGraphTooltip.displayName = 'SingleModeGraphTooltip'; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts b/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts deleted file mode 100644 index d4d68b729c1..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphTooltip/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Dimension, Dimensions, TimeZone } from '@grafana/data'; - -import { ActiveDimensions } from '../../../components/VizTooltip'; - -/** @deprecated */ -export interface GraphDimensions extends Dimensions { - xAxis: Dimension; - yAxis: Dimension; -} - -/** @deprecated */ -export interface GraphTooltipContentProps { - dimensions: GraphDimensions; // Dimension[] - activeDimensions: ActiveDimensions; - timeZone?: TimeZone; -} diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx deleted file mode 100644 index 0830b44f198..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.internal.story.tsx +++ /dev/null @@ -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 = ({ rightAxisSeries, displayMode, legendDisplayMode, ...args }) => { - const props: Partial = { - 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 ( - - ); -}; -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', -}; diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx b/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx deleted file mode 100644 index a590d5d1ed1..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/GraphWithLegend.tsx +++ /dev/null @@ -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) => 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((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 ( -
-
- - {children} - -
- - {legendVisibility && ( -
- - { - if (onSeriesToggle) { - onSeriesToggle(item.label, event); - } - }} - onToggleSort={onToggleSort} - /> - -
- )} -
- ); -}; - -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', - }), -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/types.ts b/packages/grafana-ui/src/graveyard/Graph/types.ts deleted file mode 100644 index 60cdb44b983..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @deprecated */ -export interface FlotItem { - datapoint: [number, number]; - dataIndex: number; - series: T; - seriesIndex: number; - pageX: number; - pageY: number; -} diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.test.ts b/packages/grafana-ui/src/graveyard/Graph/utils.test.ts deleted file mode 100644 index f68ed2e4b3f..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/utils.test.ts +++ /dev/null @@ -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'); - }); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/Graph/utils.ts b/packages/grafana-ui/src/graveyard/Graph/utils.ts deleted file mode 100644 index ebc34ad470e..00000000000 --- a/packages/grafana-ui/src/graveyard/Graph/utils.ts +++ /dev/null @@ -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; -}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx deleted file mode 100644 index 52dfadb27a6..00000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/GraphNG.tsx +++ /dev/null @@ -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 = (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; - 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; -} - -function sameProps(prevProps: any, nextProps: any, propsToDiff: Array = []) { - 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 { - static contextType = PanelContextRoot; - panelContext: PanelContext = {} as PanelContext; - private plotInstance: React.RefObject; - - 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 ( - - {(vizWidth: number, vizHeight: number) => ( - ((this.plotInstance as React.MutableRefObject).current = u)} - > - {children ? children(config, alignedFrame) : null} - - )} - - ); - } -} diff --git a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap deleted file mode 100644 index 3b9265254b5..00000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/__snapshots__/utils.test.ts.snap +++ /dev/null @@ -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], -} -`; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts b/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts deleted file mode 100644 index e4f1d46550a..00000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/hooks.ts +++ /dev/null @@ -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({} as GraphNGContextType); - -/** @deprecated */ -export const useGraphNGContext = () => { - const { data, dimFields, mapSeriesIndexToDataFrameFieldIndex } = useContext(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, - }; -}; diff --git a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts b/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts deleted file mode 100644 index 9675cd7ca56..00000000000 --- a/packages/grafana-ui/src/graveyard/GraphNG/utils.test.ts +++ /dev/null @@ -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 = { - 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 = { - 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 = { - 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 = { - 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 = { - 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, - } - `); - }); -}); diff --git a/packages/grafana-ui/src/graveyard/README.md b/packages/grafana-ui/src/graveyard/README.md index 2715ecec38a..627570b3aa7 100644 --- a/packages/grafana-ui/src/graveyard/README.md +++ b/packages/grafana-ui/src/graveyard/README.md @@ -1 +1,4 @@ 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 diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx deleted file mode 100644 index 9a748236d77..00000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/TimeSeries.tsx +++ /dev/null @@ -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 = ['legend', 'options', 'theme']; - -type TimeSeriesProps = Omit; - -export class UnthemedTimeSeries extends Component { - static contextType = PanelContextRoot; - declare context: React.ContextType; - - 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 ; - }; - - render() { - return ( - - ); - } -} - -export const TimeSeries = withTheme2(UnthemedTimeSeries); -TimeSeries.displayName = 'TimeSeries'; diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts deleted file mode 100644 index 583358c7c4c..00000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/utils.test.ts +++ /dev/null @@ -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); - } - }); -}); diff --git a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts b/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts deleted file mode 100644 index 12a6e3908c6..00000000000 --- a/packages/grafana-ui/src/graveyard/TimeSeries/utils.ts +++ /dev/null @@ -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 | undefined; - - for (let i = 1; i < frame.fields.length; i++) { - const field = frame.fields[i]; - - const config: FieldConfig = { - ...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 = {}; - - 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 = { - // 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 { - const originNames = new Map(); - 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; -} diff --git a/public/app/angular/angular_wrappers.ts b/public/app/angular/angular_wrappers.ts index fc0263ca322..f3372171166 100644 --- a/public/app/angular/angular_wrappers.ts +++ b/public/app/angular/angular_wrappers.ts @@ -3,7 +3,6 @@ import { ColorPicker, DataLinksInlineEditor, DataSourceHttpSettings, - GraphContextMenu, Icon, LegacyForms, SeriesColorPickerPopoverWithTheme, @@ -22,6 +21,8 @@ import { MetricSelect } from '../core/components/Select/MetricSelect'; import { TagFilter } from '../core/components/TagFilter/TagFilter'; import { HelpModal } from '../core/components/help/HelpModal'; +import { GraphContextMenu } from './components/legacy_graph_panel/GraphContextMenu'; + const { SecretFormField } = LegacyForms; export function registerAngularDirectives() { diff --git a/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx b/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx similarity index 86% rename from packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx rename to public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx index 5b3da31708a..8d828e6bdf2 100644 --- a/packages/grafana-ui/src/graveyard/Graph/GraphContextMenu.tsx +++ b/public/app/angular/components/legacy_graph_panel/GraphContextMenu.tsx @@ -9,21 +9,29 @@ import { TimeZone, FormattedValue, GrafanaTheme2, + Dimension, } from '@grafana/data'; - -import { ContextMenu, ContextMenuProps } from '../../components/ContextMenu/ContextMenu'; -import { FormattedValueDisplay } from '../../components/FormattedValueDisplay/FormattedValueDisplay'; -import { HorizontalGroup } from '../../components/Layout/Layout'; -import { MenuGroup, MenuGroupProps } from '../../components/Menu/MenuGroup'; -import { MenuItem } from '../../components/Menu/MenuItem'; -import { SeriesIcon } from '../../components/VizLegend/SeriesIcon'; -import { useStyles2 } from '../../themes'; - -import { GraphDimensions } from './GraphTooltip/types'; +import { + ContextMenu, + ContextMenuProps, + FormattedValueDisplay, + HorizontalGroup, + MenuGroup, + MenuGroupProps, + MenuItem, + SeriesIcon, + useStyles2, +} from '@grafana/ui'; /** @deprecated */ export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; +/** @deprecated */ +export interface GraphDimensions extends Dimensions { + xAxis: Dimension; + yAxis: Dimension; +} + /** @deprecated */ export type GraphContextMenuProps = ContextMenuProps & { getContextMenuSource: () => FlotDataPoint | null; diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index 59c6ad9dceb..005f3487627 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -2,15 +2,8 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useClickAway } from 'react-use'; import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data'; -import { - ContextMenu, - GraphContextMenuHeader, - MenuItemProps, - MenuItemsGroup, - MenuGroup, - MenuItem, - UPlotConfigBuilder, -} from '@grafana/ui'; +import { ContextMenu, MenuItemProps, MenuItemsGroup, MenuGroup, MenuItem, UPlotConfigBuilder } from '@grafana/ui'; +import { GraphContextMenuHeader } from 'app/angular/components/legacy_graph_panel/GraphContextMenu'; type ContextMenuSelectionCoords = { viewport: CartesianCoords2D; plotCanvas: CartesianCoords2D }; type ContextMenuSelectionPoint = { seriesIdx: number | null; dataIdx: number | null };