XYChart: Improved new tooltip (#75818)

Co-authored-by: Ihor Yeromin <yeryomin.igor@gmail.com>
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Adela Almasan 2024-01-04 16:55:23 -06:00 committed by GitHub
parent a03eca29eb
commit 935ecdd809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 979 additions and 106 deletions

View File

@ -6563,16 +6563,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
"public/app/plugins/panel/xychart/TooltipView.tsx:5381": [ "public/app/plugins/panel/xychart/TooltipView.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"]
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"],
[0, 0, 0, "Styles should be written using objects.", "4"]
], ],
"public/app/plugins/panel/xychart/XYChartPanel2.tsx:5381": [ "public/app/plugins/panel/xychart/XYChartPanel.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"]
[0, 0, 0, "Styles should be written using objects.", "2"]
], ],
"public/app/plugins/panel/xychart/dims.ts:5381": [ "public/app/plugins/panel/xychart/dims.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],

View File

@ -0,0 +1,687 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "blue",
"mode": "fixed"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 7,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "Miles_per_Gallon",
"y": "Horsepower"
}
],
"seriesMapping": "auto",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Single color",
"type": "xychart"
},
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "green",
"mode": "shades"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 7,
"x": 7,
"y": 0
},
"id": 4,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "Miles_per_Gallon",
"y": "Horsepower"
}
],
"seriesMapping": "auto",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Shades of a color",
"type": "xychart"
},
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "yellow",
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 7,
"x": 14,
"y": 0
},
"id": 3,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "Miles_per_Gallon",
"y": "Horsepower"
}
],
"seriesMapping": "auto",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Classic palette",
"type": "xychart"
},
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "blue",
"mode": "fixed"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 7,
"x": 0,
"y": 11
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "x",
"y": "y"
},
{
"pointColor": {
"fixed": "green"
},
"x": "x",
"y": "z"
}
],
"seriesMapping": "manual",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Single color + Mapping: Fixed color",
"type": "xychart"
},
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "blue",
"mode": "shades"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 7,
"x": 7,
"y": 11
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "x",
"y": "y"
},
{
"pointColor": {
"fixed": "green"
},
"x": "x",
"y": "z"
}
],
"seriesMapping": "manual",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Shades of color + Mapping: Fixed color",
"type": "xychart"
},
{
"datasource": {},
"fieldConfig": {
"defaults": {
"color": {
"fixedColor": "blue",
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 32
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"fieldMinMax": false,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 7,
"x": 14,
"y": 11
},
"id": 7,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"fixed": "orange"
},
"x": "x",
"y": "y"
},
{
"pointColor": {
"fixed": "green"
},
"x": "x",
"y": "z"
}
],
"seriesMapping": "manual",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,z\n1,2,3\n3,4,5\n5,6,7",
"datasource": {
"type": "grafana-testdata-datasource",
"uid": "${DS_GDEV-TESTDATA}"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Standard options: Classic palette + Mapping: Fixed color",
"type": "xychart"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"pointSize": {
"fixed": 50
},
"scaleDistribution": {
"type": "linear"
},
"show": "points"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 7,
"x": 0,
"y": 23
},
"id": 8,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"series": [
{
"pointColor": {
"field": "c",
"fixed": "dark-green"
},
"x": "x",
"y": "y"
}
],
"seriesMapping": "manual",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "x,y,c\n0,0,0\n50,50,25\n100,100,50",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Panel Title",
"type": "xychart"
}
],
"refresh": "",
"schemaVersion": 39,
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "XYChart tooltip color test",
"uid": "cb67db43-dd72-4ada-a313-53f46c20adcc",
"version": 1,
"weekStart": ""
}

View File

