mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
c979b5d868
commit
e68bf87de1
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>;
|
||||
};
|
||||
|
@ -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 ||
|
||||
|
@ -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}
|
||||
|
@ -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]
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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(
|
||||
|
@ -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 (
|
||||
|
Loading…
Reference in New Issue
Block a user