PieChart: Unify tooltip to look the way it looks in TimeSeries (#33032)

* feat(piechart): align styles between piechart and graph

* feat(piechart): introduce tooltip options to panel and visualisation

* feat(piechart): get tooltip options working

* feat(piechart): add SeriesTable to visx TooltipInPortal

* refactor(piechart): move getTooltipData out of PieSlice

* docs(piechart): fix storybook story errors

* feat(viztooltip): initial commit of common tooltip types and components

* refactor(viztooltip): rename type as enum and update usage

* refactor(viztooltip): move chart.tooltip into viztooltip and fix imports and typings

* refactor(viztooltip): update import paths and names where used

* docs(infotooltip): fix story import paths

* docs(piechart): fix typings in story

* docs(viztooltip): add public annotations to exported components and types
This commit is contained in:
Jack Westbrook 2021-04-20 09:45:41 +02:00 committed by GitHub
parent 5f54f2dc00
commit c2953f3a06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 314 additions and 166 deletions

View File

@ -23,7 +23,7 @@ TableSortByFieldState: {
desc?: bool
} @cuetsy(targetType="interface")
TooltipMode: "single" | "multi" | "none" @cuetsy(targetType="type")
TooltipDisplayMode: "single" | "multi" | "none" @cuetsy(targetType="enum")
FieldTextAlignment: "auto" | "left" | "right" | "center" @cuetsy(targetType="type")
AxisPlacement: "auto" | "top" | "right" | "bottom" | "left" | "hidden" @cuetsy(targetType="enum")
PointVisibility: "auto" | "never" | "always" @cuetsy(targetType="enum")
@ -72,9 +72,6 @@ HideSeriesConfig: {
} @cuetsy(targetType="interface")
LegendPlacement: "bottom" | "right" @cuetsy(targetType="type")
LegendDisplayMode: "list" | "table" | "hidden" @cuetsy(targetType="enum")
GraphTooltipOptions: {
mode: TooltipMode
} @cuetsy(targetType="interface")
TableFieldOptions: {
width?: number
align: FieldTextAlignment | *"auto"
@ -91,3 +88,6 @@ VizLegendOptions: {
placement: LegendPlacement
calcs: [string]
} @cuetsy(targetType="interface")
VizTooltipOptions: {
mode: TooltipDisplayMode
} @cuetsy(targetType="interface")

View File

@ -1,7 +0,0 @@
import { Tooltip } from './Tooltip';
const Chart = {
Tooltip,
};
export default Chart;

View File

@ -1,3 +0,0 @@
package grafanaschema
TooltipMode: "single" | "multi" | "none" @cuetsy(targetType="type")

View File

@ -1,6 +0,0 @@
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// NOTE: This file will be auto generated from models.cue
// It is currenty hand written but will serve as the target for cuetsy
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
export type TooltipMode = 'single' | 'multi' | 'none';

View File

@ -1,11 +1,9 @@
import React from 'react';
import { Graph } from '@grafana/ui';
import Chart from '../Chart';
import { dateTime, ArrayVector, FieldType, GraphSeriesXY, FieldColorModeId } from '@grafana/data';
import { Story } from '@storybook/react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { TooltipContentProps } from '../Chart/Tooltip';
import { TooltipMode } from '../Chart/models.gen';
import { VizTooltip, TooltipDisplayMode, VizTooltipContentProps } from '../VizTooltip';
import { JSONFormatter } from '../JSONFormatter/JSONFormatter';
import { GraphProps } from './Graph';
@ -114,15 +112,15 @@ export default {
},
};
export const WithTooltip: Story<GraphProps & { tooltipMode: TooltipMode }> = ({ tooltipMode, ...args }) => {
export const WithTooltip: Story<GraphProps & { tooltipMode: TooltipDisplayMode }> = ({ tooltipMode, ...args }) => {
return (
<Graph {...args}>
<Chart.Tooltip mode={tooltipMode} />
<VizTooltip mode={tooltipMode} />
</Graph>
);
};
const CustomGraphTooltip = ({ activeDimensions }: TooltipContentProps) => {
const CustomGraphTooltip = ({ activeDimensions }: VizTooltipContentProps) => {
return (
<div style={{ height: '200px' }}>
<div>Showing currently active active dimensions:</div>
@ -131,10 +129,13 @@ const CustomGraphTooltip = ({ activeDimensions }: TooltipContentProps) => {
);
};
export const WithCustomTooltip: Story<GraphProps & { tooltipMode: TooltipMode }> = ({ tooltipMode, ...args }) => {
export const WithCustomTooltip: Story<GraphProps & { tooltipMode: TooltipDisplayMode }> = ({
tooltipMode,
...args
}) => {
return (
<Graph {...args}>
<Chart.Tooltip mode={tooltipMode} tooltipComponent={CustomGraphTooltip} />
<VizTooltip mode={tooltipMode} tooltipComponent={CustomGraphTooltip} />
</Graph>
);
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
import Graph from './Graph';
import Chart from '../Chart';
import { VizTooltip, TooltipDisplayMode } from '../VizTooltip';
import { GraphSeriesXY, FieldType, ArrayVector, dateTime, FieldColorModeId } from '@grafana/data';
const series: GraphSeriesXY[] = [
@ -92,7 +92,7 @@ describe('Graph', () => {
it("doesn't render tooltip when not hovering over a datapoint", () => {
const graphWithTooltip = (
<Graph {...mockGraphProps()}>
<Chart.Tooltip mode="single" />
<VizTooltip mode={TooltipDisplayMode.Single} />
</Graph>
);
@ -105,7 +105,7 @@ describe('Graph', () => {
// Given
const graphWithTooltip = (
<Graph {...mockGraphProps()}>
<Chart.Tooltip mode="single" />
<VizTooltip mode={TooltipDisplayMode.Single} />
</Graph>
);
const container = mount(graphWithTooltip);
@ -145,7 +145,7 @@ describe('Graph', () => {
// Given
const graphWithTooltip = (
<Graph {...mockGraphProps(true)}>
<Chart.Tooltip mode="multi" />
<VizTooltip mode={TooltipDisplayMode.Multi} />
</Graph>
);
const container = mount(graphWithTooltip);

View File

@ -6,11 +6,12 @@ import uniqBy from 'lodash/uniqBy';
import { TimeRange, GraphSeriesXY, TimeZone, createDimension } from '@grafana/data';
import _ from 'lodash';
import { FlotPosition, FlotItem } from './types';
import { TooltipProps, TooltipContentProps, ActiveDimensions, Tooltip } from '../Chart/Tooltip';
import { VizTooltipProps, VizTooltipContentProps, ActiveDimensions, VizTooltip } from '../VizTooltip';
import { GraphTooltip } from './GraphTooltip/GraphTooltip';
import { GraphContextMenu, GraphContextMenuProps, ContextDimensions } from './GraphContextMenu';
import { GraphDimensions } from './GraphTooltip/types';
import { graphTimeFormat, graphTickFormatter } from './utils';
import { TooltipDisplayMode } from '../VizTooltip/models.gen';
export interface GraphProps {
ariaLabel?: string;
@ -124,7 +125,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
renderTooltip = () => {
const { children, series, timeZone } = this.props;
const { pos, activeItem, isTooltipVisible } = this.state;
let tooltipElement: React.ReactElement<TooltipProps> | null = null;
let tooltipElement: React.ReactElement<VizTooltipProps> | null = null;
if (!isTooltipVisible || !pos || series.length === 0) {
return null;
@ -139,15 +140,15 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
// @ts-ignore
const childType = c && c.type && (c.type.displayName || c.type.name);
if (childType === Tooltip.displayName) {
tooltipElement = c as React.ReactElement<TooltipProps>;
if (childType === VizTooltip.displayName) {
tooltipElement = c as React.ReactElement<VizTooltipProps>;
}
});
// If no tooltip provided, skip rendering
if (!tooltipElement) {
return null;
}
const tooltipElementProps = (tooltipElement as React.ReactElement<TooltipProps>).props;
const tooltipElementProps = (tooltipElement as React.ReactElement<VizTooltipProps>).props;
const tooltipMode = tooltipElementProps.mode || 'single';
@ -172,7 +173,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
yAxis: activeItem ? [activeItem.series.seriesIndex, activeItem.dataIndex] : null,
};
const tooltipContentProps: TooltipContentProps<GraphDimensions> = {
const tooltipContentProps: VizTooltipContentProps<GraphDimensions> = {
dimensions: {
// time/value dimension columns are index-aligned - see getGraphSeriesModel
xAxis: createDimension(
@ -186,13 +187,13 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
},
activeDimensions,
pos,
mode: tooltipElementProps.mode || 'single',
mode: tooltipElementProps.mode || TooltipDisplayMode.Single,
timeZone,
};
const tooltipContent = React.createElement(tooltipContentRenderer, { ...tooltipContentProps });
return React.cloneElement<TooltipProps>(tooltipElement as React.ReactElement<TooltipProps>, {
return React.cloneElement<VizTooltipProps>(tooltipElement as React.ReactElement<VizTooltipProps>, {
content: tooltipContent,
position: { x: pos.pageX, y: pos.pageY },
offset: { x: 10, y: 10 },

View File

@ -1,10 +1,10 @@
import React from 'react';
import { TooltipContentProps } from '../../Chart/Tooltip';
import { VizTooltipContentProps } from '../../VizTooltip';
import { SingleModeGraphTooltip } from './SingleModeGraphTooltip';
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
import { GraphDimensions } from './types';
export const GraphTooltip: React.FC<TooltipContentProps<GraphDimensions>> = ({
export const GraphTooltip: React.FC<VizTooltipContentProps<GraphDimensions>> = ({
mode = 'single',
dimensions,
activeDimensions,

View File

@ -3,7 +3,7 @@ import { mount } from 'enzyme';
import { MultiModeGraphTooltip } from './MultiModeGraphTooltip';
import { createDimension, ArrayVector, FieldType } from '@grafana/data';
import { GraphDimensions } from './types';
import { ActiveDimensions } from '../../Chart/Tooltip';
import { ActiveDimensions } from '../../VizTooltip';
let dimensions: GraphDimensions;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { SeriesTable } from './SeriesTable';
import { SeriesTable } from '../../VizTooltip';
import { GraphTooltipContentProps } from './types';
import { getMultiSeriesGraphHoverInfo } from '../utils';
import { FlotPosition } from '../types';

View File

@ -6,7 +6,7 @@ import {
getDisplayProcessor,
getFieldDisplayName,
} from '@grafana/data';
import { SeriesTable } from './SeriesTable';
import { SeriesTable } from '../../VizTooltip';
import { GraphTooltipContentProps } from './types';
export const SingleModeGraphTooltip: React.FC<GraphTooltipContentProps> = ({

View File

@ -1,11 +0,0 @@
package grafanaschema
// TODO Relative imports are flatly disallowed by CUE, but that's what's
// currently done in the corresponding typescript code. We'll have to make
// cuetsy handle this with import mappings.
import tooltip "github.com/grafana/grafana/packages/grafana-ui/src/components/Chart:grafanaschema"
GraphTooltipOptions: {
mode: tooltip.TooltipMode
} @cuetsy(targetType="interface")

View File

@ -1,10 +1,5 @@
import { ActiveDimensions } from '../../Chart/Tooltip';
import { ActiveDimensions } from '../../VizTooltip';
import { Dimension, Dimensions, TimeZone } from '@grafana/data';
import { TooltipMode } from '../../Chart/models.gen';
export interface GraphTooltipOptions {
mode: TooltipMode;
}
export interface GraphDimensions extends Dimensions {
xAxis: Dimension<number>;

View File

@ -2,11 +2,11 @@ import React from 'react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { InfoTooltip } from './InfoTooltip';
import { Tooltip } from '../Chart/Tooltip';
import { VizTooltip } from '../VizTooltip';
export default {
title: 'Overlays/TooltipInternal',
component: Tooltip,
component: VizTooltip,
decorators: [withCenteredStory],
};

View File

@ -1,6 +1,6 @@
import React from 'react';
import { select, number, boolean } from '@storybook/addon-knobs';
import { PieChart, PieChartType } from '@grafana/ui';
import { PieChart, PieChartType, TooltipDisplayMode } from '@grafana/ui';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import {
FieldColorModeId,
@ -52,15 +52,16 @@ const getKnobs = () => {
return {
width: number('Width', 500),
height: number('Height', 500),
pieType: select('pieType', [PieChartType.Pie, PieChartType.Donut], PieChartType.Pie),
pieType: select('pieType', Object.values(PieChartType), PieChartType.Pie),
showLabelName: boolean('Label.showName', true),
showLabelValue: boolean('Label.showValue', false),
showLabelPercent: boolean('Label.showPercent', false),
tooltipMode: select('Tooltip mode', Object.values(TooltipDisplayMode), TooltipDisplayMode.Single),
};
};
export const basic = () => {
const { pieType, width, height } = getKnobs();
const { pieType, width, height, tooltipMode } = getKnobs();
return (
<PieChart
@ -71,12 +72,13 @@ export const basic = () => {
fieldConfig={fieldConfig}
data={datapoints}
pieType={pieType}
tooltipOptions={{ mode: tooltipMode }}
/>
);
};
export const donut = () => {
const { width, height } = getKnobs();
const { width, height, tooltipMode } = getKnobs();
return (
<PieChart
@ -87,6 +89,7 @@ export const donut = () => {
fieldConfig={fieldConfig}
data={datapoints}
pieType={PieChartType.Donut}
tooltipOptions={{ mode: tooltipMode }}
/>
);
};

View File

@ -1,6 +1,5 @@
import React, { FC, ReactNode } from 'react';
import {
DisplayValue,
FALLBACK_COLOR,
FieldDisplay,
formattedValueToString,
@ -29,6 +28,8 @@ import {
PieChartSvgProps,
PieChartType,
} from './types';
import { getTooltipContainerStyles } from '../../themes/mixins';
import { SeriesTable, SeriesTableRowProps, VizTooltipOptions } from '../VizTooltip';
const defaultLegendOptions: PieChartLegendOptions = {
displayMode: LegendDisplayMode.List,
@ -44,6 +45,7 @@ export const PieChart: FC<PieChartProps> = ({
fieldConfig,
replaceVariables,
legendOptions = defaultLegendOptions,
tooltipOptions,
onSeriesColorChange,
width,
height,
@ -112,7 +114,13 @@ export const PieChart: FC<PieChartProps> = ({
<VizLayout width={width} height={height} legend={getLegend(fieldDisplayValues, legendOptions)}>
{(vizWidth: number, vizHeight: number) => {
return (
<PieChartSvg width={vizWidth} height={vizHeight} fieldDisplayValues={fieldDisplayValues} {...restProps} />
<PieChartSvg
width={vizWidth}
height={vizHeight}
fieldDisplayValues={fieldDisplayValues}
tooltipOptions={tooltipOptions}
{...restProps}
/>
);
}}
</VizLayout>
@ -126,11 +134,12 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
height,
useGradients = true,
displayLabels = [],
tooltipOptions,
}) => {
const theme = useTheme();
const componentInstanceId = useComponentInstanceId('PieChart');
const styles = useStyles(getStyles);
const tooltip = useTooltip<DisplayValue>();
const tooltip = useTooltip<SeriesTableRowProps[]>();
const { containerRef, TooltipInPortal } = useTooltipInPortal({
detectBounds: true,
scroll: true,
@ -147,6 +156,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
};
const showLabel = displayLabels.length > 0;
const showTooltip = tooltipOptions.mode !== 'none' && tooltip.tooltipOpen;
const total = fieldDisplayValues.reduce((acc, item) => item.display.numeric + acc, 0);
const layout = getPieLayout(width, height, pieType);
const colors = [
@ -204,6 +214,7 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
pie={pie}
fill={getGradientColor(color)}
openMenu={api.openMenu}
tooltipOptions={tooltipOptions}
>
{label}
</PieSlice>
@ -212,7 +223,14 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
);
} else {
return (
<PieSlice key={arc.index} tooltip={tooltip} arc={arc} pie={pie} fill={getGradientColor(color)}>
<PieSlice
key={arc.index}
tooltip={tooltip}
arc={arc}
pie={pie}
fill={getGradientColor(color)}
tooltipOptions={tooltipOptions}
>
{label}
</PieSlice>
);
@ -222,16 +240,18 @@ export const PieChartSvg: FC<PieChartSvgProps> = ({
</Pie>
</Group>
</svg>
{tooltip.tooltipOpen && (
{showTooltip ? (
<TooltipInPortal
key={Math.random()}
top={tooltip.tooltipTop}
className={styles.tooltipPortal}
left={tooltip.tooltipLeft}
unstyled={true}
applyPositionStyle={true}
>
{tooltip.tooltipData!.title} {formattedValueToString(tooltip.tooltipData!)}
<SeriesTable series={tooltip.tooltipData!} />
</TooltipInPortal>
)}
) : null}
</div>
);
};
@ -241,18 +261,19 @@ const PieSlice: FC<{
arc: PieArcDatum<FieldDisplay>;
pie: ProvidedProps<FieldDisplay>;
fill: string;
tooltip: UseTooltipParams<DisplayValue>;
tooltip: UseTooltipParams<SeriesTableRowProps[]>;
tooltipOptions: VizTooltipOptions;
openMenu?: (event: React.MouseEvent<SVGElement>) => void;
}> = ({ arc, children, pie, openMenu, fill, tooltip }) => {
}> = ({ arc, children, pie, openMenu, fill, tooltip, tooltipOptions }) => {
const theme = useTheme();
const styles = useStyles(getStyles);
const onMouseMoveOverArc = (event: any, datum: any) => {
const onMouseMoveOverArc = (event: any) => {
const coords = localPoint(event.target.ownerSVGElement, event);
tooltip.showTooltip({
tooltipLeft: coords!.x,
tooltipTop: coords!.y,
tooltipData: datum,
tooltipData: getTooltipData(pie, arc, tooltipOptions),
});
};
@ -260,7 +281,7 @@ const PieSlice: FC<{
<g
key={arc.data.display.title}
className={styles.svgArg}
onMouseMove={(event) => onMouseMoveOverArc(event, arc.data.display)}
onMouseMove={tooltipOptions.mode !== 'none' ? onMouseMoveOverArc : undefined}
onMouseOut={tooltip.hideTooltip}
onClick={openMenu}
>
@ -321,6 +342,30 @@ const PieLabel: FC<{
);
};
function getTooltipData(
pie: ProvidedProps<FieldDisplay>,
arc: PieArcDatum<FieldDisplay>,
tooltipOptions: VizTooltipOptions
) {
if (tooltipOptions.mode === 'multi') {
return pie.arcs.map((pieArc) => {
return {
color: pieArc.data.display.color ?? FALLBACK_COLOR,
label: pieArc.data.display.title,
value: formattedValueToString(pieArc.data.display),
isActive: pieArc.index === arc.index,
};
});
}
return [
{
color: arc.data.display.color ?? FALLBACK_COLOR,
label: arc.data.display.title,
value: formattedValueToString(arc.data.display),
},
];
}
function getLabelPos(arc: PieArcDatum<FieldDisplay>, outerRadius: number, innerRadius: number) {
const r = (outerRadius + innerRadius) / 2;
const a = (+arc.startAngle + +arc.endAngle) / 2 - Math.PI / 2;
@ -382,7 +427,7 @@ const getStyles = (theme: GrafanaTheme) => {
}
`,
tooltipPortal: css`
z-index: 1050;
${getTooltipContainerStyles(theme)}
`,
};
};

View File

@ -1,4 +1,5 @@
import { DataFrame, FieldConfigSource, FieldDisplay, InterpolateFunction, ReduceDataOptions } from '@grafana/data';
import { VizTooltipOptions } from '../VizTooltip';
import { VizLegendOptions } from '..';
export interface PieChartSvgProps {
@ -9,10 +10,12 @@ export interface PieChartSvgProps {
displayLabels?: PieChartLabels[];
useGradients?: boolean;
onSeriesColorChange?: (label: string, color: string) => void;
tooltipOptions: VizTooltipOptions;
}
export interface PieChartProps extends Omit<PieChartSvgProps, 'fieldDisplayValues'> {
legendOptions?: PieChartLegendOptions;
tooltipOptions: VizTooltipOptions;
reduceOptions: ReduceDataOptions;
fieldConfig: FieldConfigSource<any>;
replaceVariables: InterpolateFunction;

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Story, Meta } from '@storybook/react';
import { SeriesTable, SeriesTableProps } from './SeriesTable';
export default {
title: 'VizTooltip/SeriesTable',
component: SeriesTable,
argTypes: {
timestamp: {
control: 'date',
},
},
} as Meta;
const Template: Story<SeriesTableProps> = (args) => {
const date = new Date(args.timestamp!).toLocaleString();
return (
<div>
<SeriesTable {...args} timestamp={date} />
</div>
);
};
export const basic = Template.bind({});
basic.args = {
timestamp: new Date('2021-01-01T00:00:00').toISOString(),
series: [
{
color: '#299c46',
label: 'label 1',
value: '100 W',
},
],
};
export const multi = Template.bind({});
multi.args = {
timestamp: new Date('2021-01-01T00:00:00').toISOString(),
series: [
{
color: '#299c46',
label: 'label 1',
value: '100 W',
isActive: false,
},
{
color: '#9933cc',
label: 'label yes',
value: '25 W',
isActive: true,
},
{
color: '#eb7b18',
label: 'label 3',
value: '150 W',
isActive: false,
},
],
};

View File

@ -1,10 +1,12 @@
import React from 'react';
import { stylesFactory } from '../../../themes/stylesFactory';
import { GrafanaTheme, GraphSeriesValue } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { SeriesIcon } from '../../VizLegend/SeriesIcon';
import { useTheme } from '../../../themes';
import { SeriesIcon } from '../VizLegend/SeriesIcon';
import { useStyles } from '../../themes';
/**
* @public
*/
export interface SeriesTableRowProps {
color?: string;
label?: string;
@ -12,10 +14,11 @@ export interface SeriesTableRowProps {
isActive?: boolean;
}
const getSeriesTableRowStyles = stylesFactory((theme: GrafanaTheme) => {
const getSeriesTableRowStyles = (theme: GrafanaTheme) => {
return {
icon: css`
margin-right: ${theme.spacing.xs};
vertical-align: middle;
`,
seriesTable: css`
display: table;
@ -41,11 +44,11 @@ const getSeriesTableRowStyles = stylesFactory((theme: GrafanaTheme) => {
font-size: ${theme.typography.size.sm};
`,
};
});
};
const SeriesTableRow: React.FC<SeriesTableRowProps> = ({ color, label, value, isActive }) => {
const theme = useTheme();
const styles = getSeriesTableRowStyles(theme);
const styles = useStyles(getSeriesTableRowStyles);
return (
<div className={cx(styles.seriesTableRow, isActive && styles.activeSeries)}>
{color && (
@ -59,14 +62,19 @@ const SeriesTableRow: React.FC<SeriesTableRowProps> = ({ color, label, value, is
);
};
interface SeriesTableProps {
/**
* @public
*/
export interface SeriesTableProps {
timestamp?: string | GraphSeriesValue;
series: SeriesTableRowProps[];
}
/**
* @public
*/
export const SeriesTable: React.FC<SeriesTableProps> = ({ timestamp, series }) => {
const theme = useTheme();
const styles = getSeriesTableRowStyles(theme);
const styles = useStyles(getSeriesTableRowStyles);
return (
<>

View File

@ -3,9 +3,9 @@ import { css } from '@emotion/css';
import { Portal } from '../Portal/Portal';
import { Dimensions, TimeZone } from '@grafana/data';
import { FlotPosition } from '../Graph/types';
import { TooltipContainer } from './TooltipContainer';
import { VizTooltipContainer } from './VizTooltipContainer';
import { useStyles } from '../../themes';
import { TooltipMode } from './models.gen';
import { TooltipDisplayMode } from './models.gen';
// Describes active dimensions user interacts with
// It's a key-value pair where:
@ -14,7 +14,7 @@ import { TooltipMode } from './models.gen';
// If row is undefined, it means that we are not hovering over a datapoint
export type ActiveDimensions<T extends Dimensions = any> = { [key in keyof T]: [number, number | undefined] | null };
export interface TooltipContentProps<T extends Dimensions = any> {
export interface VizTooltipContentProps<T extends Dimensions = any> {
// Each dimension is described by array of fields representing it
// I.e. for graph there are two dimensions: x and y axis:
// { xAxis: [<array of time fields>], yAxis: [<array of value fields>]}
@ -23,15 +23,15 @@ export interface TooltipContentProps<T extends Dimensions = any> {
activeDimensions?: ActiveDimensions<T>;
timeZone?: TimeZone;
pos: FlotPosition;
mode: TooltipMode;
mode: TooltipDisplayMode;
}
export interface TooltipProps {
export interface VizTooltipProps {
/** Element used as tooltips content */
content?: React.ReactElement<any>;
/** Optional component to be used as a tooltip content */
tooltipComponent?: React.ComponentType<TooltipContentProps>;
tooltipComponent?: React.ComponentType<VizTooltipContentProps>;
/** x/y position relative to the window */
position?: { x: number; y: number };
@ -42,24 +42,27 @@ export interface TooltipProps {
// Mode in which tooltip works
// - single - display single series info
// - multi - display all series info
mode?: TooltipMode;
mode?: TooltipDisplayMode;
}
export const Tooltip: React.FC<TooltipProps> = ({ content, position, offset }) => {
/**
* @public
*/
export const VizTooltip: React.FC<VizTooltipProps> = ({ content, position, offset }) => {
const styles = useStyles(getStyles);
if (position) {
return (
<Portal className={styles.portal}>
<TooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
<VizTooltipContainer position={position} offset={offset || { x: 0, y: 0 }}>
{content}
</TooltipContainer>
</VizTooltipContainer>
</Portal>
);
}
return null;
};
Tooltip.displayName = 'ChartTooltip';
VizTooltip.displayName = 'VizTooltip';
const getStyles = () => {
return {

View File

@ -1,24 +1,29 @@
import React, { useState, useLayoutEffect, useRef, HTMLAttributes, useMemo } from 'react';
import { stylesFactory } from '../../themes/stylesFactory';
import { css, cx } from '@emotion/css';
import { useTheme } from '../../themes/ThemeContext';
import { useStyles } from '../../themes';
import { getTooltipContainerStyles } from '../../themes/mixins';
import useWindowSize from 'react-use/lib/useWindowSize';
import { Dimensions2D, GrafanaTheme } from '@grafana/data';
interface TooltipContainerProps extends HTMLAttributes<HTMLDivElement> {
/**
* @public
*/
export interface VizTooltipContainerProps extends HTMLAttributes<HTMLDivElement> {
position: { x: number; y: number };
offset: { x: number; y: number };
children?: JSX.Element;
}
export const TooltipContainer: React.FC<TooltipContainerProps> = ({
/**
* @public
*/
export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
position: { x: positionX, y: positionY },
offset: { x: offsetX, y: offsetY },
children,
className,
...otherProps
}) => {
const theme = useTheme();
const tooltipRef = useRef<HTMLDivElement>(null);
const tooltipMeasurementRef = useRef<Dimensions2D>({ width: 0, height: 0 });
const { width, height } = useWindowSize();
@ -80,7 +85,7 @@ export const TooltipContainer: React.FC<TooltipContainerProps> = ({
});
}, [width, height, positionX, offsetX, positionY, offsetY]);
const styles = getTooltipContainerStyles(theme);
const styles = useStyles(getStyles);
return (
<div
@ -99,18 +104,10 @@ export const TooltipContainer: React.FC<TooltipContainerProps> = ({
);
};
TooltipContainer.displayName = 'TooltipContainer';
VizTooltipContainer.displayName = 'VizTooltipContainer';
const getTooltipContainerStyles = stylesFactory((theme: GrafanaTheme) => {
return {
wrapper: css`
overflow: hidden;
background: ${theme.colors.bg2};
/* max-width is set up based on .grafana-tooltip class that's used in dashboard */
max-width: 800px;
padding: ${theme.spacing.sm};
border-radius: ${theme.border.radius.sm};
z-index: ${theme.zIndex.tooltip};
`,
};
const getStyles = (theme: GrafanaTheme) => ({
wrapper: css`
${getTooltipContainerStyles(theme)}
`,
});

View File

@ -0,0 +1,4 @@
export { VizTooltip, VizTooltipContentProps, VizTooltipProps, ActiveDimensions } from './VizTooltip';
export { VizTooltipContainer, VizTooltipContainerProps } from './VizTooltipContainer';
export { SeriesTable, SeriesTableProps, SeriesTableRowProps } from './SeriesTable';
export { TooltipDisplayMode, VizTooltipOptions } from './models.gen';

View File

@ -0,0 +1,7 @@
package grafanaschema
TooltipDisplayMode: "single" | "multi" | "none" @cuetsy(targetType="enum")
VizTooltipOptions: {
mode: TooltipDisplayMode
} @cuetsy(targetType="interface")

View File

@ -3,8 +3,18 @@
// It is currenty hand written but will serve as the target for cuetsy
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import { TooltipMode } from '../../Chart/models.gen';
export interface GraphTooltipOptions {
mode: TooltipMode;
/**
* @public
*/
export enum TooltipDisplayMode {
Single = 'single',
Multi = 'multi',
None = 'none',
}
/**
* @public
*/
export type VizTooltipOptions = {
mode: TooltipDisplayMode;
};

View File

@ -74,7 +74,15 @@ export { Graph } from './Graph/Graph';
export { GraphWithLegend } from './Graph/GraphWithLegend';
export { GraphContextMenu, GraphContextMenuHeader } from './Graph/GraphContextMenu';
export { BarGauge, BarGaugeDisplayMode } from './BarGauge/BarGauge';
export { GraphTooltipOptions } from './Graph/GraphTooltip/types';
export {
VizTooltip,
VizTooltipContainer,
SeriesTable,
VizTooltipOptions,
TooltipDisplayMode,
SeriesTableProps,
SeriesTableRowProps,
} from './VizTooltip';
export { VizRepeater, VizRepeaterRenderValueProps } from './VizRepeater/VizRepeater';
export { graphTimeFormat, graphTickFormatter } from './Graph/utils';
export {
@ -132,8 +140,6 @@ export { Spinner } from './Spinner/Spinner';
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';
export { RangeSlider } from './Slider/RangeSlider';

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Portal } from '../../Portal/Portal';
import { usePlotContext } from '../context';
import { CursorPlugin } from './CursorPlugin';
import { SeriesTable, SeriesTableRowProps } from '../../Graph/GraphTooltip/SeriesTable';
import { VizTooltipContainer, SeriesTable, SeriesTableRowProps, TooltipDisplayMode } from '../../VizTooltip';
import {
DataFrame,
FieldType,
@ -11,12 +11,10 @@ import {
getFieldDisplayName,
TimeZone,
} from '@grafana/data';
import { TooltipContainer } from '../../Chart/TooltipContainer';
import { TooltipMode } from '../../Chart/models.gen';
import { useGraphNGContext } from '../../GraphNG/hooks';
interface TooltipPluginProps {
mode?: TooltipMode;
mode?: TooltipDisplayMode;
timeZone: TimeZone;
data: DataFrame[];
}
@ -121,9 +119,9 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
return (
<Portal>
<TooltipContainer position={{ x: coords.viewport.x, y: coords.viewport.y }} offset={{ x: 10, y: 10 }}>
<VizTooltipContainer position={{ x: coords.viewport.x, y: coords.viewport.y }} offset={{ x: 10, y: 10 }}>
{tooltip}
</TooltipContainer>
</VizTooltipContainer>
</Portal>
);
}}

View File

@ -4,13 +4,13 @@ import tinycolor from 'tinycolor2';
export function cardChrome(theme: GrafanaTheme): string {
return `
background: ${theme.colors.bg2};
&:hover {
background: ${hoverColor(theme.colors.bg2, theme)};
}
box-shadow: ${theme.shadows.listItem};
border-radius: ${theme.border.radius.md};
`;
background: ${theme.colors.bg2};
&:hover {
background: ${hoverColor(theme.colors.bg2, theme)};
}
box-shadow: ${theme.shadows.listItem};
border-radius: ${theme.border.radius.md};
`;
}
export function hoverColor(color: string, theme: GrafanaTheme): string {
@ -30,9 +30,9 @@ export function listItem(theme: GrafanaTheme): string {
export function listItemSelected(theme: GrafanaTheme): string {
return `
background: ${hoverColor(theme.colors.bg2, theme)};
color: ${theme.colors.textStrong};
`;
background: ${hoverColor(theme.colors.bg2, theme)};
color: ${theme.colors.textStrong};
`;
}
export function mediaUp(breakpoint: string) {
@ -61,3 +61,13 @@ export function getFocusStyles(theme: GrafanaThemeV2): CSSObject {
transition: `all 0.2s cubic-bezier(0.19, 1, 0.22, 1)`,
};
}
// max-width is set up based on .grafana-tooltip class that's used in dashboard
export const getTooltipContainerStyles = (theme: GrafanaTheme) => `
overflow: hidden;
background: ${theme.colors.bg2};
max-width: 800px;
padding: ${theme.spacing.sm};
border-radius: ${theme.border.radius.sm};
z-index: ${theme.zIndex.tooltip};
`;

View File

@ -21,6 +21,7 @@ import {
useStyles,
useTheme,
ZoomPlugin,
TooltipDisplayMode,
} from '@grafana/ui';
import { defaultGraphConfig, getGraphFieldConfig } from 'app/plugins/panel/timeseries/config';
import { hideSeriesConfigFactory } from 'app/plugins/panel/timeseries/overrides/hideSeriesConfigFactory';
@ -129,7 +130,7 @@ export function ExploreGraphNGPanel({
timeZone={timeZone}
>
<ZoomPlugin onZoom={onUpdateTimeRange} />
<TooltipPlugin data={data} mode="single" timeZone={timeZone} />
<TooltipPlugin data={data} mode={TooltipDisplayMode.Single} timeZone={timeZone} />
<ContextMenuPlugin data={data} timeZone={timeZone} />
{annotations && <ExemplarsPlugin exemplars={annotations} timeZone={timeZone} getFieldLinks={getFieldLinks} />}
</GraphNG>

View File

@ -11,8 +11,9 @@ import {
Collapse,
GraphSeriesToggler,
GraphSeriesTogglerAPI,
Chart,
VizTooltip,
Icon,
TooltipDisplayMode,
} from '@grafana/ui';
const MAX_NUMBER_OF_TIME_SERIES = 20;
@ -130,7 +131,7 @@ class UnThemedExploreGraphPanel extends PureComponent<Props, State> {
onHorizontalRegionSelected={this.onChangeTime}
>
{/* For logs we are using mulit mode until we refactor logs histogram to use barWidth instead of lineWidth to render bars */}
<Chart.Tooltip mode={showBars ? 'multi' : 'single'} />
<VizTooltip mode={showBars ? TooltipDisplayMode.Multi : TooltipDisplayMode.Single} />
</GraphWithLegend>
);
}}

View File

@ -1,4 +1,4 @@
import { GraphTooltipOptions, LegendDisplayMode, LegendPlacement } from '@grafana/ui';
import { VizTooltipOptions, TooltipDisplayMode, LegendDisplayMode, LegendPlacement } from '@grafana/ui';
import { YAxis } from '@grafana/data';
export interface SeriesOptions {
@ -21,7 +21,7 @@ export interface Options {
series: {
[alias: string]: SeriesOptions;
};
tooltipOptions: GraphTooltipOptions;
tooltipOptions: VizTooltipOptions;
}
export const defaults: Options = {
@ -35,7 +35,7 @@ export const defaults: Options = {
placement: 'bottom',
},
series: {},
tooltipOptions: { mode: 'single' },
tooltipOptions: { mode: TooltipDisplayMode.Single },
};
export interface GraphLegendEditorLegendOptions {

View File

@ -36,6 +36,7 @@ export const PieChartPanel: React.FC<Props> = ({
pieType={options.pieType}
displayLabels={options.displayLabels}
legendOptions={options.legend}
tooltipOptions={options.tooltip}
/>
);
};

View File

@ -49,6 +49,19 @@ export const plugin = new PanelPlugin<PieChartOptions>(PieChartPanel)
],
},
})
.addRadio({
name: 'Tooltip mode',
path: 'tooltip.mode',
description: '',
defaultValue: 'single',
settings: {
options: [
{ value: 'single', label: 'Single' },
{ value: 'multi', label: 'All' },
{ value: 'none', label: 'Hidden' },
],
},
})
.addRadio({
path: 'legend.displayMode',
name: 'Legend mode',

View File

@ -1,7 +1,13 @@
import { PieChartType, SingleStatBaseOptions, PieChartLabels, PieChartLegendOptions } from '@grafana/ui';
import {
PieChartType,
SingleStatBaseOptions,
PieChartLabels,
PieChartLegendOptions,
VizTooltipOptions,
} from '@grafana/ui';
export interface PieChartOptions extends SingleStatBaseOptions {
pieType: PieChartType;
displayLabels: PieChartLabels[];
legend: PieChartLegendOptions;
tooltip: VizTooltipOptions;
}

View File

@ -20,6 +20,7 @@ import {
LineStyle,
PointVisibility,
StackingMode,
TooltipDisplayMode,
} from '@grafana/ui';
import { Options } from './types';
import omitBy from 'lodash/omitBy';
@ -291,7 +292,7 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
calcs: [],
},
tooltipOptions: {
mode: 'single',
mode: TooltipDisplayMode.Single,
},
};

View File

@ -1,6 +1,6 @@
import React, { useCallback, useRef, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { HorizontalGroup, Portal, Tag, TooltipContainer, useStyles } from '@grafana/ui';
import { HorizontalGroup, Portal, Tag, VizTooltipContainer, useStyles } from '@grafana/ui';
import { css } from '@emotion/css';
interface AnnotationMarkerProps {
@ -38,7 +38,7 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
const elBBox = el.getBoundingClientRect();
return (
<TooltipContainer
<VizTooltipContainer
position={{ x: elBBox.left, y: elBBox.top + elBBox.height }}
offset={{ x: 0, y: 0 }}
onMouseEnter={onMouseEnter}
@ -61,7 +61,7 @@ export const AnnotationMarker: React.FC<AnnotationMarkerProps> = ({ time, text,
</>
</div>
</div>
</TooltipContainer>
</VizTooltipContainer>
);
}, [onMouseEnter, onMouseLeave, styles, time, text, tags]);

View File

@ -8,7 +8,7 @@ import {
systemDateFormats,
TimeZone,
} from '@grafana/data';
import { FieldLinkList, Portal, TooltipContainer, useStyles } from '@grafana/ui';
import { FieldLinkList, Portal, VizTooltipContainer, useStyles } from '@grafana/ui';
import { css, cx } from '@emotion/css';
import React, { useCallback, useRef, useState } from 'react';
@ -58,7 +58,7 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
const elBBox = el.getBoundingClientRect();
return (
<TooltipContainer
<VizTooltipContainer
position={{ x: elBBox.left, y: elBBox.top + elBBox.height }}
offset={{ x: 0, y: 0 }}
onMouseEnter={onMouseEnter}
@ -93,7 +93,7 @@ export const ExemplarMarker: React.FC<ExemplarMarkerProps> = ({ timeZone, dataFr
</div>
</div>
</div>
</TooltipContainer>
</VizTooltipContainer>
);
}, [dataFrame.fields, getFieldLinks, index, onMouseEnter, onMouseLeave, styles, timeFormatter]);

View File

@ -1,9 +1,9 @@
import { VizLegendOptions, GraphTooltipOptions } from '@grafana/ui';
import { VizLegendOptions, VizTooltipOptions } from '@grafana/ui';
export interface OptionsWithLegend {
legend: VizLegendOptions;
}
export interface Options extends OptionsWithLegend {
tooltipOptions: GraphTooltipOptions;
tooltipOptions: VizTooltipOptions;
}

View File

@ -1,4 +1,4 @@
import { GraphTooltipOptions } from '@grafana/ui';
import { VizTooltipOptions } from '@grafana/ui';
import { OptionsWithLegend } from '../timeseries/types';
export interface XYDimensionConfig {
@ -9,5 +9,5 @@ export interface XYDimensionConfig {
export interface Options extends OptionsWithLegend {
dims: XYDimensionConfig;
tooltipOptions: GraphTooltipOptions;
tooltipOptions: VizTooltipOptions;
}

View File

@ -46,13 +46,13 @@ cue def -s $(find packages/grafana-data -type f -name "*.cue") > cue/data/gen.cu
#
# It's important to understand why this is necessary, though. We are expecting
# that these core components may depend on each other - e.g., how
# GraphTooltipOptions composes in TooltipMode. We have to preserve those
# VizTooltipOptions composes in TooltipDisplayMode. We have to preserve those
# literal identifiers in our assembled CUE, so that when a panel plugin's
# models.cue imports and references something like GraphTooltipOptions in CUE,
# models.cue imports and references something like VizTooltipOptions in CUE,
# it's still the same identifier as appeared in the original core models.cue
# files, AND therefore is exactly the identifier that appears in
# cuetsy-generated @grafana/{ui,data} packages. That is, as long as we preserve
# the relation between the identifier "GraphTooltipOptions" as a top-level
# the relation between the identifier "VizTooltipOptions" as a top-level
# importable thing at all stages on the CUE side, then everything on the
# TypeScript side will line up.
sed -i -e 's/^import.*//g' {cue/ui/gen.cue,cue/data/gen.cue}