GraphNG: Remove plot context (#38928)

* Remove plot ctx usage: Tooltip plugin

* Remove plot ctx usage: Context menu plugin

* Remove plot ctx usage: Annotations plugin

* Remove plot ctx usage: Exemplars plugin

* Remove plot ctx usage: EventsCanvas plugin

* Remove plot ctx usage: EventsCanvas/XYCanvas plugin

* Remove plot ctx usage: AnnotationEditor plugin

* Remove plot ctx usage: AnnotationMarker

* Remove plot context

* Do not throw react warnings from uPlot performed hooks
This commit is contained in:
Dominik Prokop 2021-09-10 13:50:21 +02:00 committed by GitHub
parent c979b5d868
commit e68bf87de1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 243 additions and 245 deletions

View File

@ -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';

View File

@ -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<PlotProps, UPlotChartState> {
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<any>).current = rect;
});
this.props.config.addHook('setSize', (u) => {
const canvas = u.over;
if (!canvas) {
return;
}
(this.plotCanvasBBox as MutableRefObject<any>).current = canvas.getBoundingClientRect();
});
const config: Options = {
@ -90,13 +78,7 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
plotRef(plot);
}
this.setState((s) => ({
...s,
ctx: {
...s.ctx,
plot,
},
}));
this.setState({ plot });
}
componentDidMount() {
@ -104,23 +86,23 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
}
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<PlotProps, UPlotChartState> {
render() {
return (
<PlotContext.Provider value={this.state.ctx}>
<div style={{ position: 'relative' }}>
<div ref={this.plotContainer} data-testid="uplot-main-div" />
{this.props.children}
</div>
</PlotContext.Provider>
<div style={{ position: 'relative' }}>
<div ref={this.plotContainer} data-testid="uplot-main-div" />
{this.props.children}
</div>
);
}
}

View File

@ -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<PlotContextType>({} as PlotContextType);
// Exposes uPlot instance and bounding box of the entire canvas and plot area
export const usePlotContext = (): PlotContextType => {
return useContext<PlotContextType>(PlotContext);
};

View File

@ -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<uPlot>();
// 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 <XYCanvas>{eventMarkers}</XYCanvas>;
return (
<XYCanvas
left={plotInstance.current.bbox.left / window.devicePixelRatio}
top={plotInstance.current.bbox.top / window.devicePixelRatio}
>
{eventMarkers}
</XYCanvas>
);
}

View File