@ -124,5 +124,6 @@
"timeseries-yaxis-ticks": (import '../dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json'), "timeseries-yaxis-ticks": (import '../dev-dashboards/panel-timeseries/timeseries-yaxis-ticks.json'),
"trend_example": (import '../dev-dashboards/panel-trend/trend_example.json'), "trend_example": (import '../dev-dashboards/panel-trend/trend_example.json'),
"xychart-example": (import '../dev-dashboards/panel-xychart/xychart-example.json'), "xychart-example": (import '../dev-dashboards/panel-xychart/xychart-example.json'),
"xychart-tooltip-color-test": (import '../dev-dashboards/panel-xychart/xychart-tooltip-color-test.json'),
}, },
} }

View File

@ -163,22 +163,22 @@ function fmt(field: Field, val: number): string {
} }
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
infoWrap: css` infoWrap: css({
padding: 8px; padding: '8px',
width: 100%; width: '100%',
th { th: {
font-weight: ${theme.typography.fontWeightMedium}; fontWeight: theme.typography.fontWeightMedium,
padding: ${theme.spacing(0.25, 2)}; padding: theme.spacing(0.25, 2),
} },
`, }),
highlight: css` highlight: css({
background: ${theme.colors.action.hover}; background: theme.colors.action.hover,
`, }),
xVal: css` xVal: css({
font-weight: ${theme.typography.fontWeightBold}; fontWeight: theme.typography.fontWeightBold,
`, }),
icon: css` icon: css({
margin-right: ${theme.spacing(1)}; marginRight: theme.spacing(1),
vertical-align: middle; verticalAlign: 'middle',
`, }),
}); });

View File

