Commit a3d1d9a9 by Dominik Prokop Committed by GitHub

Graph NG: EventsCanvas & WIP exemplars support (#28071)

* Use annotations data observable

* WIP exemplars

* Refactor usePlotContext to use getters instead of properties

* Use DataFrame in EventsCanvas instead of custom type

* Minor tweaks
parent dc662025
......@@ -75,6 +75,7 @@ export * from './uPlot/geometries';
export { usePlotConfigContext } from './uPlot/context';
export { Canvas } from './uPlot/Canvas';
export * from './uPlot/plugins';
export { useRefreshAfterGraphRendered } from './uPlot/hooks';
export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context';
export { Gauge } from './Gauge/Gauge';
......
......@@ -8,13 +8,9 @@ interface CanvasProps {
// Ref element to render the uPlot canvas to
// This is a required child of Plot component!
export const Canvas: React.FC<CanvasProps> = ({ width, height }) => {
const plot = usePlotContext();
if (!plot) {
return null;
}
return <div ref={plot.canvasRef} />;
export const Canvas: React.FC<CanvasProps> = () => {
const plotCtx = usePlotContext();
return <div ref={plotCtx.canvasRef} />;
};
Canvas.displayName = 'Canvas';
......@@ -23,6 +23,13 @@ export const UPlotChart: React.FC<PlotProps> = props => {
const prevConfig = usePrevious(currentConfig);
const getPlotInstance = useCallback(() => {
if (!plotInstance) {
throw new Error("Plot hasn't initialised yet");
}
return plotInstance;
}, [plotInstance]);
// Main function initialising uPlot. If final config is not settled it will do nothing
const initPlot = () => {
if (!currentConfig || !canvasRef.current) {
......@@ -81,8 +88,17 @@ export const UPlotChart: React.FC<PlotProps> = props => {
// Memoize plot context
const plotCtx = useMemo(() => {
return buildPlotContext(registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance);
}, [registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance]);
return buildPlotContext(
Boolean(plotInstance),
canvasRef,
props.data,
registerPlugin,
addSeries,
addAxis,
addScale,
getPlotInstance
);
}, [plotInstance, canvasRef, props.data, registerPlugin, addSeries, addAxis, addScale, getPlotInstance]);
return (
<PlotContext.Provider value={plotCtx}>
......
......@@ -43,28 +43,29 @@ interface PlotPluginsContextType {
}
interface PlotContextType extends PlotConfigContextType, PlotPluginsContextType {
u?: uPlot;
series?: uPlot.Series[];
canvas?: PlotCanvasContextType;
isPlotReady: boolean;
getPlotInstance: () => uPlot;
getSeries: () => uPlot.Series[];
getCanvas: () => PlotCanvasContextType;
canvasRef: any;
data: DataFrame;
}
export const PlotContext = React.createContext<PlotContextType | null>(null);
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 | null => {
return useContext<PlotContextType | null>(PlotContext);
export const usePlotContext = (): PlotContextType => {
return useContext<PlotContextType>(PlotContext);
};
const throwWhenNoContext = (name: string) => {
throw new Error(`${name} must be used within PlotContext`);
throw new Error(`${name} must be used within PlotContext or PlotContext is not ready yet!`);
};
// Exposes API for registering uPlot plugins
export const usePlotPluginContext = (): PlotPluginsContextType => {
const ctx = useContext(PlotContext);
if (!ctx) {
if (Object.keys(ctx).length === 0) {
throwWhenNoContext('usePlotPluginContext');
}
return {
......@@ -74,7 +75,8 @@ export const usePlotPluginContext = (): PlotPluginsContextType => {
// Exposes API for building uPlot config
export const usePlotConfigContext = (): PlotConfigContextType => {
const ctx = useContext(PlotContext);
const ctx = usePlotContext();
if (!ctx) {
throwWhenNoContext('usePlotPluginContext');
}
......@@ -101,7 +103,7 @@ interface PlotDataAPI {
}
export const usePlotData = (): PlotDataAPI => {
const ctx = useContext(PlotContext);
const ctx = usePlotContext();
const getField = useCallback(
(idx: number) => {
......@@ -149,7 +151,7 @@ export const usePlotData = (): PlotDataAPI => {
}
return {
data: ctx!.data,
data: ctx.data,
getField,
getFieldValue,
getFieldConfig,
......@@ -158,45 +160,35 @@ export const usePlotData = (): PlotDataAPI => {
};
};
// Returns bbox of the plot canvas (only the graph, no axes)
export const usePlotCanvas = (): PlotCanvasContextType | null => {
const ctx = usePlotContext();
if (!ctx) {
throwWhenNoContext('usePlotCanvas');
}
return ctx!.canvas || null;
};
export const buildPlotContext = (
isPlotReady: boolean,
canvasRef: any,
data: DataFrame,
registerPlugin: any,
addSeries: any,
addAxis: any,
addScale: any,
canvasRef: any,
data: DataFrame,
u?: uPlot
): PlotContextType | null => {
getPlotInstance: () => uPlot
): PlotContextType => {
return {
u,
series: u?.series,
canvas: u
? {
width: u.width,
height: u.height,
plot: {
width: u.bbox.width / window.devicePixelRatio,
height: u.bbox.height / window.devicePixelRatio,
top: u.bbox.top / window.devicePixelRatio,
left: u.bbox.left / window.devicePixelRatio,
},
}
: undefined,
isPlotReady,
canvasRef,
data,
registerPlugin,
addSeries,
addAxis,
addScale,
canvasRef,
data,
getPlotInstance,
getSeries: () => getPlotInstance().series,
getCanvas: () => ({
width: getPlotInstance().width,
height: getPlotInstance().height,
plot: {
width: getPlotInstance().bbox.width / window.devicePixelRatio,
height: getPlotInstance().bbox.height / window.devicePixelRatio,
top: getPlotInstance().bbox.top / window.devicePixelRatio,
left: getPlotInstance().bbox.left / window.devicePixelRatio,
},
}),
};
};
import React, { useMemo } from 'react';
import { DataFrame, DataFrameView } from '@grafana/data';
import { usePlotContext } from '../context';
import { Marker } from './Marker';
import { XYCanvas } from './XYCanvas';
import { useRefreshAfterGraphRendered } from '../hooks';
interface EventsCanvasProps<T> {
id: string;
events: DataFrame[];
renderEventMarker: (event: T) => React.ReactNode;
mapEventToXYCoords: (event: T) => { x: number; y: number } | undefined;
}
export function EventsCanvas<T>({ id, events, renderEventMarker, mapEventToXYCoords }: EventsCanvasProps<T>) {
const plotCtx = usePlotContext();
const renderToken = useRefreshAfterGraphRendered(id);
const eventMarkers = useMemo(() => {
const markers: React.ReactNode[] = [];
if (!plotCtx.isPlotReady || events.length === 0) {
return markers;
}
for (let i = 0; i < events.length; i++) {
const view = new DataFrameView<T>(events[i]);
for (let j = 0; j < view.length; j++) {
const event = view.get(j);
const coords = mapEventToXYCoords(event);
if (!coords) {
continue;
}
markers.push(
<Marker {...coords} key={`${id}-marker-${i}-${j}`}>
{renderEventMarker(event)}
</Marker>
);
}
}
return <>{markers}</>;
}, [events, renderEventMarker, renderToken, plotCtx.isPlotReady]);
if (!plotCtx.isPlotReady) {
return null;
}
return <XYCanvas>{eventMarkers}</XYCanvas>;
}
import { css } from 'emotion';
import React from 'react';
interface MarkerProps {
/** x position relative to plotting area bounding box*/
x: number;
/** y position relative to plotting area bounding box*/
y: number;
}
// An abstraction over a component rendered within a chart canvas.
// Marker is rendered with DOM coords of the chart bounding box.
export const Marker: React.FC<MarkerProps> = ({ x, y, children }) => {
return (
<div
className={css`
position: absolute;
top: ${y}px;
left: ${x}px;
transform: translate3d(-50%, -50%, 0);
`}
>
{children}
</div>
);
};
import { usePlotContext } from '../context';
import React from 'react';
import { css } from 'emotion';
interface XYCanvasProps {}
/**
* 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 plotContext = usePlotContext();
if (!plotContext.isPlotReady) {
return null;
}
return (
<div
className={css`
position: absolute;
overflow: visible;
left: ${plotContext.getPlotInstance().bbox.left / window.devicePixelRatio}px;
top: ${plotContext.getPlotInstance().bbox.top / window.devicePixelRatio}px;
`}
>
{children}
</div>
);
};
......@@ -4,4 +4,7 @@ import { Point } from './Point';
import { Axis } from './Axis';
import { Scale } from './Scale';
import { SeriesGeometry } from './SeriesGeometry';
export { Area, Line, Point, SeriesGeometry, Axis, Scale };
import { XYCanvas } from './XYCanvas';
import { Marker } from './Marker';
import { EventsCanvas } from './EventsCanvas';
export { Area, Line, Point, SeriesGeometry, Axis, Scale, XYCanvas, Marker, EventsCanvas };
......@@ -3,6 +3,7 @@ import { PlotPlugin } from './types';
import { pluginLog } from './utils';
import uPlot from 'uplot';
import { getTimeZoneInfo, TimeZone } from '@grafana/data';
import { usePlotPluginContext } from './context';
export const usePlotPlugins = () => {
/**
......@@ -246,3 +247,32 @@ export const usePlotConfig = (width: number, height: number, timeZone: TimeZone)
currentConfig,
};
};
/**
* Forces re-render of a component when uPlots's draw hook is fired.
* This hook is usefull in scenarios when you want to reposition XYCanvas elements when i.e. plot size changes
* @param pluginId - id under which the plugin will be registered
*/
export const useRefreshAfterGraphRendered = (pluginId: string) => {
const pluginsApi = usePlotPluginContext();
const [renderToken, setRenderToken] = useState(0);
useEffect(() => {
const unregister = pluginsApi.registerPlugin({
id: pluginId,
hooks: {
// refresh events when uPlot draws
draw: u => {
setRenderToken(c => c + 1);
return;
},
},
});
return () => {
unregister();
};
}, []);
return renderToken;
};
import React, { useState, useEffect, useCallback } from 'react';
import { PlotPluginProps } from '../types';
import { usePlotCanvas, usePlotPluginContext } from '../context';
import { usePlotContext, usePlotPluginContext } from '../context';
import { pluginLog } from '../utils';
interface Selection {
......@@ -33,10 +33,9 @@ interface SelectionPluginProps extends PlotPluginProps {
export const SelectionPlugin: React.FC<SelectionPluginProps> = ({ onSelect, onDismiss, lazy, id, children }) => {
const pluginId = `SelectionPlugin:${id}`;
const pluginsApi = usePlotPluginContext();
const canvas = usePlotCanvas();
const plotCtx = usePlotContext();
const [selection, setSelection] = useState<Selection | null>(null);
//
useEffect(() => {
if (!lazy && selection) {
pluginLog(pluginId, false, 'selected', selection);
......@@ -77,7 +76,7 @@ export const SelectionPlugin: React.FC<SelectionPluginProps> = ({ onSelect, onDi
};
}, []);
if (!children || !canvas || !selection) {
if (!plotCtx.isPlotReady || !children || !selection) {
return null;
}
......
......@@ -25,7 +25,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
return (
<CursorPlugin id={pluginId}>
{({ focusedSeriesIdx, focusedPointIdx, coords }) => {
if (!plotContext || !plotContext.series) {
if (!plotContext.isPlotReady) {
return null;
}
......@@ -46,7 +46,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
series={[
{
// stroke is typed as CanvasRenderingContext2D['strokeStyle'] - we are using strings only for now
color: plotContext.series![focusedSeriesIdx!].stroke as string,
color: plotContext.getSeries()[focusedSeriesIdx!].stroke as string,
label: getFieldDisplayName(field, data),
value: fieldFmt(field.values.get(focusedPointIdx)).text,
},
......@@ -70,7 +70,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
...agg,
{
// stroke is typed as CanvasRenderingContext2D['strokeStyle'] - we are using strings only for now
color: plotContext.series![i].stroke as string,
color: plotContext.getSeries()[i].stroke as string,
label: getFieldDisplayName(f, data),
value: formattedValueToString(f.display!(f.values.get(focusedPointIdx!))),
isActive: focusedSeriesIdx === i,
......
......@@ -34,6 +34,7 @@ import { VizLayout } from './VizLayout';
import { Axis } from '@grafana/ui/src/components/uPlot/geometries/Axis';
import { timeFormatToTemplate } from '@grafana/ui/src/components/uPlot/utils';
import { AnnotationsPlugin } from './plugins/AnnotationsPlugin';
import { ExemplarsPlugin } from './plugins/ExemplarsPlugin';
interface GraphPanelProps extends PanelProps<Options> {}
......@@ -238,6 +239,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
<ZoomPlugin onZoom={onChangeTimeRange} />
<ContextMenuPlugin />
{data.annotations && <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} />}
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
{/* TODO: */}
{/*<AnnotationsEditorPlugin />*/}
......
import React, { useCallback, useRef, useState } from 'react';
import { AnnotationEvent, GrafanaTheme } from '@grafana/data';
import { GrafanaTheme } from '@grafana/data';
import { HorizontalGroup, Portal, Tag, TooltipContainer, useStyles } from '@grafana/ui';
import { css, cx } from 'emotion';
import { css } from 'emotion';
interface AnnotationMarkerProps {
formatTime: (value: number) => string;
annotationEvent: AnnotationEvent;
x: number;
time: string;
text: string;
tags: string[];
}
export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ annotationEvent, x, formatTime }) => {
export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text, tags }) => {
const styles = useStyles(getAnnotationMarkerStyles);
const [isOpen, setIsOpen] = useState(false);
const markerRef = useRef<HTMLDivElement>(null);
......@@ -47,14 +47,14 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ annotationEv
>
<div ref={annotationPopoverRef} className={styles.wrapper}>
<div className={styles.header}>
<span className={styles.title}>{annotationEvent.title}</span>
{annotationEvent.time && <span className={styles.time}>{formatTime(annotationEvent.time)}</span>}
{/*<span className={styles.title}>{annotationEvent.title}</span>*/}
{time && <span className={styles.time}>{time}</span>}
</div>
<div className={styles.body}>
{annotationEvent.text && <div dangerouslySetInnerHTML={{ __html: annotationEvent.text }} />}
{text && <div dangerouslySetInnerHTML={{ __html: text }} />}
<>
<HorizontalGroup spacing="xs" wrap>
{annotationEvent.tags?.map((t, i) => (
{tags?.map((t, i) => (
<Tag name={t} key={`${t}-${i}`} />
))}
</HorizontalGroup>
......@@ -63,21 +63,11 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ annotationEv
</div>
</TooltipContainer>
);
}, [annotationEvent]);
}, [time, tags, text]);
return (
<>
<div
ref={markerRef}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={cx(
styles.markerWrapper,
css`
left: ${x - 8}px;
`
)}
>
<div ref={markerRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={styles.markerWrapper}>
<div className={styles.marker} />
</div>
{isOpen && <Portal>{renderMarker()}</Portal>}
......@@ -93,8 +83,6 @@ const getAnnotationMarkerStyles = (theme: GrafanaTheme) => {
return {
markerWrapper: css`
padding: 0 4px 4px 4px;
position: absolute;
top: 0;
`,
marker: css`
width: 0;
......
import { AnnotationEvent, DataFrame, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
import { usePlotContext, usePlotPluginContext, useTheme } from '@grafana/ui';
import { getAnnotationsFromData } from 'app/features/annotations/standardAnnotationSupport';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { css } from 'emotion';
import { DataFrame, DataFrameView, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
import { EventsCanvas, usePlotContext, usePlotPluginContext, useTheme } from '@grafana/ui';
import React, { useCallback, useEffect, useRef } from 'react';
import { AnnotationMarker } from './AnnotationMarker';
import { useObservable } from 'react-use';
interface AnnotationsPluginProps {
annotations: DataFrame[];
timeZone: TimeZone;
}
interface AnnotationsDataFrameViewDTO {
time: number;
text: string;
tags: string[];
}
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone }) => {
const pluginId = 'AnnotationsPlugin';
const plotCtx = usePlotContext();
const pluginsApi = usePlotPluginContext();
const plotContext = usePlotContext();
const annotationsRef = useRef<AnnotationEvent[]>();
const [renderCounter, setRenderCounter] = useState(0);
const theme = useTheme();
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
const timeFormatter = useCallback(
(value: number) => {
......@@ -29,54 +32,17 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
[timeZone]
);
const annotationEventsStream = useMemo(() => getAnnotationsFromData(annotations), [annotations]);
const annotationsData = useObservable<AnnotationEvent[]>(annotationEventsStream);
const annotationMarkers = useMemo(() => {
if (!plotContext || !plotContext?.u) {
return null;
}
const markers: AnnotationEvent[] = [];
useEffect(() => {
if (plotCtx.isPlotReady && annotations.length > 0) {
const views: Array<DataFrameView<AnnotationsDataFrameViewDTO>> = [];
if (!annotationsData) {
return markers;
for (const frame of annotations) {
views.push(new DataFrameView(frame));
}
for (let i = 0; i < annotationsData.length; i++) {
const annotation = annotationsData[i];
if (!annotation.time) {
continue;
annotationsRef.current = views;
}
const xpos = plotContext.u.valToPos(annotation.time / 1000, 'x');
markers.push(
<AnnotationMarker
x={xpos}
key={`${annotation.time}-${i}`}
formatTime={timeFormatter}
annotationEvent={annotation}
/>
);
}
return (
<div
className={css`
position: absolute;
left: ${plotContext.u.bbox.left / window.devicePixelRatio}px;
top: ${plotContext.u.bbox.top / window.devicePixelRatio +
plotContext.u.bbox.height / window.devicePixelRatio}px;
width: ${plotContext.u.bbox.width / window.devicePixelRatio}px;
height: 14px;
`}
>
{markers}
</div>
);
}, [annotationsData, timeFormatter, plotContext, renderCounter]);
// For uPlot plugin to have access to lates annotation data we need to update the data ref
useEffect(() => {
annotationsRef.current = annotationsData;
}, [annotationsData]);
}, [plotCtx.isPlotReady, annotations]);
useEffect(() => {
const unregister = pluginsApi.registerPlugin({
......@@ -91,15 +57,20 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
if (!annotationsRef.current) {
return null;
}
const ctx = u.ctx;
if (!ctx) {
return;
}
for (let i = 0; i < annotationsRef.current.length; i++) {
const annotation = annotationsRef.current[i];
const annotationsView = annotationsRef.current[i];
for (let j = 0; j < annotationsView.length; j++) {
const annotation = annotationsView.get(j);
if (!annotation.time) {
continue;
}
const xpos = u.valToPos(annotation.time / 1000, 'x', true);
ctx.beginPath();
ctx.lineWidth = 2;
......@@ -110,7 +81,7 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
ctx.stroke();
ctx.closePath();
}
setRenderCounter(c => c + 1);
}
return;
},
},
......@@ -121,9 +92,33 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
};
}, []);
if (!plotContext || !plotContext.u || !plotContext.canvas) {
return null;
const mapAnnotationToXYCoords = useCallback(
(annotation: AnnotationsDataFrameViewDTO) => {
if (!annotation.time) {
return undefined;
}
return <div>{annotationMarkers}</div>;
return {
x: plotCtx.getPlotInstance().valToPos(annotation.time / 1000, 'x'),
y: plotCtx.getPlotInstance().bbox.height / window.devicePixelRatio + 4,
};
},
[plotCtx.getPlotInstance]
);
const renderMarker = useCallback(
(annotation: AnnotationsDataFrameViewDTO) => {
return <AnnotationMarker time={timeFormatter(annotation.time)} text={annotation.text} tags={annotation.tags} />;
},
[timeFormatter]
);
return (
<EventsCanvas<AnnotationsDataFrameViewDTO>
id="annotations"
events={annotations}
renderEventMarker={renderMarker}
mapEventToXYCoords={mapAnnotationToXYCoords}
/>
);
};
import React, { useCallback, useRef, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { HorizontalGroup, Portal, Tag, TooltipContainer, useStyles } from '@grafana/ui';
import { css, cx } from 'emotion';
interface ExemplarMarkerProps {
time: string;
text: string;
tags: string[];
}
export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ time, text, tags }) => {
const styles = useStyles(getExemplarMarkerStyles);
const [isOpen, setIsOpen] = useState(false);
const markerRef = useRef<HTMLDivElement>(null);
const annotationPopoverRef = useRef<HTMLDivElement>(null);
const popoverRenderTimeout = useRef<NodeJS.Timer>();
const onMouseEnter = useCallback(() => {
if (popoverRenderTimeout.current) {
clearTimeout(popoverRenderTimeout.current);
}
setIsOpen(true);
}, [setIsOpen]);
const onMouseLeave = useCallback(() => {
popoverRenderTimeout.current = setTimeout(() => {
setIsOpen(false);
}, 100);
}, [setIsOpen]);
const renderMarker = useCallback(() => {
if (!markerRef?.current) {
return null;
}
const el = markerRef.current;
const elBBox = el.getBoundingClientRect();
return (
<TooltipContainer
position={{ x: elBBox.left, y: elBBox.top + elBBox.height }}
offset={{ x: 0, y: 0 }}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={styles.tooltip}
>
<div ref={annotationPopoverRef} className={styles.wrapper}>
<div className={styles.header}>
{/*<span className={styles.title}>{exemplar.title}</span>*/}
{time && <span className={styles.time}>{time}</span>}
</div>
<div className={styles.body}>
{text && <div dangerouslySetInnerHTML={{ __html: text }} />}
<>
<HorizontalGroup spacing="xs" wrap>
{tags?.map((t, i) => (
<Tag name={t} key={`${t}-${i}`} />
))}
</HorizontalGroup>
</>
</div>
</div>
</TooltipContainer>
);
}, [time, tags, text]);
return (
<>
<div ref={markerRef} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} className={cx(styles.markerWrapper)}>
<svg viewBox="0 0 599 599" width="8" height="8">
<path id="black_diamond" fill="#000" d="M 300,575 L 575,300 L 300,25 L 25,300 L 300,575 Z" />
</svg>
</div>
{isOpen && <Portal>{renderMarker()}</Portal>}
</>
);
};
const getExemplarMarkerStyles = (theme: GrafanaTheme) => {
const bg = theme.isDark ? theme.palette.dark2 : theme.palette.white;
const headerBg = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
const shadowColor = theme.isDark ? theme.palette.black : theme.palette.white;
return {
markerWrapper: css`
padding: 0 4px 4px 4px;
width: 8px;
height: 8px;
box-sizing: content-box;
> svg {
display: block;
}
`,
marker: css`
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid ${theme.palette.red};
pointer-events: none;
`,
wrapper: css`
background: ${bg};
border: 1px solid ${headerBg};
border-radius: ${theme.border.radius.md};
max-width: 400px;
box-shadow: 0 0 20px ${shadowColor};
`,
tooltip: css`
background: none;
padding: 0;
`,
header: css`
background: ${headerBg};
padding: 6px 10px;
display: flex;
`,
title: css`
font-weight: ${theme.typography.weight.semibold};
padding-right: ${theme.spacing.md};
overflow: hidden;
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
flex-grow: 1;
`,
time: css`
color: ${theme.colors.textWeak};
font-style: italic;
font-weight: normal;
display: inline-block;
position: relative;
top: 1px;
`,
body: css`
padding: ${theme.spacing.sm};
font-weight: ${theme.typography.weight.semibold};
`,
};
};
import React, { useCallback, useEffect, useState } from 'react';
import {
ArrayVector,
DataFrame,
dateTimeFormat,
FieldType,
MutableDataFrame,
systemDateFormats,
TimeZone,
} from '@grafana/data';
import { EventsCanvas, usePlotContext } from '@grafana/ui';
import { ExemplarMarker } from './ExemplarMarker';
interface ExemplarsPluginProps {
exemplars: DataFrame[];
timeZone: TimeZone;
}
// Type representing exemplars data frame fields
interface ExemplarsDataFrameViewDTO {
time: number;
y: number;
text: string;
tags: string[];
}
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone }) => {
const plotCtx = usePlotContext();
// TEMPORARY MOCK
const [exemplarsMock, setExemplarsMock] = useState<DataFrame[]>([]);
const timeFormatter = useCallback(
(value: number) => {
return dateTimeFormat(value, {
format: systemDateFormats.fullDate,
timeZone,
});
},
[timeZone]
);
// THIS EVENT ONLY MOCKS EXEMPLAR Y VALUE!!!! TO BE REMOVED WHEN WE GET CORRECT EXEMPLARS SHAPE VIA PROPS
useEffect(() => {
if (plotCtx.isPlotReady && exemplars.length) {
const mocks: DataFrame[] = [];
for (const frame of exemplars) {
const mock = new MutableDataFrame(frame);
mock.addField({
name: 'y',
type: FieldType.number,
values: new ArrayVector(
Array(frame.length)
.fill(0)
.map(() => Math.random())
),
});
mocks.push(mock);
}
setExemplarsMock(mocks);
}
}, [plotCtx.isPlotReady, exemplars]);
const mapExemplarToXYCoords = useCallback(
(exemplar: ExemplarsDataFrameViewDTO) => {
if (!exemplar.time) {
return undefined;
}
return {
x: plotCtx.getPlotInstance().valToPos(exemplar.time / 1000, 'x'),
// exemplar.y is a temporary mock for an examplar. This Needs to be calculated according to examplar scale!
y: Math.floor((exemplar.y * plotCtx.getPlotInstance().bbox.height) / window.devicePixelRatio),
};
},
[plotCtx.getPlotInstance]
);
const renderMarker = useCallback(
(exemplar: ExemplarsDataFrameViewDTO) => {
return <ExemplarMarker time={timeFormatter(exemplar.time)} text={exemplar.text} tags={exemplar.tags} />;
},
[timeFormatter]
);
return (
<EventsCanvas<ExemplarsDataFrameViewDTO>
id="exemplars"
events={exemplarsMock}
renderEventMarker={renderMarker}
mapEventToXYCoords={mapExemplarToXYCoords}
/>
);
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment