mirror of
https://github.com/grafana/grafana.git
synced 2024-12-30 10:47:30 -06:00
Graph NG: annotations display (#27972)
* Annotations support POC * Fix markers memoization * dev dashboard update * Update public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx
This commit is contained in:
parent
a2816ee64a
commit
aa6c98f7ff
@ -22,6 +22,9 @@
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -36,11 +39,11 @@
|
||||
"alpha": 0.1
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -355,6 +358,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -371,10 +377,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -560,6 +563,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -576,10 +582,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -678,6 +681,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -694,10 +700,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -811,6 +814,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -827,10 +833,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": false,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -899,7 +902,7 @@
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"id": "custom.line.color",
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "purple",
|
||||
"mode": "fixed"
|
||||
@ -960,6 +963,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -976,10 +982,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": false,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1048,7 +1051,7 @@
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"id": "custom.line.color",
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "purple",
|
||||
"mode": "fixed"
|
||||
@ -1109,6 +1112,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -1125,10 +1131,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": false,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1197,7 +1200,7 @@
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"id": "custome.line.color",
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "purple",
|
||||
"mode": "fixed"
|
||||
@ -1258,6 +1261,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -1274,10 +1280,7 @@
|
||||
},
|
||||
"line": {
|
||||
"show": false,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1346,7 +1349,7 @@
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"id": "custom.line.color",
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "purple",
|
||||
"mode": "fixed"
|
||||
@ -1422,6 +1425,9 @@
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -1437,11 +1443,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1506,6 +1512,9 @@
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -1521,11 +1530,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1590,6 +1599,9 @@
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -1604,11 +1616,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1668,6 +1680,96 @@
|
||||
"timeShift": null,
|
||||
"title": "No tooltip",
|
||||
"type": "graph3"
|
||||
},
|
||||
{
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
"label": "Temperature",
|
||||
"side": 3,
|
||||
"width": 60
|
||||
},
|
||||
"bars": {
|
||||
"show": false
|
||||
},
|
||||
"fill": {
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
"radius": 8,
|
||||
"show": true
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "celsius"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 22
|
||||
},
|
||||
"id": 9,
|
||||
"options": {
|
||||
"graph": {
|
||||
"realTimeUpdates": false
|
||||
},
|
||||
"legend": {
|
||||
"asTable": false,
|
||||
"isVisible": true,
|
||||
"placement": "under"
|
||||
},
|
||||
"tooltipOptions": {
|
||||
"mode": "multi"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "7.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,20,90,30,5,0"
|
||||
},
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "10,25,40"
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Diff number of datapoints milt multi series tooltip",
|
||||
"transformations": [],
|
||||
"type": "graph3"
|
||||
}
|
||||
],
|
||||
"title": "Tooltip",
|
||||
@ -1688,6 +1790,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -1702,11 +1807,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -1775,6 +1880,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -1795,10 +1903,7 @@
|
||||
"show": false
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
"show": true
|
||||
},
|
||||
"points": {
|
||||
"radius": 8,
|
||||
@ -1809,6 +1914,9 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
@ -1879,6 +1987,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -1893,11 +2004,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "connected",
|
||||
"points": {
|
||||
@ -1981,6 +2092,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
@ -1995,11 +2109,11 @@
|
||||
"alpha": 0
|
||||
},
|
||||
"line": {
|
||||
"show": true,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -2031,7 +2145,7 @@
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 9,
|
||||
"id": 43,
|
||||
"options": {
|
||||
"graph": {
|
||||
"realTimeUpdates": false
|
||||
@ -2083,6 +2197,9 @@
|
||||
"datasource": "gdev-testdata",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"align": null,
|
||||
"axis": {
|
||||
@ -2098,11 +2215,11 @@
|
||||
"alpha": 0.2
|
||||
},
|
||||
"line": {
|
||||
"show": false,
|
||||
"width": 1,
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
}
|
||||
},
|
||||
"show": false,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
@ -2175,7 +2292,7 @@
|
||||
"value": false
|
||||
},
|
||||
{
|
||||
"id": "custom.line.color",
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "purple",
|
||||
"mode": "fixed"
|
||||
@ -2229,6 +2346,124 @@
|
||||
],
|
||||
"title": "Streaming",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"datasource": null,
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 17
|
||||
},
|
||||
"id": 45,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": null,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"custom": {
|
||||
"axis": {
|
||||
"grid": true,
|
||||
"label": "",
|
||||
"side": 3,
|
||||
"width": 60
|
||||
},
|
||||
"bars": {
|
||||
"show": false
|
||||
},
|
||||
"fill": {
|
||||
"alpha": 0.1
|
||||
},
|
||||
"line": {
|
||||
"color": {
|
||||
"mode": "fixed"
|
||||
},
|
||||
"show": true,
|
||||
"width": 1
|
||||
},
|
||||
"nullValues": "null",
|
||||
"points": {
|
||||
"radius": 4,
|
||||
"show": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 11,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 18
|
||||
},
|
||||
"id": 46,
|
||||
"options": {
|
||||
"graph": {
|
||||
"realTimeUpdates": false
|
||||
},
|
||||
"legend": {
|
||||
"asTable": false,
|
||||
"isVisible": true,
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltipOptions": {
|
||||
"mode": "single"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "7.3.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "B",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "C",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "D",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "E",
|
||||
"scenarioId": "random_walk"
|
||||
},
|
||||
{
|
||||
"refId": "F",
|
||||
"scenarioId": "annotations",
|
||||
"stringInput": ""
|
||||
}
|
||||
],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "React NG graph",
|
||||
"type": "graph3"
|
||||
}
|
||||
],
|
||||
"title": "Annotations",
|
||||
"type": "row"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
@ -2239,15 +2474,14 @@
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"from": "now-2d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
|
||||
},
|
||||
"timezone": "Africa/Accra",
|
||||
"title": "Panel Tests - Graph NG",
|
||||
"title": "Graph NG - tests",
|
||||
"uid": "TkZXxlNG3",
|
||||
"version": 61,
|
||||
"tags": ["gdev", "panel-tests", "graph"]
|
||||
"version": 65
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, { useState, useLayoutEffect, useRef } from 'react';
|
||||
import React, { useState, useLayoutEffect, useRef, HTMLAttributes } from 'react';
|
||||
import { stylesFactory } from '../../themes/stylesFactory';
|
||||
import { selectThemeVariant } from '../../themes/selectThemeVariant';
|
||||
import { css } from 'emotion';
|
||||
import { css, cx } from 'emotion';
|
||||
import { useTheme } from '../../themes/ThemeContext';
|
||||
import useWindowSize from 'react-use/lib/useWindowSize';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
|
||||
interface TooltipContainerProps {
|
||||
interface TooltipContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
position: { x: number; y: number };
|
||||
offset: { x: number; y: number };
|
||||
children?: JSX.Element;
|
||||
@ -27,7 +27,13 @@ const getTooltipContainerStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
};
|
||||
});
|
||||
|
||||
export const TooltipContainer: React.FC<TooltipContainerProps> = ({ position, offset, children }) => {
|
||||
export const TooltipContainer: React.FC<TooltipContainerProps> = ({
|
||||
position,
|
||||
offset,
|
||||
children,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
const { width, height } = useWindowSize();
|
||||
@ -70,7 +76,8 @@ export const TooltipContainer: React.FC<TooltipContainerProps> = ({ position, of
|
||||
top: 0,
|
||||
transform: `translate3d(${placement.x}px, ${placement.y}px, 0)`,
|
||||
}}
|
||||
className={styles.wrapper}
|
||||
{...otherProps}
|
||||
className={cx(styles.wrapper, className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -25,7 +25,13 @@ export const Tag = forwardRef<HTMLElement, Props>(({ name, onClick, className, c
|
||||
};
|
||||
|
||||
return (
|
||||
<span key={name} ref={ref} onClick={onTagClick} className={cx(styles.wrapper, className)} {...rest}>
|
||||
<span
|
||||
key={name}
|
||||
ref={ref}
|
||||
onClick={onTagClick}
|
||||
className={cx(styles.wrapper, className, onClick && styles.hover)}
|
||||
{...rest}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
);
|
||||
@ -50,8 +56,9 @@ const getTagStyles = (theme: GrafanaTheme, name: string, colorIndex?: number) =>
|
||||
text-shadow: none;
|
||||
padding: 3px 6px;
|
||||
border-radius: ${theme.border.radius.md};
|
||||
|
||||
:hover {
|
||||
`,
|
||||
hover: css`
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ export * from './uPlot/geometries';
|
||||
export { usePlotConfigContext } from './uPlot/context';
|
||||
export { Canvas } from './uPlot/Canvas';
|
||||
export * from './uPlot/plugins';
|
||||
export { usePlotContext, usePlotData, usePlotPluginContext } from './uPlot/context';
|
||||
|
||||
export { Gauge } from './Gauge/Gauge';
|
||||
export { Graph } from './Graph/Graph';
|
||||
@ -129,6 +130,7 @@ export { FadeTransition } from './transitions/FadeTransition';
|
||||
export { SlideOutTransition } from './transitions/SlideOutTransition';
|
||||
export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/';
|
||||
export { default as Chart } from './Chart';
|
||||
export { TooltipContainer } from './Chart/TooltipContainer';
|
||||
export { Drawer } from './Drawer/Drawer';
|
||||
export { Slider } from './Slider/Slider';
|
||||
|
||||
|
@ -82,7 +82,7 @@ export const UPlotChart: React.FC<PlotProps> = props => {
|
||||
// Memoize plot context
|
||||
const plotCtx = useMemo(() => {
|
||||
return buildPlotContext(registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance);
|
||||
}, [registerPlugin, canvasRef, props.data, plotInstance, addSeries, addAxis, addScale]);
|
||||
}, [registerPlugin, addSeries, addAxis, addScale, canvasRef, props.data, plotInstance]);
|
||||
|
||||
return (
|
||||
<PlotContext.Provider value={plotCtx}>
|
||||
|
@ -36,11 +36,11 @@ export const usePlotPlugins = () => {
|
||||
(plugin: PlotPlugin) => {
|
||||
pluginLog(plugin.id, false, 'register');
|
||||
|
||||
if (plugins.hasOwnProperty(plugin.id)) {
|
||||
throw new Error(`${plugin.id} that is already registered`);
|
||||
}
|
||||
|
||||
setPlugins(plugs => {
|
||||
if (plugs.hasOwnProperty(plugin.id)) {
|
||||
throw new Error(`${plugin.id} that is already registered`);
|
||||
}
|
||||
|
||||
return {
|
||||
...plugs,
|
||||
[plugin.id]: plugin,
|
||||
@ -58,7 +58,7 @@ export const usePlotPlugins = () => {
|
||||
});
|
||||
};
|
||||
},
|
||||
[setPlugins, plugins]
|
||||
[setPlugins]
|
||||
);
|
||||
|
||||
// When uPlot mounts let's check if there are any plugins pending registration
|
||||
@ -74,7 +74,7 @@ export const usePlotPlugins = () => {
|
||||
|
||||
return {
|
||||
arePluginsReady,
|
||||
plugins,
|
||||
plugins: plugins || {},
|
||||
registerPlugin,
|
||||
};
|
||||
};
|
||||
|
@ -10,7 +10,7 @@ interface ContextMenuPluginProps {
|
||||
}
|
||||
|
||||
export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({ onClose }) => {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
setIsOpen(!isOpen);
|
||||
|
@ -32,7 +32,7 @@ export const LegendPlugin: React.FC<LegendPluginProps> = ({ placement, displayMo
|
||||
label: getFieldDisplayName(field, data),
|
||||
isVisible: true,
|
||||
//flot vs uPlot differences
|
||||
yAxis: (field.config.custom as GraphCustomFieldConfig).axis?.side === 1 ? 3 : 1,
|
||||
yAxis: (field.config.custom as GraphCustomFieldConfig)?.axis?.side === 1 ? 3 : 1,
|
||||
});
|
||||
seriesIdx++;
|
||||
}
|
||||
|
@ -99,6 +99,9 @@ export const shouldReinitialisePlot = (prevConfig?: uPlot.Options, config?: uPlo
|
||||
}
|
||||
|
||||
if (!prevConfig && config) {
|
||||
if (config.width === 0 || config.height === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,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';
|
||||
|
||||
interface GraphPanelProps extends PanelProps<Options> {}
|
||||
|
||||
@ -232,6 +233,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
|
||||
<ZoomPlugin onZoom={onChangeTimeRange} />
|
||||
<ContextMenuPlugin />
|
||||
|
||||
{data.annotations && <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} />}
|
||||
{/* TODO: */}
|
||||
{/*<AnnotationsEditorPlugin />*/}
|
||||
</UPlotChart>
|
||||
|
145
public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx
Normal file
145
public/app/plugins/panel/graph3/plugins/AnnotationMarker.tsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { AnnotationEvent, GrafanaTheme } from '@grafana/data';
|
||||
import { HorizontalGroup, Portal, Tag, TooltipContainer, useStyles } from '@grafana/ui';
|
||||
import { css, cx } from 'emotion';
|
||||
|
||||
interface AnnotationMarkerProps {
|
||||
formatTime: (value: number) => string;
|
||||
annotationEvent: AnnotationEvent;
|
||||
x: number;
|
||||
}
|
||||
|
||||
export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ annotationEvent, x, formatTime }) => {
|
||||
const styles = useStyles(getAnnotationMarkerStyles);
|
||||
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}>{annotationEvent.title}</span>
|
||||
{annotationEvent.time && <span className={styles.time}>{formatTime(annotationEvent.time)}</span>}
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
{annotationEvent.text && <div dangerouslySetInnerHTML={{ __html: annotationEvent.text }} />}
|
||||
<>
|
||||
<HorizontalGroup spacing="xs" wrap>
|
||||
{annotationEvent.tags?.map((t, i) => (
|
||||
<Tag name={t} key={`${t}-${i}`} />
|
||||
))}
|
||||
</HorizontalGroup>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContainer>
|
||||
);
|
||||
}, [annotationEvent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={markerRef}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className={cx(
|
||||
styles.markerWrapper,
|
||||
css`
|
||||
left: ${x - 8}px;
|
||||
`
|
||||
)}
|
||||
>
|
||||
<div className={styles.marker} />
|
||||
</div>
|
||||
{isOpen && <Portal>{renderMarker()}</Portal>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getAnnotationMarkerStyles = (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;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
`,
|
||||
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};
|
||||
`,
|
||||
};
|
||||
};
|
126
public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx
Normal file
126
public/app/plugins/panel/graph3/plugins/AnnotationsPlugin.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
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 { AnnotationMarker } from './AnnotationMarker';
|
||||
|
||||
interface AnnotationsPluginProps {
|
||||
annotations: DataFrame[];
|
||||
timeZone: TimeZone;
|
||||
}
|
||||
|
||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone }) => {
|
||||
const pluginId = 'AnnotationsPlugin';
|
||||
const pluginsApi = usePlotPluginContext();
|
||||
const plotContext = usePlotContext();
|
||||
const annotationsRef = useRef<AnnotationEvent[] | null>(null);
|
||||
const [renderCounter, setRenderCounter] = useState(0);
|
||||
const theme = useTheme();
|
||||
|
||||
const timeFormatter = useCallback(
|
||||
(value: number) => {
|
||||
return dateTimeFormat(value, {
|
||||
format: systemDateFormats.fullDate,
|
||||
timeZone,
|
||||
});
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
const annotationsData = useMemo(() => {
|
||||
return getAnnotationsFromData(annotations);
|
||||
}, [annotations]);
|
||||
|
||||
const annotationMarkers = useMemo(() => {
|
||||
if (!plotContext || !plotContext?.u) {
|
||||
return null;
|
||||
}
|
||||
const markers = [];
|
||||
|
||||
for (let i = 0; i < annotationsData.length; i++) {
|
||||
const annotation = annotationsData[i];
|
||||
if (!annotation.time) {
|
||||
continue;
|
||||
}
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
const unregister = pluginsApi.registerPlugin({
|
||||
id: pluginId,
|
||||
hooks: {
|
||||
// Render annotation lines on the canvas
|
||||
draw: u => {
|
||||
/**
|
||||
* We cannot rely on state value here, as it would require this effect to be dependent on the state value.
|
||||
* This would make the plugin re-register making the entire plot to reinitialise. ref is the way to go :)
|
||||
*/
|
||||
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];
|
||||
if (!annotation.time) {
|
||||
continue;
|
||||
}
|
||||
const xpos = u.valToPos(annotation.time / 1000, 'x', true);
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = theme.palette.red;
|
||||
ctx.setLineDash([5, 5]);
|
||||
ctx.moveTo(xpos, u.bbox.top);
|
||||
ctx.lineTo(xpos, u.bbox.top + u.bbox.height);
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
}
|
||||
setRenderCounter(c => c + 1);
|
||||
return;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregister();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!plotContext || !plotContext.u || !plotContext.canvas) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div>{annotationMarkers}</div>;
|
||||
};
|
Loading…
Reference in New Issue
Block a user