@ -16,6 +16,7 @@ import { config } from '@grafana/runtime';
import { import {
Portal, Portal,
TooltipDisplayMode, TooltipDisplayMode,
TooltipPlugin2,
UPlotChart, UPlotChart,
UPlotConfigBuilder, UPlotConfigBuilder,
VizLayout, VizLayout,
@ -23,10 +24,12 @@ import {
VizLegendItem, VizLegendItem,
VizTooltipContainer, VizTooltipContainer,
} from '@grafana/ui'; } from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { FacetedData } from '@grafana/ui/src/components/uPlot/types'; import { FacetedData } from '@grafana/ui/src/components/uPlot/types';
import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
import { TooltipView } from './TooltipView'; import { TooltipView } from './TooltipView';
import { XYChartTooltip } from './XYChartTooltip';
import { Options, SeriesMapping } from './panelcfg.gen'; import { Options, SeriesMapping } from './panelcfg.gen';
import { prepData, prepScatter, ScatterPanelInfo } from './scatter'; import { prepData, prepScatter, ScatterPanelInfo } from './scatter';
import { ScatterHoverEvent, ScatterSeries } from './types'; import { ScatterHoverEvent, ScatterSeries } from './types';
@ -34,13 +37,14 @@ import { ScatterHoverEvent, ScatterSeries } from './types';
type Props = PanelProps<Options>; type Props = PanelProps<Options>;
const TOOLTIP_OFFSET = 10; const TOOLTIP_OFFSET = 10;
export const XYChartPanel2 = (props: Props) => { export const XYChartPanel = (props: Props) => {
const [error, setError] = useState<string | undefined>(); const [error, setError] = useState<string | undefined>();
const [series, setSeries] = useState<ScatterSeries[]>([]); const [series, setSeries] = useState<ScatterSeries[]>([]);
const [builder, setBuilder] = useState<UPlotConfigBuilder | undefined>(); const [builder, setBuilder] = useState<UPlotConfigBuilder | undefined>();
const [facets, setFacets] = useState<FacetedData | undefined>(); const [facets, setFacets] = useState<FacetedData | undefined>();
const [hover, setHover] = useState<ScatterHoverEvent | undefined>(); const [hover, setHover] = useState<ScatterHoverEvent | undefined>();
const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false); const [shouldDisplayCloseButton, setShouldDisplayCloseButton] = useState<boolean>(false);
const showNewVizTooltips = Boolean(config.featureToggles.newVizTooltips);
const isToolTipOpen = useRef<boolean>(false); const isToolTipOpen = useRef<boolean>(false);
const oldOptions = usePrevious(props.options); const oldOptions = usePrevious(props.options);
@ -69,9 +73,9 @@ export const XYChartPanel2 = (props: Props) => {
props.options, props.options,
getData, getData,
config.theme2, config.theme2,
scatterHoverCallback, showNewVizTooltips ? null : scatterHoverCallback,
onUPlotClick, showNewVizTooltips ? null : onUPlotClick,
isToolTipOpen showNewVizTooltips ? null : isToolTipOpen
); );
if (info.error) { if (info.error) {
@ -82,7 +86,7 @@ export const XYChartPanel2 = (props: Props) => {
setFacets(() => prepData(info, props.data.series)); setFacets(() => prepData(info, props.data.series));
setError(undefined); setError(undefined);
} }
}, [props.data.series, props.options]); }, [props.data.series, props.options, showNewVizTooltips]);
const initFacets = useCallback(() => { const initFacets = useCallback(() => {
setFacets(() => prepData({ error, series }, props.data.series)); setFacets(() => prepData({ error, series }, props.data.series));
@ -187,11 +191,11 @@ export const XYChartPanel2 = (props: Props) => {
} }
const legendStyle = { const legendStyle = {
flexStart: css` flexStart: css({
div { div: {
justify-content: flex-start !important; justifyContent: 'flex-start',
} },
`, }),
}; };
return ( return (
@ -218,47 +222,69 @@ export const XYChartPanel2 = (props: Props) => {
<> <>
<VizLayout width={props.width} height={props.height} legend={renderLegend()}> <VizLayout width={props.width} height={props.height} legend={renderLegend()}>
{(vizWidth: number, vizHeight: number) => ( {(vizWidth: number, vizHeight: number) => (
<UPlotChart config={builder} data={facets} width={vizWidth} height={vizHeight} /> <UPlotChart config={builder} data={facets} width={vizWidth} height={vizHeight}>
{config.featureToggles.newVizTooltips && props.options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={TooltipHoverMode.xyOne}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss) => {
return (
<XYChartTooltip
data={props.data.series}
dataIdxs={dataIdxs}
allSeries={series}
dismiss={dismiss}
isPinned={isPinned}
options={props.options}
seriesIdx={seriesIdx}
/>
);
}}
/>
)}
</UPlotChart>
)} )}
</VizLayout> </VizLayout>
<Portal> {!config.featureToggles.newVizTooltips && (
{hover && props.options.tooltip.mode !== TooltipDisplayMode.None && ( <Portal>
<VizTooltipContainer {hover && props.options.tooltip.mode !== TooltipDisplayMode.None && (
position={{ x: hover.pageX, y: hover.pageY }} <VizTooltipContainer
offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }} position={{ x: hover.pageX, y: hover.pageY }}
allowPointerEvents={isToolTipOpen.current} offset={{ x: TOOLTIP_OFFSET, y: TOOLTIP_OFFSET }}
> allowPointerEvents={isToolTipOpen.current}
{shouldDisplayCloseButton && ( >
<div {shouldDisplayCloseButton && (
style={{ <div
width: '100%',
display: 'flex',
justifyContent: 'flex-end',
}}
>
<CloseButton
onClick={onCloseToolTip}
style={{ style={{
position: 'relative', width: '100%',
top: 'auto', display: 'flex',
right: 'auto', justifyContent: 'flex-end',
marginRight: 0,
}} }}
/> >
</div> <CloseButton
)} onClick={onCloseToolTip}
<TooltipView style={{
options={props.options.tooltip} position: 'relative',
allSeries={series} top: 'auto',
manualSeriesConfigs={props.options.series} right: 'auto',
seriesMapping={props.options.seriesMapping!} marginRight: 0,
rowIndex={hover.xIndex} }}
hoveredPointIndex={hover.scatterIndex} />
data={props.data.series} </div>
/> )}
</VizTooltipContainer> <TooltipView
)} options={props.options.tooltip}
</Portal> allSeries={series}
manualSeriesConfigs={props.options.series}
seriesMapping={props.options.seriesMapping!}
rowIndex={hover.xIndex}
hoveredPointIndex={hover.scatterIndex}
data={props.data.series}
/>
</VizTooltipContainer>
)}
</Portal>
)}
</> </>
); );
}; };

