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:
@@ -250,7 +250,6 @@ export { UPlotChart } from './uPlot/Plot';
|
|||||||
export { PlotLegend } from './uPlot/PlotLegend';
|
export { PlotLegend } from './uPlot/PlotLegend';
|
||||||
export * from './uPlot/geometries';
|
export * from './uPlot/geometries';
|
||||||
export * from './uPlot/plugins';
|
export * from './uPlot/plugins';
|
||||||
export { usePlotContext } from './uPlot/context';
|
|
||||||
export { PlotTooltipInterpolator, PlotSelection } from './uPlot/types';
|
export { PlotTooltipInterpolator, PlotSelection } from './uPlot/types';
|
||||||
export { GraphNG, GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG';
|
export { GraphNG, GraphNGProps, FIXED_UNIT } from './GraphNG/GraphNG';
|
||||||
export { TimeSeries } from './TimeSeries/TimeSeries';
|
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 uPlot, { Options } from 'uplot';
|
||||||
import { PlotContext, PlotContextType } from './context';
|
|
||||||
import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils';
|
import { DEFAULT_PLOT_CONFIG, pluginLog } from './utils';
|
||||||
import { PlotProps } from './types';
|
import { PlotProps } from './types';
|
||||||
|
|
||||||
@@ -27,7 +26,7 @@ function sameTimeRange(prevProps: PlotProps, nextProps: PlotProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UPlotChartState = {
|
type UPlotChartState = {
|
||||||
ctx: PlotContextType;
|
plot: uPlot | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,35 +43,24 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
ctx: {
|
plot: null,
|
||||||
plot: null,
|
|
||||||
getCanvasBoundingBox: () => {
|
|
||||||
return this.plotCanvasBBox.current;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
reinitPlot() {
|
reinitPlot() {
|
||||||
let { ctx } = this.state;
|
|
||||||
let { width, height, plotRef } = this.props;
|
let { width, height, plotRef } = this.props;
|
||||||
|
|
||||||
ctx.plot?.destroy();
|
this.state.plot?.destroy();
|
||||||
|
|
||||||
if (width === 0 && height === 0) {
|
if (width === 0 && height === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.config.addHook('syncRect', (u, rect) => {
|
|
||||||
(this.plotCanvasBBox as MutableRefObject<any>).current = rect;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.config.addHook('setSize', (u) => {
|
this.props.config.addHook('setSize', (u) => {
|
||||||
const canvas = u.over;
|
const canvas = u.over;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(this.plotCanvasBBox as MutableRefObject<any>).current = canvas.getBoundingClientRect();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const config: Options = {
|
const config: Options = {
|
||||||
@@ -90,13 +78,7 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
|||||||
plotRef(plot);
|
plotRef(plot);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState((s) => ({
|
this.setState({ plot });
|
||||||
...s,
|
|
||||||
ctx: {
|
|
||||||
...s.ctx,
|
|
||||||
plot,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@@ -104,23 +86,23 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.state.ctx.plot?.destroy();
|
this.state.plot?.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: PlotProps) {
|
componentDidUpdate(prevProps: PlotProps) {
|
||||||
let { ctx } = this.state;
|
let { plot } = this.state;
|
||||||
|
|
||||||
if (!sameDims(prevProps, this.props)) {
|
if (!sameDims(prevProps, this.props)) {
|
||||||
ctx.plot?.setSize({
|
plot?.setSize({
|
||||||
width: this.props.width,
|
width: this.props.width,
|
||||||
height: this.props.height,
|
height: this.props.height,
|
||||||
});
|
});
|
||||||
} else if (!sameConfig(prevProps, this.props)) {
|
} else if (!sameConfig(prevProps, this.props)) {
|
||||||
this.reinitPlot();
|
this.reinitPlot();
|
||||||
} else if (!sameData(prevProps, this.props)) {
|
} else if (!sameData(prevProps, this.props)) {
|
||||||
ctx.plot?.setData(this.props.data);
|
plot?.setData(this.props.data);
|
||||||
} else if (!sameTimeRange(prevProps, this.props)) {
|
} else if (!sameTimeRange(prevProps, this.props)) {
|
||||||
ctx.plot?.setScale('x', {
|
plot?.setScale('x', {
|
||||||
min: this.props.timeRange.from.valueOf(),
|
min: this.props.timeRange.from.valueOf(),
|
||||||
max: this.props.timeRange.to.valueOf(),
|
max: this.props.timeRange.to.valueOf(),
|
||||||
});
|
});
|
||||||
@@ -129,12 +111,10 @@ export class UPlotChart extends React.Component<PlotProps, UPlotChartState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<PlotContext.Provider value={this.state.ctx}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div style={{ position: 'relative' }}>
|
<div ref={this.plotContainer} data-testid="uplot-main-div" />
|
||||||
<div ref={this.plotContainer} data-testid="uplot-main-div" />
|
{this.props.children}
|
||||||
{this.props.children}
|
</div>
|
||||||
</div>
|
|
||||||
</PlotContext.Provider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { DataFrame, DataFrameFieldIndex } from '@grafana/data';
|
||||||
import React, { useLayoutEffect, useMemo, useState } from 'react';
|
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { usePlotContext } from '../context';
|
import { useMountedState } from 'react-use';
|
||||||
|
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
||||||
import { Marker } from './Marker';
|
import { Marker } from './Marker';
|
||||||
import { XYCanvas } from './XYCanvas';
|
import { XYCanvas } from './XYCanvas';
|
||||||
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
|
||||||
|
|
||||||
interface EventsCanvasProps {
|
interface EventsCanvasProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,21 +17,29 @@ interface EventsCanvasProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords, config }: 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
|
// 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
|
// so we need to force the re-render when the draw hook was performed by uPlot
|
||||||
const [renderToken, setRenderToken] = useState(0);
|
const [renderToken, setRenderToken] = useState(0);
|
||||||
|
const isMounted = useMountedState();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
plotInstance.current = u;
|
||||||
|
});
|
||||||
|
|
||||||
config.addHook('draw', () => {
|
config.addHook('draw', () => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setRenderToken((s) => s + 1);
|
setRenderToken((s) => s + 1);
|
||||||
});
|
});
|
||||||
}, [config, setRenderToken]);
|
}, [config, setRenderToken]);
|
||||||
|
|
||||||
const eventMarkers = useMemo(() => {
|
const eventMarkers = useMemo(() => {
|
||||||
const markers: React.ReactNode[] = [];
|
const markers: React.ReactNode[] = [];
|
||||||
const plotInstance = plotCtx.plot;
|
|
||||||
if (!plotInstance || events.length === 0) {
|
if (!plotInstance.current || events.length === 0) {
|
||||||
return markers;
|
return markers;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +59,18 @@ export function EventsCanvas({ id, events, renderEventMarker, mapEventToXYCoords
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <>{markers}</>;
|
return <>{markers}</>;
|
||||||
}, [events, renderEventMarker, renderToken, plotCtx]);
|
}, [events, renderEventMarker, renderToken]);
|
||||||
|
|
||||||
if (!plotCtx.plot) {
|
if (!plotInstance.current) {
|
||||||
return null;
|
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 React, { useMemo } from 'react';
|
||||||
import { css } from '@emotion/css';
|
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!).
|
* 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.
|
* Useful when you want to render some overlay with canvas-independent elements on top of the plot.
|
||||||
*/
|
*/
|
||||||
export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
|
export const XYCanvas: React.FC<XYCanvasProps> = ({ children, left, top }) => {
|
||||||
const plotCtx = usePlotContext();
|
|
||||||
const plotInstance = plotCtx.plot;
|
|
||||||
|
|
||||||
if (!plotInstance) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = useMemo(() => {
|
const className = useMemo(() => {
|
||||||
return css`
|
return css`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
left: ${plotInstance.bbox.left / window.devicePixelRatio}px;
|
left: ${left}px;
|
||||||
top: ${plotInstance.bbox.top / window.devicePixelRatio}px;
|
top: ${top}px;
|
||||||
`;
|
`;
|
||||||
}, [plotInstance.bbox.left, plotInstance.bbox.top]);
|
}, [left, top]);
|
||||||
|
|
||||||
return <div className={className}>{children}</div>;
|
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 {
|
import {
|
||||||
CartesianCoords2D,
|
CartesianCoords2D,
|
||||||
DashboardCursorSync,
|
DashboardCursorSync,
|
||||||
@@ -13,11 +9,15 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
} from '@grafana/data';
|
} 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 { SeriesTable, SeriesTableRowProps, VizTooltipContainer } from '../../VizTooltip';
|
||||||
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
|
||||||
import { findMidPointYPosition, pluginLog } from '../utils';
|
import { findMidPointYPosition, pluginLog } from '../utils';
|
||||||
import { useTheme2 } from '../../../themes/ThemeContext';
|
|
||||||
import uPlot from 'uplot';
|
|
||||||
|
|
||||||
interface TooltipPluginProps {
|
interface TooltipPluginProps {
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
@@ -44,13 +44,12 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const plotCtx = usePlotContext();
|
|
||||||
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
||||||
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
||||||
const [focusedPointIdxs, setFocusedPointIdxs] = useState<Array<number | null>>([]);
|
const [focusedPointIdxs, setFocusedPointIdxs] = useState<Array<number | null>>([]);
|
||||||
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
||||||
const plotInstance = plotCtx.plot;
|
|
||||||
const [isActive, setIsActive] = useState<boolean>(false);
|
const [isActive, setIsActive] = useState<boolean>(false);
|
||||||
|
const isMounted = useMountedState();
|
||||||
|
|
||||||
const pluginId = `TooltipPlugin`;
|
const pluginId = `TooltipPlugin`;
|
||||||
|
|
||||||
@@ -59,42 +58,46 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`);
|
pluginLog(pluginId, true, `Focused series: ${focusedSeriesIdx}, focused point: ${focusedPointIdx}`);
|
||||||
}, [focusedPointIdx, focusedSeriesIdx]);
|
}, [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 = () => {
|
const plotMouseLeave = () => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setCoords(null);
|
setCoords(null);
|
||||||
setIsActive(false);
|
setIsActive(false);
|
||||||
if (plotCtx.plot) {
|
plotInstance?.root.classList.remove('plot-active');
|
||||||
plotCtx.plot.root.classList.remove('plot-active');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const plotMouseEnter = () => {
|
const plotMouseEnter = () => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
if (plotCtx.plot) {
|
plotInstance?.root.classList.add('plot-active');
|
||||||
plotCtx.plot.root.classList.add('plot-active');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (plotCtx && plotCtx.plot) {
|
// cache uPlot plotting area bounding box
|
||||||
plotCtx.plot.over.addEventListener('mouseleave', plotMouseLeave);
|
config.addHook('syncRect', (u, rect) => {
|
||||||
plotCtx.plot.over.addEventListener('mouseenter', plotMouseEnter);
|
bbox = rect;
|
||||||
|
});
|
||||||
|
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
plotInstance = u;
|
||||||
|
|
||||||
|
u.over.addEventListener('mouseleave', plotMouseLeave);
|
||||||
|
u.over.addEventListener('mouseenter', plotMouseEnter);
|
||||||
|
|
||||||
if (sync === DashboardCursorSync.Crosshair) {
|
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();
|
const tooltipInterpolator = config.getTooltipInterpolator();
|
||||||
|
|
||||||
if (tooltipInterpolator) {
|
if (tooltipInterpolator) {
|
||||||
// Custom toolitp positioning
|
// Custom toolitp positioning
|
||||||
config.addHook('setCursor', (u) => {
|
config.addHook('setCursor', (u) => {
|
||||||
@@ -107,7 +110,6 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
|
||||||
if (!bbox) {
|
if (!bbox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -122,14 +124,16 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
config.addHook('setLegend', (u) => {
|
config.addHook('setLegend', (u) => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFocusedPointIdx(u.legend.idx!);
|
setFocusedPointIdx(u.legend.idx!);
|
||||||
setFocusedPointIdxs(u.legend.idxs!.slice());
|
setFocusedPointIdxs(u.legend.idxs!.slice());
|
||||||
});
|
});
|
||||||
|
|
||||||
// default series/datapoint idx retireval
|
// default series/datapoint idx retireval
|
||||||
config.addHook('setCursor', (u) => {
|
config.addHook('setCursor', (u) => {
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
if (!bbox || !isMounted()) {
|
||||||
if (!bbox) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +146,23 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
config.addHook('setSeries', (_, idx) => {
|
config.addHook('setSeries', (_, idx) => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setFocusedSeriesIdx(idx);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +204,10 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
|||||||
|
|
||||||
if (mode === TooltipDisplayMode.Multi) {
|
if (mode === TooltipDisplayMode.Multi) {
|
||||||
let series: SeriesTableRowProps[] = [];
|
let series: SeriesTableRowProps[] = [];
|
||||||
const plotSeries = plotInstance.series;
|
const frame = otherProps.data;
|
||||||
|
const fields = frame.fields;
|
||||||
|
|
||||||
for (let i = 0; i < plotSeries.length; i++) {
|
for (let i = 0; i < fields.length; i++) {
|
||||||
const frame = otherProps.data;
|
|
||||||
const field = frame.fields[i];
|
const field = frame.fields[i];
|
||||||
if (
|
if (
|
||||||
!field ||
|
!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 { 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';
|
import { AnnotationEditor } from './annotations/AnnotationEditor';
|
||||||
|
|
||||||
type StartAnnotatingFn = (props: {
|
type StartAnnotatingFn = (props: {
|
||||||
@@ -20,22 +20,50 @@ interface AnnotationEditorPluginProps {
|
|||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({ data, timeZone, config, children }) => {
|
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 [isAddingAnnotation, setIsAddingAnnotation] = useState(false);
|
||||||
const [selection, setSelection] = useState<PlotSelection | null>(null);
|
const [selection, setSelection] = useState<PlotSelection | null>(null);
|
||||||
|
const isMounted = useMountedState();
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
const clearSelection = useCallback(() => {
|
||||||
setSelection(null);
|
setSelection(null);
|
||||||
const plotInstance = plotCtx.plot;
|
|
||||||
if (plotInstance) {
|
if (plotInstance.current) {
|
||||||
plotInstance.setSelect({ top: 0, left: 0, width: 0, height: 0 });
|
plotInstance.current.setSelect({ top: 0, left: 0, width: 0, height: 0 });
|
||||||
}
|
}
|
||||||
setIsAddingAnnotation(false);
|
setIsAddingAnnotation(false);
|
||||||
}, [plotCtx]);
|
}, [setIsAddingAnnotation, setSelection]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
let annotating = false;
|
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) => {
|
const setSelect = (u: uPlot) => {
|
||||||
if (annotating) {
|
if (annotating) {
|
||||||
setIsAddingAnnotation(true);
|
setIsAddingAnnotation(true);
|
||||||
@@ -55,23 +83,6 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
|||||||
|
|
||||||
config.addHook('setSelect', setSelect);
|
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({
|
config.setCursor({
|
||||||
bind: {
|
bind: {
|
||||||
mousedown: (u, targ, handler) => (e) => {
|
mousedown: (u, targ, handler) => (e) => {
|
||||||
@@ -91,21 +102,15 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [config]);
|
}, [config, setBbox, isMounted]);
|
||||||
|
|
||||||
const startAnnotating = useCallback<StartAnnotatingFn>(
|
const startAnnotating = useCallback<StartAnnotatingFn>(
|
||||||
({ coords }) => {
|
({ coords }) => {
|
||||||
if (!plotCtx || !plotCtx.plot || !coords) {
|
if (!plotInstance.current || !bbox || !coords) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
const min = plotInstance.current.posToVal(coords.plotCanvas.x, 'x');
|
||||||
|
|
||||||
if (!bbox) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const min = plotCtx.plot.posToVal(coords.plotCanvas.x, 'x');
|
|
||||||
|
|
||||||
if (!min) {
|
if (!min) {
|
||||||
return;
|
return;
|
||||||
@@ -123,18 +128,25 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
|||||||
});
|
});
|
||||||
setIsAddingAnnotation(true);
|
setIsAddingAnnotation(true);
|
||||||
},
|
},
|
||||||
[plotCtx]
|
[bbox]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isAddingAnnotation && selection && (
|
{isAddingAnnotation && selection && bbox && (
|
||||||
<AnnotationEditor
|
<AnnotationEditor
|
||||||
selection={selection}
|
selection={selection}
|
||||||
onDismiss={clearSelection}
|
onDismiss={clearSelection}
|
||||||
onSave={clearSelection}
|
onSave={clearSelection}
|
||||||
data={data}
|
data={data}
|
||||||
timeZone={timeZone}
|
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}
|
{children ? children({ startAnnotating }) : null}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { colorManipulator, DataFrame, DataFrameFieldIndex, DataFrameView, TimeZone } from '@grafana/data';
|
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 React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||||
import { AnnotationMarker } from './annotations/AnnotationMarker';
|
import { AnnotationMarker } from './annotations/AnnotationMarker';
|
||||||
|
|
||||||
@@ -10,8 +10,8 @@ interface AnnotationsPluginProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone, config }) => {
|
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone, config }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme2();
|
||||||
const plotCtx = usePlotContext();
|
const plotInstance = useRef<uPlot>();
|
||||||
|
|
||||||
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
||||||
|
|
||||||
@@ -27,6 +27,10 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
|||||||
}, [annotations]);
|
}, [annotations]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
config.addHook('init', (u) => {
|
||||||
|
plotInstance.current = u;
|
||||||
|
});
|
||||||
|
|
||||||
config.addHook('draw', (u) => {
|
config.addHook('draw', (u) => {
|
||||||
// Render annotation lines on the canvas
|
// Render annotation lines on the canvas
|
||||||
/**
|
/**
|
||||||
@@ -86,32 +90,47 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
|||||||
});
|
});
|
||||||
}, [config, theme]);
|
}, [config, theme]);
|
||||||
|
|
||||||
const mapAnnotationToXYCoords = useCallback(
|
const mapAnnotationToXYCoords = useCallback((frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||||
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
||||||
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
|
||||||
const plotInstance = plotCtx.plot;
|
|
||||||
if (!annotation.time || !plotInstance) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let x = plotInstance.valToPos(annotation.time, 'x');
|
|
||||||
|
|
||||||
if (x < 0) {
|
if (!annotation.time || !plotInstance.current) {
|
||||||
x = 0;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
let x = plotInstance.current.valToPos(annotation.time, 'x');
|
||||||
x,
|
|
||||||
y: plotInstance.bbox.height / window.devicePixelRatio + 4,
|
if (x < 0) {
|
||||||
};
|
x = 0;
|
||||||
},
|
}
|
||||||
[plotCtx]
|
return {
|
||||||
);
|
x,
|
||||||
|
y: plotInstance.current.bbox.height / window.devicePixelRatio + 4,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderMarker = useCallback(
|
const renderMarker = useCallback(
|
||||||
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||||
|
let markerStyle;
|
||||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||||
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
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]
|
[timeZone]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
MenuGroup,
|
MenuGroup,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
UPlotConfigBuilder,
|
UPlotConfigBuilder,
|
||||||
usePlotContext,
|
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data';
|
import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data';
|
||||||
import { useClickAway } from 'react-use';
|
import { useClickAway } from 'react-use';
|
||||||
@@ -40,7 +39,6 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
|||||||
replaceVariables,
|
replaceVariables,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const plotCtx = usePlotContext();
|
|
||||||
const plotCanvas = useRef<HTMLDivElement>();
|
const plotCanvas = useRef<HTMLDivElement>();
|
||||||
const [coords, setCoords] = useState<ContextMenuSelectionCoords | null>(null);
|
const [coords, setCoords] = useState<ContextMenuSelectionCoords | null>(null);
|
||||||
const [point, setPoint] = useState<ContextMenuSelectionPoint | 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
|
// Add uPlot hooks to the config, or re-add when the config changed
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
let bbox: DOMRect | undefined = undefined;
|
||||||
|
|
||||||
const onMouseCapture = (e: MouseEvent) => {
|
const onMouseCapture = (e: MouseEvent) => {
|
||||||
const bbox = plotCtx.getCanvasBoundingBox();
|
|
||||||
let update = {
|
let update = {
|
||||||
viewport: {
|
viewport: {
|
||||||
x: e.clientX,
|
x: e.clientX,
|
||||||
@@ -85,6 +84,11 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
|||||||
setCoords(update);
|
setCoords(update);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// cache uPlot plotting area bounding box
|
||||||
|
config.addHook('syncRect', (u, rect) => {
|
||||||
|
bbox = rect;
|
||||||
|
});
|
||||||
|
|
||||||
config.addHook('init', (u) => {
|
config.addHook('init', (u) => {
|
||||||
const canvas = u.over;
|
const canvas = u.over;
|
||||||
plotCanvas.current = canvas || undefined;
|
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(() => {
|
const defaultItems = useMemo(() => {
|
||||||
return otherProps.defaultItems
|
return otherProps.defaultItems
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
TIME_SERIES_TIME_FIELD_NAME,
|
TIME_SERIES_TIME_FIELD_NAME,
|
||||||
TIME_SERIES_VALUE_FIELD_NAME,
|
TIME_SERIES_VALUE_FIELD_NAME,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder, usePlotContext } from '@grafana/ui';
|
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useLayoutEffect, useRef } from 'react';
|
||||||
import { ExemplarMarker } from './ExemplarMarker';
|
import { ExemplarMarker } from './ExemplarMarker';
|
||||||
|
|
||||||
interface ExemplarsPluginProps {
|
interface ExemplarsPluginProps {
|
||||||
@@ -19,42 +19,44 @@ interface ExemplarsPluginProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
|
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
|
||||||
const plotCtx = usePlotContext();
|
const plotInstance = useRef<uPlot>();
|
||||||
|
|
||||||
const mapExemplarToXYCoords = useCallback(
|
useLayoutEffect(() => {
|
||||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
config.addHook('init', (u) => {
|
||||||
const plotInstance = plotCtx.plot;
|
plotInstance.current = u;
|
||||||
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);
|
}, [config]);
|
||||||
|
|
||||||
if (!time || !value || !plotInstance) {
|
const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||||
return undefined;
|
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
|
if (!time || !value || !plotInstance.current) {
|
||||||
const yScale =
|
return undefined;
|
||||||
Object.keys(plotInstance.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT;
|
}
|
||||||
|
|
||||||
const yMin = plotInstance.scales[yScale].min;
|
// Filter x, y scales out
|
||||||
const yMax = plotInstance.scales[yScale].max;
|
const yScale =
|
||||||
|
Object.keys(plotInstance.current.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT;
|
||||||
|
|
||||||
let y = value.values.get(dataFrameFieldIndex.fieldIndex);
|
const yMin = plotInstance.current.scales[yScale].min;
|
||||||
// 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
|
const yMax = plotInstance.current.scales[yScale].max;
|
||||||
if (yMin != null && y < yMin) {
|
|
||||||
y = yMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (yMax != null && y > yMax) {
|
let y = value.values.get(dataFrameFieldIndex.fieldIndex);
|
||||||
y = yMax;
|
// 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 {
|
if (yMax != null && y > yMax) {
|
||||||
x: plotInstance.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'),
|
y = yMax;
|
||||||
y: plotInstance.valToPos(y, yScale),
|
}
|
||||||
};
|
|
||||||
},
|
return {
|
||||||
[plotCtx]
|
x: plotInstance.current.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'),
|
||||||
);
|
y: plotInstance.current.valToPos(y, yScale),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const renderMarker = useCallback(
|
const renderMarker = useCallback(
|
||||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
(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 { usePopper } from 'react-popper';
|
||||||
import { css, cx } from '@emotion/css';
|
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 { colorManipulator, DataFrame, getDisplayProcessor, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||||
import { getCommonAnnotationStyles } from '../styles';
|
import { getCommonAnnotationStyles } from '../styles';
|
||||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||||
|
|
||||||
interface AnnotationEditorProps {
|
interface AnnotationEditorProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
selection: PlotSelection;
|
selection: PlotSelection;
|
||||||
@@ -22,11 +22,11 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
|
|||||||
data,
|
data,
|
||||||
selection,
|
selection,
|
||||||
annotation,
|
annotation,
|
||||||
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
||||||
const plotCtx = usePlotContext();
|
|
||||||
const [popperTrigger, setPopperTrigger] = useState<HTMLDivElement | null>(null);
|
const [popperTrigger, setPopperTrigger] = useState<HTMLDivElement | null>(null);
|
||||||
const [editorPopover, setEditorPopover] = 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];
|
let xField = data.fields[0];
|
||||||
if (!xField) {
|
if (!xField) {
|
||||||
return null;
|
return null;
|
||||||
@@ -59,13 +54,7 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
|
|||||||
<Portal>
|
<Portal>
|
||||||
<>
|
<>
|
||||||
<div // div overlay matching uPlot canvas bbox
|
<div // div overlay matching uPlot canvas bbox
|
||||||
className={css`
|
style={style}
|
||||||
position: absolute;
|
|
||||||
top: ${canvasBbox!.top}px;
|
|
||||||
left: ${canvasBbox!.left}px;
|
|
||||||
width: ${canvasBbox!.width}px;
|
|
||||||
height: ${canvasBbox!.height}px;
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div // Annotation marker
|
<div // Annotation marker
|
||||||
className={cx(
|
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 { 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 { css } from '@emotion/css';
|
||||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||||
import { getCommonAnnotationStyles } from '../styles';
|
import { getCommonAnnotationStyles } from '../styles';
|
||||||
@@ -8,7 +8,7 @@ import { usePopper } from 'react-popper';
|
|||||||
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
|
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
|
||||||
import { AnnotationTooltip } from './AnnotationTooltip';
|
import { AnnotationTooltip } from './AnnotationTooltip';
|
||||||
|
|
||||||
interface Props {
|
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||||
timeZone: TimeZone;
|
timeZone: TimeZone;
|
||||||
annotation: AnnotationsDataFrameViewDTO;
|
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 commonStyles = useStyles2(getCommonAnnotationStyles);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const plotCtx = usePlotContext();
|
|
||||||
const { canAddAnnotations, ...panelCtx } = usePanelContext();
|
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [isEditing, setIsEditing] = 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)' }} />
|
<div className={commonStyles(annotation).markerTriangle} style={{ transform: 'translate3d(-100%,-50%, 0)' }} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isRegionAnnotation && plotCtx.plot) {
|
if (isRegionAnnotation) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
marker = (
|
marker = (
|
||||||
<div
|
<div className={commonStyles(annotation).markerBar} style={{ ...style, transform: 'translate3d(0,-50%, 0)' }} />
|
||||||
className={commonStyles(annotation).markerBar}
|
|
||||||
style={{ width: `${x1 - x0}px`, transform: 'translate3d(0,-50%, 0)' }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user