@ -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<XYCanvasProps> = ({ children }) => {
const plotCtx = usePlotContext();
const plotInstance = plotCtx.plot;
if (!plotInstance) {
return null;
}
export const XYCanvas: React.FC<XYCanvasProps> = ({ 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 <div className={className}>{children}</div>;
};

View File

@ -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<TooltipPluginProps> = ({
...otherProps
}) => {
const theme = useTheme2();
const plotCtx = usePlotContext();
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
const [focusedPointIdxs, setFocusedPointIdxs] = useState<Array<number | null>>([]);
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
const plotInstance = plotCtx.plot;
const [isActive, setIsActive] = useState<boolean>(false);
const isMounted = useMountedState();
const pluginId = `TooltipPlugin`;
@ -59,42 +58,46 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
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<TooltipPluginProps> = ({
return;
}
const bbox = plotCtx.getCanvasBoundingBox();
if (!bbox) {
return;
}
@ -122,14 +124,16 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
});
} 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<TooltipPluginProps> = ({
});
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<TooltipPluginProps> = ({
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 ||

View File

@ -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<AnnotationEditorPluginProps> = ({ data, timeZone, config, children }) => {
const plotCtx = usePlotContext();
const plotInstance = useRef<uPlot>();
const [bbox, setBbox] = useState<DOMRect>();
const [isAddingAnnotation, setIsAddingAnnotation] = useState(false);
const [selection, setSelection] = useState<PlotSelection | null>(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<AnnotationEditorPluginProps> = ({
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<AnnotationEditorPluginProps> = ({
},
},
});
}, [config]);
}, [config, setBbox, isMounted]);
const startAnnotating = useCallback<StartAnnotatingFn>(
({ 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<AnnotationEditorPluginProps> = ({
});
setIsAddingAnnotation(true);
},
[plotCtx]
[bbox]
);
return (
<>
{isAddingAnnotation && selection && (
{isAddingAnnotation && selection && bbox && (
<AnnotationEditor
selection={selection}
onDismiss={clearSelection}
onSave={clearSelection}
data={data}
timeZone={timeZone}
style={{
position: 'absolute',
top: `${bbox.top}px`,
left: `${bbox.left}px`,
width: `${bbox.width}px`,
height: `${bbox.height}px`,
}}
/>
)}
{children ? children({ startAnnotating }) : null}

View File

@ -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<AnnotationsPluginProps> = ({ annotations, timeZone, config }) => {
const theme = useTheme();
const plotCtx = usePlotContext();
const theme = useTheme2();
const plotInstance = useRef<uPlot>();
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
@ -27,6 +27,10 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ 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<AnnotationsPluginProps> = ({ annotation
});
}, [config, theme]);
const mapAnnotationToXYCoords = useCallback(
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(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<AnnotationsDataFrameViewDTO>(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<AnnotationsDataFrameViewDTO>(frame);
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
return <AnnotationMarker annotation={annotation} timeZone={timeZone} />;
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 <AnnotationMarker annotation={annotation} timeZone={timeZone} style={markerStyle} />;
},
[timeZone]
);

View File

@ -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<ContextMenuPluginProps> = ({
replaceVariables,
...otherProps
}) => {
const plotCtx = usePlotContext();
const plotCanvas = useRef<HTMLDivElement>();
const [coords, setCoords] = useState<ContextMenuSelectionCoords | null>(null);
const [point, setPoint] = useState<ContextMenuSelectionPoint | null>(null);
@ -61,8 +59,9 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
// 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<ContextMenuPluginProps> = ({
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<ContextMenuPluginProps> = ({
});
}
});
}, [config, openMenu, setCoords, setPoint, plotCtx]);
}, [config, openMenu, setCoords, setPoint]);
const defaultItems = useMemo(() => {
return otherProps.defaultItems

View File

@ -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<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
const plotCtx = usePlotContext();
const plotInstance = useRef<uPlot>();
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) => {

View File

@ -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<HTMLDivElement> {
data: DataFrame;
timeZone: TimeZone;
selection: PlotSelection;
@ -22,11 +22,11 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
data,
selection,
annotation,
style,
}) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const commonStyles = useStyles2(getCommonAnnotationStyles);
const plotCtx = usePlotContext();
const [popperTrigger, setPopperTrigger] = useState<HTMLDivElement | null>(null);
const [editorPopover, setEditorPopover] = useState<HTMLDivElement | null>(null);
@ -43,11 +43,6 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
],
});
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<AnnotationEditorProps> = ({
<Portal>
<>
<div // div overlay matching uPlot canvas bbox
className={css`
position: absolute;
top: ${canvasBbox!.top}px;
left: ${canvasBbox!.left}px;
width: ${canvasBbox!.width}px;
height: ${canvasBbox!.height}px;
`}
style={style}
>
<div // Annotation marker
className={cx(

View File

@ -1,6 +1,6 @@
import React, { useCallback, useRef, useState } from 'react';
import React, { HTMLAttributes, useCallback, useRef, useState } from 'react';
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
import { Portal, useStyles2, usePanelContext, usePlotContext } from '@grafana/ui';
import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
import { css } from '@emotion/css';
import { AnnotationEditorForm } from './AnnotationEditorForm';
import { getCommonAnnotationStyles } from '../styles';
@ -8,7 +8,7 @@ import { usePopper } from 'react-popper';
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
import { AnnotationTooltip } from './AnnotationTooltip';
interface Props {
interface Props extends HTMLAttributes<HTMLDivElement> {
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) {
<div className={commonStyles(annotation).markerTriangle} style={{ transform: 'translate3d(-100%,-50%, 0)' }} />
);
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 = (
<div
className={commonStyles(annotation).markerBar}
style={{ width: `${x1 - x0}px`, transform: 'translate3d(0,-50%, 0)' }}
/>
<div className={commonStyles(annotation).markerBar} style={{ ...style, transform: 'translate3d(0,-50%, 0)' }} />
);
}
return (