View File

@ -0,0 +1,136 @@
import { css } from '@emotion/css';
import React from 'react';
import { DataFrame, Field, getFieldDisplayName, GrafanaTheme2, LinkModel } from '@grafana/data';
import { alpha } from '@grafana/data/src/themes/colorManipulator';
import { useStyles2 } from '@grafana/ui';
import { VizTooltipContent } from '@grafana/ui/src/components/VizTooltip/VizTooltipContent';
import { VizTooltipFooter } from '@grafana/ui/src/components/VizTooltip/VizTooltipFooter';
import { VizTooltipHeader } from '@grafana/ui/src/components/VizTooltip/VizTooltipHeader';
import { ColorIndicator, LabelValue } from '@grafana/ui/src/components/VizTooltip/types';
import { getTitleFromHref } from 'app/features/explore/utils/links';
import { Options } from './panelcfg.gen';
import { ScatterSeries, YValue } from './types';
import { fmt } from './utils';
export interface Props {
dataIdxs: Array<number | null>;
seriesIdx: number | null | undefined;
isPinned: boolean;
dismiss: () => void;
options: Options;
data: DataFrame[]; // source data
allSeries: ScatterSeries[];
}
export const XYChartTooltip = ({ dataIdxs, seriesIdx, data, allSeries, dismiss, options, isPinned }: Props) => {
const styles = useStyles2(getStyles);
const rowIndex = dataIdxs.find((idx) => idx !== null);
// @todo: remove -1 when uPlot v2 arrive
// context: first value in dataIdxs always null and represent X series
const hoveredPointIndex = seriesIdx! - 1;
if (!allSeries || rowIndex == null) {
return null;
}
const series = allSeries[hoveredPointIndex];
const frame = series.frame(data);
const xField = series.x(frame);
const yField = series.y(frame);
const getHeaderLabel = (): LabelValue => {
let label = series.name;
if (options.seriesMapping === 'manual') {
label = options.series?.[hoveredPointIndex]?.name ?? `Series ${hoveredPointIndex + 1}`;
}
let colorThing = series.pointColor(frame);
if (Array.isArray(colorThing)) {
colorThing = colorThing[rowIndex];
}
return {
label,
value: null,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
color: alpha(colorThing as string, 0.5),
colorIndicator: ColorIndicator.marker_md,
};
};
const getContentLabel = (): LabelValue[] => {
let colorThing = series.pointColor(frame);
if (Array.isArray(colorThing)) {
colorThing = colorThing[rowIndex];
}
const yValue: YValue = {
name: getFieldDisplayName(yField, frame),
val: yField.values[rowIndex],
field: yField,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
color: alpha(colorThing as string, 0.5),
};
const content: LabelValue[] = [
{
label: getFieldDisplayName(xField, frame),
value: fmt(xField, xField.values[rowIndex]),
},
{
label: yValue.name,
value: fmt(yValue.field, yValue.val),
},
];
// add extra fields
const extraFields: Field[] = frame.fields.filter((f) => f !== xField && f !== yField);
if (extraFields) {
extraFields.forEach((field) => {
content.push({
label: field.name,
value: fmt(field, field.values[rowIndex]),
});
});
}
return content;
};
const getLinks = (): Array<LinkModel<Field>> => {
let links: Array<LinkModel<Field>> = [];
if (yField.getLinks) {
const v = yField.values[rowIndex];
const disp = yField.display ? yField.display(v) : { text: `${v}`, numeric: +v };
links = yField.getLinks({ calculatedValue: disp, valueRowIndex: rowIndex }).map((linkModel) => {
if (!linkModel.title) {
linkModel.title = getTitleFromHref(linkModel.href);
}
return linkModel;
});
}
return links;
};
return (
<div className={styles.wrapper}>
<VizTooltipHeader headerLabel={getHeaderLabel()} />
<VizTooltipContent contentLabelValue={getContentLabel()} />
{isPinned && <VizTooltipFooter dataLinks={getLinks()} canAnnotate={false} />}
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css({
display: 'flex',
flexDirection: 'column',
width: '280px',
}),
});

