From bac8b967be1876bb01538effa42993116caf2cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 28 Apr 2021 14:02:41 +0200 Subject: [PATCH] PieChart: Refactoring & adding unsubscribe (#33432) * Tweaks to piechart and theme * Adds unsubscribe to events and move out to separate hook * reverted constrast change * Minor refactor after review feedback * chain the subs --- .../src/components/PieChart/PieChart.tsx | 323 +++++++++--------- .../VizTooltip/VizTooltipContainer.tsx | 8 +- packages/grafana-ui/src/themes/mixins.ts | 8 +- 3 files changed, 179 insertions(+), 160 deletions(-) diff --git a/packages/grafana-ui/src/components/PieChart/PieChart.tsx b/packages/grafana-ui/src/components/PieChart/PieChart.tsx index 1ce67377c81..3b3fd3cc50c 100644 --- a/packages/grafana-ui/src/components/PieChart/PieChart.tsx +++ b/packages/grafana-ui/src/components/PieChart/PieChart.tsx @@ -1,4 +1,4 @@ -import React, { FC, ReactNode, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { DataHoverClearEvent, DataHoverEvent, @@ -6,9 +6,9 @@ import { FieldDisplay, formattedValueToString, getFieldDisplayValues, - GrafanaTheme, + GrafanaThemeV2, } from '@grafana/data'; -import { useStyles, useTheme } from '../../themes/ThemeContext'; +import { useStyles2, useTheme2 } from '../../themes/ThemeContext'; import tinycolor from 'tinycolor2'; import Pie, { PieArcDatum, ProvidedProps } from '@visx/shape/lib/shapes/Pie'; import { Group } from '@visx/group'; @@ -33,6 +33,7 @@ import { import { getTooltipContainerStyles } from '../../themes/mixins'; import { SeriesTable, SeriesTableRowProps, VizTooltipOptions } from '../VizTooltip'; import { usePanelContext } from '../PanelChrome'; +import { Subscription } from 'rxjs'; const defaultLegendOptions: PieChartLegendOptions = { displayMode: LegendDisplayMode.List, @@ -44,99 +45,33 @@ const defaultLegendOptions: PieChartLegendOptions = { /** * @beta */ -export const PieChart: FC = ({ - data, - timeZone, - reduceOptions, - fieldConfig, - replaceVariables, - legendOptions = defaultLegendOptions, - tooltipOptions, - onSeriesColorChange, - width, - height, - ...restProps -}) => { - const theme = useTheme(); - const [highlightedTitle, setHighlightedTitle] = useState(); - const { eventBus } = usePanelContext(); - - if (eventBus) { - const setHighlightedSlice = (event: DataHoverEvent) => { - if (eventBus.isOwnEvent(event)) { - setHighlightedTitle(event.payload.dataId); - } - }; - - const resetHighlightedSlice = (event: DataHoverClearEvent) => { - if (eventBus.isOwnEvent(event)) { - setHighlightedTitle(undefined); - } - }; - - eventBus.subscribe(DataHoverEvent, setHighlightedSlice); - eventBus.subscribe(DataHoverClearEvent, resetHighlightedSlice); - } - - const getLegend = (fields: FieldDisplay[], legendOptions: PieChartLegendOptions) => { - if (legendOptions.displayMode === LegendDisplayMode.Hidden) { - return undefined; - } - const values = fields.map((v) => v.display); - const total = values.reduce((acc, item) => item.numeric + acc, 0); - - const legendItems = values.map((value, idx) => { - return { - label: value.title ?? '', - color: value.color ?? FALLBACK_COLOR, - yAxis: 1, - getItemKey: () => (value.title ?? '') + idx, - getDisplayValues: () => { - const valuesToShow = legendOptions.values ?? []; - let displayValues = []; - - if (valuesToShow.includes(PieChartLegendValues.Value)) { - displayValues.push({ numeric: value.numeric, text: formattedValueToString(value), title: 'Value' }); - } - - if (valuesToShow.includes(PieChartLegendValues.Percent)) { - const fractionOfTotal = value.numeric / total; - const percentOfTotal = fractionOfTotal * 100; - - displayValues.push({ - numeric: fractionOfTotal, - percent: percentOfTotal, - text: percentOfTotal.toFixed(0) + '%', - title: valuesToShow.length > 1 ? 'Percent' : undefined, - }); - } - - return displayValues; - }, - }; - }); - - return ( - - ); - }; +export function PieChart(props: PieChartProps) { + const { + data, + timeZone, + reduceOptions, + fieldConfig, + replaceVariables, + tooltipOptions, + onSeriesColorChange, + width, + height, + ...restProps + } = props; + const theme = useTheme2(); + const highlightedTitle = useSliceHighlightState(); const fieldDisplayValues = getFieldDisplayValues({ fieldConfig, reduceOptions, data, - theme, + theme: theme.v1, replaceVariables, timeZone, }); return ( - + {(vizWidth: number, vizHeight: number) => { return ( = ({ }} ); -}; +} + +function getLegend(props: PieChartProps, displayValues: FieldDisplay[]) { + const { legendOptions = defaultLegendOptions } = props; + + if (legendOptions.displayMode === LegendDisplayMode.Hidden) { + return undefined; + } + const values = displayValues.map((v) => v.display); + const total = values.reduce((acc, item) => item.numeric + acc, 0); + + const legendItems = values.map((value, idx) => { + return { + label: value.title ?? '', + color: value.color ?? FALLBACK_COLOR, + yAxis: 1, + getItemKey: () => (value.title ?? '') + idx, + getDisplayValues: () => { + const valuesToShow = legendOptions.values ?? []; + let displayValues = []; + + if (valuesToShow.includes(PieChartLegendValues.Value)) { + displayValues.push({ numeric: value.numeric, text: formattedValueToString(value), title: 'Value' }); + } + + if (valuesToShow.includes(PieChartLegendValues.Percent)) { + const fractionOfTotal = value.numeric / total; + const percentOfTotal = fractionOfTotal * 100; + + displayValues.push({ + numeric: fractionOfTotal, + percent: percentOfTotal, + text: percentOfTotal.toFixed(0) + '%', + title: valuesToShow.length > 1 ? 'Percent' : undefined, + }); + } + + return displayValues; + }, + }; + }); + + return ( + + ); +} + +function useSliceHighlightState() { + const [highlightedTitle, setHighlightedTitle] = useState(); + const { eventBus } = usePanelContext(); + + useEffect(() => { + if (!eventBus) { + return; + } + + const setHighlightedSlice = (event: DataHoverEvent) => { + if (eventBus.isOwnEvent(event)) { + setHighlightedTitle(event.payload.dataId); + } + }; + + const resetHighlightedSlice = (event: DataHoverClearEvent) => { + if (eventBus.isOwnEvent(event)) { + setHighlightedTitle(undefined); + } + }; + + const subs = new Subscription() + .add(eventBus.subscribe(DataHoverEvent, setHighlightedSlice)) + .add(eventBus.subscribe(DataHoverClearEvent, resetHighlightedSlice)); + + return () => { + subs.unsubscribe(); + }; + }, [setHighlightedTitle, eventBus]); + + return highlightedTitle; +} export const PieChartSvg: FC = ({ fieldDisplayValues, @@ -159,13 +177,12 @@ export const PieChartSvg: FC = ({ width, height, highlightedTitle, - useGradients = true, displayLabels = [], tooltipOptions, }) => { - const theme = useTheme(); + const theme = useTheme2(); const componentInstanceId = useComponentInstanceId('PieChart'); - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); const tooltip = useTooltip(); const { containerRef, TooltipInPortal } = useTooltipInPortal({ detectBounds: true, @@ -218,55 +235,55 @@ export const PieChartSvg: FC = ({ cornerRadius={3} padAngle={0.005} > - {(pie) => { - return pie.arcs.map((arc) => { - const color = arc.data.display.color ?? FALLBACK_COLOR; - const highlighted = highlightedTitle === arc.data.display.title; - const label = showLabel ? ( - - ) : undefined; - if (arc.data.hasLinks && arc.data.getLinks) { - return ( - - {(api) => ( - - {label} - - )} - - ); - } else { - return ( - ( + <> + {pie.arcs.map((arc) => { + let color = arc.data.display.color ?? FALLBACK_COLOR; + const highlighted = highlightedTitle === arc.data.display.title; + if (arc.data.hasLinks && arc.data.getLinks) { + return ( + + {(api) => ( + + )} + + ); + } else { + return ( + + ); + } + })} + {showLabel && + pie.arcs.map((arc) => ( + - {label} - - ); - } - }); - }} + key={arc.index} + outerRadius={layout.outerRadius} + innerRadius={layout.innerRadius} + displayLabels={displayLabels} + total={total} + color={theme.colors.text.primary} + /> + ))} + + )} @@ -286,8 +303,7 @@ export const PieChartSvg: FC = ({ ); }; -const PieSlice: FC<{ - children: ReactNode; +interface SliceProps { arc: PieArcDatum; pie: ProvidedProps; highlighted?: boolean; @@ -295,9 +311,11 @@ const PieSlice: FC<{ tooltip: UseTooltipParams; tooltipOptions: VizTooltipOptions; openMenu?: (event: React.MouseEvent) => void; -}> = ({ arc, children, pie, highlighted, openMenu, fill, tooltip, tooltipOptions }) => { - const theme = useTheme(); - const styles = useStyles(getStyles); +} + +function PieSlice({ arc, pie, highlighted, openMenu, fill, tooltip, tooltipOptions }: SliceProps) { + const theme = useTheme2(); + const styles = useStyles2(getStyles); const onMouseMoveOverArc = (event: any) => { const coords = localPoint(event.target.ownerSVGElement, event); @@ -316,20 +334,21 @@ const PieSlice: FC<{ onMouseOut={tooltip.hideTooltip} onClick={openMenu} > - - {children} + ); -}; +} -const PieLabel: FC<{ +interface LabelProps { arc: PieArcDatum; outerRadius: number; innerRadius: number; displayLabels: PieChartLabels[]; total: number; color: string; -}> = ({ arc, outerRadius, innerRadius, displayLabels, total, color }) => { +} + +function PieLabel({ arc, outerRadius, innerRadius, displayLabels, total, color }: LabelProps) { const labelRadius = innerRadius === 0 ? outerRadius / 6 : innerRadius; const [labelX, labelY] = getLabelPos(arc, outerRadius, labelRadius); const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.3; @@ -371,7 +390,7 @@ const PieLabel: FC<{ ); -}; +} function getTooltipData( pie: ProvidedProps, @@ -403,14 +422,14 @@ function getLabelPos(arc: PieArcDatum, outerRadius: number, innerR return [Math.cos(a) * r, Math.sin(a) * r]; } -function getGradientColorFrom(color: string, theme: GrafanaTheme) { +function getGradientColorFrom(color: string, theme: GrafanaThemeV2) { return tinycolor(color) .darken(20 * (theme.isDark ? 1 : -0.7)) .spin(8) .toRgbString(); } -function getGradientColorTo(color: string, theme: GrafanaTheme) { +function getGradientColorTo(color: string, theme: GrafanaThemeV2) { return tinycolor(color) .darken(10 * (theme.isDark ? 1 : -0.7)) .spin(-8) @@ -442,7 +461,7 @@ function getPieLayout(height: number, width: number, pieType: PieChartType, marg }; } -const getStyles = (theme: GrafanaTheme) => { +const getStyles = (theme: GrafanaThemeV2) => { return { container: css` width: 100%; diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx index 573e6ca60e3..62b4c8bda44 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipContainer.tsx @@ -1,9 +1,9 @@ import React, { useState, useLayoutEffect, useRef, HTMLAttributes, useMemo } from 'react'; import { css, cx } from '@emotion/css'; -import { useStyles } from '../../themes'; +import { useStyles2 } from '../../themes'; import { getTooltipContainerStyles } from '../../themes/mixins'; import useWindowSize from 'react-use/lib/useWindowSize'; -import { Dimensions2D, GrafanaTheme } from '@grafana/data'; +import { Dimensions2D, GrafanaThemeV2 } from '@grafana/data'; /** * @public @@ -84,7 +84,7 @@ export const VizTooltipContainer: React.FC = ({ }); }, [width, height, positionX, offsetX, positionY, offsetY, tooltipMeasurement.width, tooltipMeasurement.height]); - const styles = useStyles(getStyles); + const styles = useStyles2(getStyles); return (
= ({ VizTooltipContainer.displayName = 'VizTooltipContainer'; -const getStyles = (theme: GrafanaTheme) => ({ +const getStyles = (theme: GrafanaThemeV2) => ({ wrapper: css` ${getTooltipContainerStyles(theme)} `, diff --git a/packages/grafana-ui/src/themes/mixins.ts b/packages/grafana-ui/src/themes/mixins.ts index ab43a9c48eb..aa2043743ef 100644 --- a/packages/grafana-ui/src/themes/mixins.ts +++ b/packages/grafana-ui/src/themes/mixins.ts @@ -63,11 +63,11 @@ export function getFocusStyles(theme: GrafanaThemeV2): CSSObject { } // max-width is set up based on .grafana-tooltip class that's used in dashboard -export const getTooltipContainerStyles = (theme: GrafanaTheme) => ` +export const getTooltipContainerStyles = (theme: GrafanaThemeV2) => ` overflow: hidden; - background: ${theme.colors.bg2}; + background: ${theme.components.tooltip.background}; max-width: 800px; - padding: ${theme.spacing.sm}; - border-radius: ${theme.border.radius.sm}; + padding: ${theme.spacing(1)}; + border-radius: ${theme.shape.borderRadius()}; z-index: ${theme.zIndex.tooltip}; `;