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
12 changed files with 243 additions and 245 deletions

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 (