View File

@ -3,11 +3,11 @@ import { commonOptionsBuilder } from '@grafana/ui';
import { AutoEditor } from './AutoEditor'; import { AutoEditor } from './AutoEditor';
import { ManualEditor } from './ManualEditor'; import { ManualEditor } from './ManualEditor';
import { XYChartPanel2 } from './XYChartPanel2'; import { XYChartPanel } from './XYChartPanel';
import { getScatterFieldConfig } from './config'; import { getScatterFieldConfig } from './config';
import { Options, FieldConfig, defaultFieldConfig } from './panelcfg.gen'; import { Options, FieldConfig, defaultFieldConfig } from './panelcfg.gen';
export const plugin = new PanelPlugin<Options, FieldConfig>(XYChartPanel2) export const plugin = new PanelPlugin<Options, FieldConfig>(XYChartPanel)
.useFieldConfig(getScatterFieldConfig(defaultFieldConfig)) .useFieldConfig(getScatterFieldConfig(defaultFieldConfig))
.setPanelOptions((builder) => { .setPanelOptions((builder) => {
builder builder
@ -38,6 +38,6 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(XYChartPanel2)
showIf: (cfg) => cfg.seriesMapping === 'manual', showIf: (cfg) => cfg.seriesMapping === 'manual',
}); });
commonOptionsBuilder.addTooltipOptions(builder); commonOptionsBuilder.addTooltipOptions(builder, true);
commonOptionsBuilder.addLegendOptions(builder); commonOptionsBuilder.addLegendOptions(builder);
}); });

View File

