diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 667b99a84c1..e723ba62c70 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -250,7 +250,6 @@ export { UPlotChart } from './uPlot/Plot'; export { PlotLegend } from './uPlot/PlotLegend'; export * from './uPlot/geometries'; export * from './uPlot/plugins'; -export { usePlotContext } from './uPlot/context'; export { PlotTooltipInterpolator, PlotSelection } from './uPlot/types'; export { GraphNG, GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG'; export { TimeSeries } from './TimeSeries/TimeSeries'; diff --git a/packages/grafana-ui/src/components/uPlot/Plot.tsx b/packages/grafana-ui/src/components/uPlot/Plot.tsx index 49d13b7f61f..43bd77ed549 100755 --- a/packages/grafana-ui/src/components/uPlot/Plot.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.tsx @@ -1,6 +1,5 @@ -import React, { createRef, MutableRefObject } from 'react'; +import React, { createRef } from 'react'; import uPlot, { Options } from 'uplot'; -import { PlotContext, PlotContextType } from './context'; import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils'; import { PlotProps } from './types'; @@ -27,7 +26,7 @@ function sameTimeRange(prevProps: PlotProps, nextProps: PlotProps) { } type UPlotChartState = { - ctx: PlotContextType; + plot: uPlot | null; }; /** @@ -44,35 +43,24 @@ export class UPlotChart extends React.Component { super(props); this.state = { - ctx: { - plot: null, - getCanvasBoundingBox: () => { - return this.plotCanvasBBox.current; - }, - }, + plot: null, }; } reinitPlot() { - let { ctx } = this.state; let { width, height, plotRef } = this.props; - ctx.plot?.destroy(); + this.state.plot?.destroy(); if (width === 0 && height === 0) { return; } - this.props.config.addHook('syncRect', (u, rect) => { - (this.plotCanvasBBox as MutableRefObject).current = rect; - }); - this.props.config.addHook('setSize', (u) => { const canvas = u.over; if (!canvas) { return; } - (this.plotCanvasBBox as MutableRefObject).current = canvas.getBoundingClientRect(); }); const config: Options = { @@ -90,13 +78,7 @@ export class UPlotChart extends React.Component { plotRef(plot); } - this.setState((s) => ({ - ...s, - ctx: { - ...s.ctx, - plot, - }, - })); + this.setState({ plot }); } componentDidMount() { @@ -104,23 +86,23 @@ export class UPlotChart extends React.Component { } componentWillUnmount() { - this.state.ctx.plot?.destroy(); + this.state.plot?.destroy(); } componentDidUpdate(prevProps: PlotProps) { - let { ctx } = this.state; + let { plot } = this.state; if (!sameDims(prevProps, this.props)) { - ctx.plot?.setSize({ + plot?.setSize({ width: this.props.width, height: this.props.height, }); } else if (!sameConfig(prevProps, this.props)) { this.reinitPlot(); } else if (!sameData(prevProps, this.props)) { - ctx.plot?.setData(this.props.data); + plot?.setData(this.props.data); } else if (!sameTimeRange(prevProps, this.props)) { - ctx.plot?.setScale('x', { + plot?.setScale('x', { min: this.props.timeRange.from.valueOf(), max: this.props.timeRange.to.valueOf(), }); @@ -129,12 +111,10 @@ export class UPlotChart extends React.Component { render() { return ( - -
-
- {this.props.children} -
- +
+
+ {this.props.children} +
); } } diff --git a/packages/grafana-ui/src/components/uPlot/context.tsx b/packages/grafana-ui/src/components/uPlot/context.tsx deleted file mode 100644 index ea6470a16e0..00000000000 --- a/packages/grafana-ui/src/components/uPlot/context.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { useContext } from 'react'; -import uPlot from 'uplot'; -export interface PlotContextType { - plot: uPlot | null; - getCanvasBoundingBox: () => DOMRect | null; -} - -/** - * @alpha - */ -export const PlotContext = React.createContext({} as PlotContextType); - -// Exposes uPlot instance and bounding box of the entire canvas and plot area -export const usePlotContext = (): PlotContextType => { - return useContext(PlotContext); -}; diff --git a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx index fe2b5e7cbb1..99089dc5bfa 100644 --- a/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx +++ b/packages/grafana-ui/src/components/uPlot/geometries/EventsCanvas.tsx @@ -1,9 +1,9 @@ import { DataFrame, DataFrameFieldIndex } from '@grafana/data'; -import React, { useLayoutEffect, useMemo, useState } from 'react'; -import { usePlotContext } from '../context'; +import React, { useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useMountedState } from 'react-use'; +import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { Marker } from './Marker'; import { XYCanvas } from './XYCanvas'; -import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; interface EventsCanvasProps { id: string; @@ -17,21 +17,29 @@ interface EventsCanvasProps { } export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: EventsCanvasProps) { - const plotCtx = usePlotContext(); + const plotInstance = useRef(); // render token required to re-render annotation markers. Rendering lines happens in uPlot and the props do not change // so we need to force the re-render when the draw hook was performed by uPlot const [renderToken, setRenderToken] = useState(0); + const isMounted = useMountedState(); useLayoutEffect(() => { + config.addHook('init', (u) => { + plotInstance.current = u; + }); + config.addHook('draw', () => { + if (!isMounted()) { + return; + } setRenderToken((s) => s + 1); }); }, [config, setRenderToken]); const eventMarkers = useMemo(() => { const markers: React.ReactNode[] = []; - const plotInstance = plotCtx.plot; - if (!plotInstance || events.length === 0) { + + if (!plotInstance.current || events.length === 0) { return markers; } @@ -51,11 +59,18 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords } return <>{markers}; - }, [events, renderEventMarker, renderToken, plotCtx]); + }, [events, renderEventMarker, renderToken]); - if (!plotCtx.plot) { + if (!plotInstance.current) { return null; } - return {eventMarkers}; + return ( + + {eventMarkers} + + ); } diff --git a/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx b/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx index c83871a3912..1d5a3d015c7 100644 --- a/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx +++ b/packages/grafana-ui/src/components/uPlot/geometries/XYCanvas.tsx @@ -1,29 +1,24 @@ -import { usePlotContext } from '../context'; import React, { useMemo } from 'react'; import { css } from '@emotion/css'; -interface XYCanvasProps {} +interface XYCanvasProps { + top: number; // css pxls + left: number; // css pxls +} /** * Renders absolutely positioned element on top of the uPlot's plotting area (axes are not included!). * Useful when you want to render some overlay with canvas-independent elements on top of the plot. */ -export const XYCanvas: React.FC = ({ children }) => { - const plotCtx = usePlotContext(); - const plotInstance = plotCtx.plot; - - if (!plotInstance) { - return null; - } - +export const XYCanvas: React.FC = ({ children, left, top }) => { const className = useMemo(() => { return css` position: absolute; overflow: visible; - left: ${plotInstance.bbox.left / window.devicePixelRatio}px; - top: ${plotInstance.bbox.top / window.devicePixelRatio}px; + left: ${left}px; + top: ${top}px; `; - }, [plotInstance.bbox.left, plotInstance.bbox.top]); + }, [left, top]); return
{children}
; }; diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx index 5fe92461307..923708e218d 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin.tsx @@ -1,7 +1,3 @@ -import React, { useEffect, useLayoutEffect, useState } from 'react'; -import { Portal } from '../../Portal/Portal'; -import { usePlotContext } from '../context'; -import { TooltipDisplayMode } from '@grafana/schema'; import { CartesianCoords2D, DashboardCursorSync, @@ -13,11 +9,15 @@ import { getFieldDisplayName, TimeZone, } from '@grafana/data'; +import { TooltipDisplayMode } from '@grafana/schema'; +import React, { useEffect, useLayoutEffect, useState } from 'react'; +import { useMountedState } from 'react-use'; +import uPlot from 'uplot'; +import { useTheme2 } from '../../../themes/ThemeContext'; +import { Portal } from '../../Portal/Portal'; import { SeriesTable, SeriesTableRowProps, VizTooltipContainer } from '../../VizTooltip'; import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder'; import { findMidPointYPosition, pluginLog } from '../utils'; -import { useTheme2 } from '../../../themes/ThemeContext'; -import uPlot from 'uplot'; interface TooltipPluginProps { timeZone: TimeZone; @@ -44,13 +44,12 @@ export const TooltipPlugin: React.FC = ({ ...otherProps }) => { const theme = useTheme2(); - const plotCtx = usePlotContext(); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); const [focusedPointIdx, setFocusedPointIdx] = useState(null); const [focusedPointIdxs, setFocusedPointIdxs] = useState>([]); const [coords, setCoords] = useState(null); - const plotInstance = plotCtx.plot; const [isActive, setIsActive] = useState(false); + const isMounted = useMountedState(); const pluginId = `TooltipPlugin`; @@ -59,42 +58,46 @@ export const TooltipPlugin: React.FC = ({ pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`); }, [focusedPointIdx, focusedSeriesIdx]); - useEffect(() => { + // Add uPlot hooks to the config, or re-add when the config changed + useLayoutEffect(() => { + let plotInstance: uPlot | undefined = undefined; + let bbox: DOMRect | undefined = undefined; + const plotMouseLeave = () => { + if (!isMounted()) { + return; + } setCoords(null); setIsActive(false); - if (plotCtx.plot) { - plotCtx.plot.root.classList.remove('plot-active'); - } + plotInstance?.root.classList.remove('plot-active'); }; const plotMouseEnter = () => { + if (!isMounted()) { + return; + } setIsActive(true); - if (plotCtx.plot) { - plotCtx.plot.root.classList.add('plot-active'); - } + plotInstance?.root.classList.add('plot-active'); }; - if (plotCtx && plotCtx.plot) { - plotCtx.plot.over.addEventListener('mouseleave', plotMouseLeave); - plotCtx.plot.over.addEventListener('mouseenter', plotMouseEnter); + // cache uPlot plotting area bounding box + config.addHook('syncRect', (u, rect) => { + bbox = rect; + }); + + config.addHook('init', (u) => { + plotInstance = u; + + u.over.addEventListener('mouseleave', plotMouseLeave); + u.over.addEventListener('mouseenter', plotMouseEnter); + if (sync === DashboardCursorSync.Crosshair) { - plotCtx.plot.root.classList.add('shared-crosshair'); + u.root.classList.add('shared-crosshair'); } - } + }); - return () => { - setCoords(null); - if (plotCtx && plotCtx.plot) { - plotCtx.plot.over.removeEventListener('mouseleave', plotMouseLeave); - plotCtx.plot.over.removeEventListener('mouseenter', plotMouseEnter); - } - }; - }, [plotCtx.plot?.root]); - - // Add uPlot hooks to the config, or re-add when the config changed - useLayoutEffect(() => { const tooltipInterpolator = config.getTooltipInterpolator(); + if (tooltipInterpolator) { // Custom toolitp positioning config.addHook('setCursor', (u) => { @@ -107,7 +110,6 @@ export const TooltipPlugin: React.FC = ({ return; } - const bbox = plotCtx.getCanvasBoundingBox(); if (!bbox) { return; } @@ -122,14 +124,16 @@ export const TooltipPlugin: React.FC = ({ }); } else { config.addHook('setLegend', (u) => { + if (!isMounted()) { + return; + } setFocusedPointIdx(u.legend.idx!); setFocusedPointIdxs(u.legend.idxs!.slice()); }); // default series/datapoint idx retireval config.addHook('setCursor', (u) => { - const bbox = plotCtx.getCanvasBoundingBox(); - if (!bbox) { + if (!bbox || !isMounted()) { return; } @@ -142,12 +146,23 @@ export const TooltipPlugin: React.FC = ({ }); config.addHook('setSeries', (_, idx) => { + if (!isMounted()) { + return; + } setFocusedSeriesIdx(idx); }); } - }, [plotCtx, config]); - if (!plotInstance || focusedPointIdx === null || (!isActive && sync === DashboardCursorSync.Crosshair)) { + return () => { + setCoords(null); + if (plotInstance) { + plotInstance.over.removeEventListener('mouseleave', plotMouseLeave); + plotInstance.over.removeEventListener('mouseenter', plotMouseEnter); + } + }; + }, [config, setCoords, setIsActive, setFocusedPointIdx, setFocusedPointIdxs]); + + if (focusedPointIdx === null || (!isActive && sync === DashboardCursorSync.Crosshair)) { return null; } @@ -189,10 +204,10 @@ export const TooltipPlugin: React.FC = ({ if (mode === TooltipDisplayMode.Multi) { let series: SeriesTableRowProps[] = []; - const plotSeries = plotInstance.series; + const frame = otherProps.data; + const fields = frame.fields; - for (let i = 0; i < plotSeries.length; i++) { - const frame = otherProps.data; + for (let i = 0; i < fields.length; i++) { const field = frame.fields[i]; if ( !field || diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx index 1142b0a8a36..e082454e80a 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationEditorPlugin.tsx @@ -1,7 +1,7 @@ -import React, { useLayoutEffect, useState, useCallback } from 'react'; - -import { UPlotConfigBuilder, PlotSelection, usePlotContext } from '@grafana/ui'; import { CartesianCoords2D, DataFrame, TimeZone } from '@grafana/data'; +import { PlotSelection, UPlotConfigBuilder } from '@grafana/ui'; +import React, { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { useMountedState } from 'react-use'; import { AnnotationEditor } from './annotations/AnnotationEditor'; type StartAnnotatingFn = (props: { @@ -20,22 +20,50 @@ interface AnnotationEditorPluginProps { * @alpha */ export const AnnotationEditorPlugin: React.FC = ({ data, timeZone, config, children }) => { - const plotCtx = usePlotContext(); + const plotInstance = useRef(); + const [bbox, setBbox] = useState(); const [isAddingAnnotation, setIsAddingAnnotation] = useState(false); const [selection, setSelection] = useState(null); + const isMounted = useMountedState(); const clearSelection = useCallback(() => { setSelection(null); - const plotInstance = plotCtx.plot; - if (plotInstance) { - plotInstance.setSelect({ top: 0, left: 0, width: 0, height: 0 }); + + if (plotInstance.current) { + plotInstance.current.setSelect({ top: 0, left: 0, width: 0, height: 0 }); } setIsAddingAnnotation(false); - }, [plotCtx]); + }, [setIsAddingAnnotation, setSelection]); useLayoutEffect(() => { let annotating = false; + config.addHook('init', (u) => { + plotInstance.current = u; + // Wrap all setSelect hooks to prevent them from firing if user is annotating + const setSelectHooks = u.hooks.setSelect; + + if (setSelectHooks) { + for (let i = 0; i < setSelectHooks.length; i++) { + const hook = setSelectHooks[i]; + + if (hook !== setSelect) { + setSelectHooks[i] = (...args) => { + !annotating && hook!(...args); + }; + } + } + } + }); + + // cache uPlot plotting area bounding box + config.addHook('syncRect', (u, rect) => { + if (!isMounted()) { + return; + } + setBbox(rect); + }); + const setSelect = (u: uPlot) => { if (annotating) { setIsAddingAnnotation(true); @@ -55,23 +83,6 @@ export const AnnotationEditorPlugin: React.FC = ({ config.addHook('setSelect', setSelect); - config.addHook('init', (u) => { - // Wrap all setSelect hooks to prevent them from firing if user is annotating - const setSelectHooks = u.hooks.setSelect; - - if (setSelectHooks) { - for (let i = 0; i < setSelectHooks.length; i++) { - const hook = setSelectHooks[i]; - - if (hook !== setSelect) { - setSelectHooks[i] = (...args) => { - !annotating && hook!(...args); - }; - } - } - } - }); - config.setCursor({ bind: { mousedown: (u, targ, handler) => (e) => { @@ -91,21 +102,15 @@ export const AnnotationEditorPlugin: React.FC = ({ }, }, }); - }, [config]); + }, [config, setBbox, isMounted]); const startAnnotating = useCallback( ({ coords }) => { - if (!plotCtx || !plotCtx.plot || !coords) { + if (!plotInstance.current || !bbox || !coords) { return; } - const bbox = plotCtx.getCanvasBoundingBox(); - - if (!bbox) { - return; - } - - const min = plotCtx.plot.posToVal(coords.plotCanvas.x, 'x'); + const min = plotInstance.current.posToVal(coords.plotCanvas.x, 'x'); if (!min) { return; @@ -123,18 +128,25 @@ export const AnnotationEditorPlugin: React.FC = ({ }); setIsAddingAnnotation(true); }, - [plotCtx] + [bbox] ); return ( <> - {isAddingAnnotation && selection && ( + {isAddingAnnotation && selection && bbox && ( )} {children ? children({ startAnnotating }) : null} diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx index 9205b840427..d9a24dd3234 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin.tsx @@ -1,5 +1,5 @@ import { colorManipulator, DataFrame, DataFrameFieldIndex, DataFrameView, TimeZone } from '@grafana/data'; -import { EventsCanvas, UPlotConfigBuilder, usePlotContext, useTheme } from '@grafana/ui'; +import { EventsCanvas, UPlotConfigBuilder, useTheme2 } from '@grafana/ui'; import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { AnnotationMarker } from './annotations/AnnotationMarker'; @@ -10,8 +10,8 @@ interface AnnotationsPluginProps { } export const AnnotationsPlugin: React.FC = ({ annotations, timeZone, config }) => { - const theme = useTheme(); - const plotCtx = usePlotContext(); + const theme = useTheme2(); + const plotInstance = useRef(); const annotationsRef = useRef>>(); @@ -27,6 +27,10 @@ export const AnnotationsPlugin: React.FC = ({ annotation }, [annotations]); useLayoutEffect(() => { + config.addHook('init', (u) => { + plotInstance.current = u; + }); + config.addHook('draw', (u) => { // Render annotation lines on the canvas /** @@ -86,32 +90,47 @@ export const AnnotationsPlugin: React.FC = ({ annotation }); }, [config, theme]); - const mapAnnotationToXYCoords = useCallback( - (frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - const view = new DataFrameView(frame); - const annotation = view.get(dataFrameFieldIndex.fieldIndex); - const plotInstance = plotCtx.plot; - if (!annotation.time || !plotInstance) { - return undefined; - } - let x = plotInstance.valToPos(annotation.time, 'x'); + const mapAnnotationToXYCoords = useCallback((frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + const view = new DataFrameView(frame); + const annotation = view.get(dataFrameFieldIndex.fieldIndex); - if (x < 0) { - x = 0; - } - return { - x, - y: plotInstance.bbox.height / window.devicePixelRatio + 4, - }; - }, - [plotCtx] - ); + if (!annotation.time || !plotInstance.current) { + return undefined; + } + let x = plotInstance.current.valToPos(annotation.time, 'x'); + + if (x < 0) { + x = 0; + } + return { + x, + y: plotInstance.current.bbox.height / window.devicePixelRatio + 4, + }; + }, []); const renderMarker = useCallback( (frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + let markerStyle; const view = new DataFrameView(frame); const annotation = view.get(dataFrameFieldIndex.fieldIndex); - return ; + const isRegionAnnotation = Boolean(annotation.isRegion); + + if (isRegionAnnotation && plotInstance.current) { + let x0 = plotInstance.current.valToPos(annotation.time, 'x'); + let x1 = plotInstance.current.valToPos(annotation.timeEnd, 'x'); + + // markers are rendered relatively to uPlot canvas overly, not caring about axes width + if (x0 < 0) { + x0 = 0; + } + + if (x1 > plotInstance.current.bbox.width / window.devicePixelRatio) { + x1 = plotInstance.current.bbox.width / window.devicePixelRatio; + } + markerStyle = { width: `${x1 - x0}px` }; + } + + return ; }, [timeZone] ); diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index d46fe3b03ff..23e8d6fc559 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -9,7 +9,6 @@ import { MenuGroup, MenuItem, UPlotConfigBuilder, - usePlotContext, } from '@grafana/ui'; import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data'; import { useClickAway } from 'react-use'; @@ -40,7 +39,6 @@ export const ContextMenuPlugin: React.FC = ({ replaceVariables, ...otherProps }) => { - const plotCtx = usePlotContext(); const plotCanvas = useRef(); const [coords, setCoords] = useState(null); const [point, setPoint] = useState(null); @@ -61,8 +59,9 @@ export const ContextMenuPlugin: React.FC = ({ // Add uPlot hooks to the config, or re-add when the config changed useLayoutEffect(() => { + let bbox: DOMRect | undefined = undefined; + const onMouseCapture = (e: MouseEvent) => { - const bbox = plotCtx.getCanvasBoundingBox(); let update = { viewport: { x: e.clientX, @@ -85,6 +84,11 @@ export const ContextMenuPlugin: React.FC = ({ setCoords(update); }; + // cache uPlot plotting area bounding box + config.addHook('syncRect', (u, rect) => { + bbox = rect; + }); + config.addHook('init', (u) => { const canvas = u.over; plotCanvas.current = canvas || undefined; @@ -137,7 +141,7 @@ export const ContextMenuPlugin: React.FC = ({ }); } }); - }, [config, openMenu, setCoords, setPoint, plotCtx]); + }, [config, openMenu, setCoords, setPoint]); const defaultItems = useMemo(() => { return otherProps.defaultItems diff --git a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx index 7c34ee868b5..31ef89bef67 100644 --- a/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ExemplarsPlugin.tsx @@ -7,8 +7,8 @@ import { TIME_SERIES_TIME_FIELD_NAME, TIME_SERIES_VALUE_FIELD_NAME, } from '@grafana/data'; -import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder, usePlotContext } from '@grafana/ui'; -import React, { useCallback } from 'react'; +import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui'; +import React, { useCallback, useLayoutEffect, useRef } from 'react'; import { ExemplarMarker } from './ExemplarMarker'; interface ExemplarsPluginProps { @@ -19,42 +19,44 @@ interface ExemplarsPluginProps { } export const ExemplarsPlugin: React.FC = ({ exemplars, timeZone, getFieldLinks, config }) => { - const plotCtx = usePlotContext(); + const plotInstance = useRef(); - const mapExemplarToXYCoords = useCallback( - (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { - const plotInstance = plotCtx.plot; - const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME); - const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); + useLayoutEffect(() => { + config.addHook('init', (u) => { + plotInstance.current = u; + }); + }, [config]); - if (!time || !value || !plotInstance) { - return undefined; - } + const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { + const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME); + const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME); - // Filter x, y scales out - const yScale = - Object.keys(plotInstance.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT; + if (!time || !value || !plotInstance.current) { + return undefined; + } - const yMin = plotInstance.scales[yScale].min; - const yMax = plotInstance.scales[yScale].max; + // Filter x, y scales out + const yScale = + Object.keys(plotInstance.current.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT; - let y = value.values.get(dataFrameFieldIndex.fieldIndex); - // To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph - if (yMin != null && y < yMin) { - y = yMin; - } + const yMin = plotInstance.current.scales[yScale].min; + const yMax = plotInstance.current.scales[yScale].max; - if (yMax != null && y > yMax) { - y = yMax; - } + let y = value.values.get(dataFrameFieldIndex.fieldIndex); + // To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph + if (yMin != null && y < yMin) { + y = yMin; + } - return { - x: plotInstance.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'), - y: plotInstance.valToPos(y, yScale), - }; - }, - [plotCtx] - ); + if (yMax != null && y > yMax) { + y = yMax; + } + + return { + x: plotInstance.current.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'), + y: plotInstance.current.valToPos(y, yScale), + }; + }, []); const renderMarker = useCallback( (dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => { diff --git a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx index bf20b7c01d0..6186365b1f8 100644 --- a/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx +++ b/public/app/plugins/panel/timeseries/plugins/annotations/AnnotationEditor.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react'; +import React, { HTMLAttributes, useState } from 'react'; import { usePopper } from 'react-popper'; import { css, cx } from '@emotion/css'; -import { PlotSelection, usePlotContext, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; +import { PlotSelection, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { colorManipulator, DataFrame, getDisplayProcessor, GrafanaTheme2, TimeZone } from '@grafana/data'; import { getCommonAnnotationStyles } from '../styles'; import { AnnotationEditorForm } from './AnnotationEditorForm'; -interface AnnotationEditorProps { +interface AnnotationEditorProps extends HTMLAttributes { data: DataFrame; timeZone: TimeZone; selection: PlotSelection; @@ -22,11 +22,11 @@ export const AnnotationEditor: React.FC = ({ data, selection, annotation, + style, }) => { const theme = useTheme2(); const styles = useStyles2(getStyles); const commonStyles = useStyles2(getCommonAnnotationStyles); - const plotCtx = usePlotContext(); const [popperTrigger, setPopperTrigger] = useState(null); const [editorPopover, setEditorPopover] = useState(null); @@ -43,11 +43,6 @@ export const AnnotationEditor: React.FC = ({ ], }); - if (!plotCtx || !plotCtx.getCanvasBoundingBox()) { - return null; - } - const canvasBbox = plotCtx.getCanvasBoundingBox(); - let xField = data.fields[0]; if (!xField) { return null; @@ -59,13 +54,7 @@ export const AnnotationEditor: React.FC = ({ <>
{ timeZone: TimeZone; annotation: AnnotationsDataFrameViewDTO; } @@ -26,11 +26,10 @@ const POPPER_CONFIG = { ], }; -export function AnnotationMarker({ annotation, timeZone }: Props) { +export function AnnotationMarker({ annotation, timeZone, style }: Props) { + const { canAddAnnotations, ...panelCtx } = usePanelContext(); const commonStyles = useStyles2(getCommonAnnotationStyles); const styles = useStyles2(getStyles); - const plotCtx = usePlotContext(); - const { canAddAnnotations, ...panelCtx } = usePanelContext(); const [isOpen, setIsOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -101,24 +100,9 @@ export function AnnotationMarker({ annotation, timeZone }: Props) {
); - if (isRegionAnnotation && plotCtx.plot) { - let x0 = plotCtx.plot!.valToPos(annotation.time, 'x'); - let x1 = plotCtx.plot!.valToPos(annotation.timeEnd, 'x'); - - // markers are rendered relatively to uPlot canvas overly, not caring about axes width - if (x0 < 0) { - x0 = 0; - } - - if (x1 > plotCtx.plot!.bbox.width / window.devicePixelRatio) { - x1 = plotCtx.plot!.bbox.width / window.devicePixelRatio; - } - + if (isRegionAnnotation) { marker = ( -
+
); } return (