@ -46,9 +46,9 @@ export function prepScatter(
options: Options, options: Options,
getData: () => DataFrame[], getData: () => DataFrame[],
theme: GrafanaTheme2, theme: GrafanaTheme2,
ttip: ScatterHoverCallback, ttip: null | ScatterHoverCallback,
onUPlotClick: null | ((evt?: Object) => void), onUPlotClick: null | ((evt?: Object) => void),
isToolTipOpen: MutableRefObject<boolean> isToolTipOpen: null | MutableRefObject<boolean>
): ScatterPanelInfo { ): ScatterPanelInfo {
let series: ScatterSeries[]; let series: ScatterSeries[];
let builder: UPlotConfigBuilder; let builder: UPlotConfigBuilder;
@ -294,14 +294,13 @@ interface DrawBubblesOpts {
}; };
} }
//const prepConfig: UPlotConfigPrepFnXY<Options> = ({ frames, series, theme }) => {
const prepConfig = ( const prepConfig = (
getData: () => DataFrame[], getData: () => DataFrame[],
scatterSeries: ScatterSeries[], scatterSeries: ScatterSeries[],
theme: GrafanaTheme2, theme: GrafanaTheme2,
ttip: ScatterHoverCallback, ttip: null | ScatterHoverCallback,
onUPlotClick: null | ((evt?: Object) => void), onUPlotClick: null | ((evt?: Object) => void),
isToolTipOpen: MutableRefObject<boolean> isToolTipOpen: null | MutableRefObject<boolean>
) => { ) => {
let qt: Quadtree; let qt: Quadtree;
let hRect: Rect | null; let hRect: Rect | null;
@ -522,8 +521,10 @@ const prepConfig = (
}); });
const clearPopupIfOpened = () => { const clearPopupIfOpened = () => {
if (isToolTipOpen.current) { if (isToolTipOpen?.current) {
ttip(undefined); if (ttip) {
ttip(undefined);
}
if (onUPlotClick) { if (onUPlotClick) {
onUPlotClick(); onUPlotClick();
} }
@ -534,7 +535,9 @@ const prepConfig = (
// clip hover points/bubbles to plotting area // clip hover points/bubbles to plotting area
builder.addHook('init', (u, r) => { builder.addHook('init', (u, r) => {
u.over.style.overflow = 'hidden'; if (!config.featureToggles.newVizTooltips) {
u.over.style.overflow = 'hidden';
}
ref_parent = u.root.parentElement; ref_parent = u.root.parentElement;
if (onUPlotClick) { if (onUPlotClick) {
@ -556,26 +559,28 @@ const prepConfig = (
rect = r; rect = r;
}); });
builder.addHook('setLegend', (u) => { if (ttip) {
if (u.cursor.idxs != null) { builder.addHook('setLegend', (u) => {
for (let i = 0; i < u.cursor.idxs.length; i++) { if (u.cursor.idxs != null) {
const sel = u.cursor.idxs[i]; for (let i = 0; i < u.cursor.idxs.length; i++) {
if (sel != null && !isToolTipOpen.current) { const sel = u.cursor.idxs[i];
ttip({ if (sel != null && !isToolTipOpen?.current) {
scatterIndex: i - 1, ttip({
xIndex: sel, scatterIndex: i - 1,
pageX: rect.left + u.cursor.left!, xIndex: sel,
pageY: rect.top + u.cursor.top!, pageX: rect.left + u.cursor.left!,
}); pageY: rect.top + u.cursor.top!,
return; // only show the first one });
return; // only show the first one
}
} }
} }
}
if (!isToolTipOpen.current) { if (!isToolTipOpen?.current) {
ttip(undefined); ttip(undefined);
} }
}); });
}
builder.addHook('drawClear', (u) => { builder.addHook('drawClear', (u) => {
clearPopupIfOpened(); clearPopupIfOpened();
@ -598,8 +603,8 @@ const prepConfig = (
const frames = getData(); const frames = getData();
let xField = scatterSeries[0].x(scatterSeries[0].frame(frames)); let xField = scatterSeries[0].x(scatterSeries[0].frame(frames));
let config = xField.config; let fieldConfig = xField.config;
let customConfig = config.custom; let customConfig = fieldConfig.custom;
let scaleDistr = customConfig?.scaleDistribution; let scaleDistr = customConfig?.scaleDistribution;
builder.addScale({ builder.addScale({
@ -610,12 +615,12 @@ const prepConfig = (
distribution: scaleDistr?.type, distribution: scaleDistr?.type,
log: scaleDistr?.log, log: scaleDistr?.log,
linearThreshold: scaleDistr?.linearThreshold, linearThreshold: scaleDistr?.linearThreshold,
min: config.min, min: fieldConfig.min,
max: config.max, max: fieldConfig.max,
softMin: customConfig?.axisSoftMin, softMin: customConfig?.axisSoftMin,
softMax: customConfig?.axisSoftMax, softMax: customConfig?.axisSoftMax,
centeredZero: customConfig?.axisCenteredZero, centeredZero: customConfig?.axisCenteredZero,
decimals: config.decimals, decimals: fieldConfig.decimals,
}); });
// why does this fall back to '' instead of null or undef? // why does this fall back to '' instead of null or undef?

View File

@ -50,3 +50,17 @@ export interface ScatterSeries {
}; };
}; };
} }
export interface YValue {
name: string;
val: number;
field: Field;
color: string;
}
export interface ExtraFacets {
colorFacetFieldName: string;
sizeFacetFieldName: string;
colorFacetValue: number;
sizeFacetValue: number;
}

View File

@ -0,0 +1,9 @@
import { Field, formattedValueToString } from '@grafana/data';
export function fmt(field: Field, val: number): string {
if (field.display) {
return formattedValueToString(field.display(val));
}
return `${val}`;
}