From 4fa26a2dadca181b0bfa7ea2a2a579533aba578d Mon Sep 17 00:00:00 2001 From: plouc Date: Mon, 20 Nov 2023 15:31:19 +0900 Subject: [PATCH] feat(pie): add support for forwarding legend data --- packages/pie/src/Pie.tsx | 5 +- packages/pie/src/PieCanvas.tsx | 2 + packages/pie/src/PieLegends.tsx | 8 +-- packages/pie/src/hooks.ts | 35 ++++++++-- packages/pie/src/types.ts | 9 +++ packages/waffle/src/hooks.ts | 2 +- storybook/stories/pie/Pie.stories.tsx | 72 ++++++++++++++++++++- storybook/stories/pie/PieCanvas.stories.tsx | 72 ++++++++++++++++++++- storybook/stories/waffle/Waffle.stories.tsx | 2 +- website/src/data/components/pie/meta.yml | 4 ++ website/src/data/components/pie/props.ts | 22 +++++++ 11 files changed, 216 insertions(+), 17 deletions(-) diff --git a/packages/pie/src/Pie.tsx b/packages/pie/src/Pie.tsx index d543754ed..2f6eb159a 100644 --- a/packages/pie/src/Pie.tsx +++ b/packages/pie/src/Pie.tsx @@ -8,7 +8,7 @@ import { } from '@nivo/core' import { ArcLabelsLayer, ArcLinkLabelsLayer } from '@nivo/arcs' import { InheritedColorConfig } from '@nivo/colors' -import PieLegends from './PieLegends' +import { PieLegends } from './PieLegends' import { useNormalizedData, usePieFromBox, usePieLayerContext } from './hooks' import { ComputedDatum, PieLayer, PieSvgProps, PieLayerId, MayHaveLabel } from './types' import { defaultProps } from './props' @@ -81,6 +81,8 @@ const InnerPie = ({ transitionMode = defaultProps.transitionMode, legends = defaultProps.legends, + forwardLegendData, + role = defaultProps.role, }: PieSvgProps) => { const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions( @@ -123,6 +125,7 @@ const InnerPie = ({ activeId: activeIdFromProps, onActiveIdChange, defaultActiveId, + forwardLegendData, }) const boundDefs = bindDefs(defs, dataWithArc, fill) diff --git a/packages/pie/src/PieCanvas.tsx b/packages/pie/src/PieCanvas.tsx index c7e9f1311..2d59b4b22 100644 --- a/packages/pie/src/PieCanvas.tsx +++ b/packages/pie/src/PieCanvas.tsx @@ -72,6 +72,7 @@ const InnerPieCanvas = ({ defaultActiveId, legends = defaultProps.legends, + forwardLegendData, }: PieCanvasProps) => { const canvasEl = useRef(null) const theme = useTheme() @@ -107,6 +108,7 @@ const InnerPieCanvas = ({ activeId: activeIdFromProps, onActiveIdChange, defaultActiveId, + forwardLegendData, }) const getBorderColor = useInheritedColor>(borderColor, theme) diff --git a/packages/pie/src/PieLegends.tsx b/packages/pie/src/PieLegends.tsx index 481ad9823..f461dffdc 100644 --- a/packages/pie/src/PieLegends.tsx +++ b/packages/pie/src/PieLegends.tsx @@ -1,15 +1,15 @@ import { BoxLegendSvg } from '@nivo/legends' -import { CompletePieSvgProps, ComputedDatum, DatumId } from './types' +import { CompletePieSvgProps, DatumId, LegendDatum } from './types' interface PieLegendsProps { width: number height: number legends: CompletePieSvgProps['legends'] - data: Omit, 'arc'>[] + data: LegendDatum[] toggleSerie: (id: DatumId) => void } -const PieLegends = ({ +export const PieLegends = ({ width, height, legends, @@ -31,5 +31,3 @@ const PieLegends = ({ ) } - -export default PieLegends diff --git a/packages/pie/src/hooks.ts b/packages/pie/src/hooks.ts index d09a481dd..5fdb669cd 100644 --- a/packages/pie/src/hooks.ts +++ b/packages/pie/src/hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { pie as d3Pie } from 'd3-shape' import { useArcGenerator, computeArcBoundingBox } from '@nivo/arcs' import { @@ -16,6 +16,8 @@ import { DatumId, PieArc, PieCustomLayerProps, + LegendDatum, + CommonPieProps, } from './types' /** @@ -81,6 +83,7 @@ export const usePieArcs = ({ activeInnerRadiusOffset, activeOuterRadiusOffset, hiddenIds, + forwardLegendData, }: { data: Omit, 'arc' | 'fill'>[] // in degrees @@ -97,9 +100,10 @@ export const usePieArcs = ({ activeInnerRadiusOffset: number activeOuterRadiusOffset: number hiddenIds: DatumId[] + forwardLegendData?: CommonPieProps['forwardLegendData'] }): { dataWithArc: Omit, 'fill'>[] - legendData: Omit, 'arc' | 'fill'>[] + legendData: LegendDatum[] } => { const pie = useMemo(() => { const innerPie = d3Pie, 'arc' | 'fill'>>() @@ -115,7 +119,7 @@ export const usePieArcs = ({ return innerPie }, [startAngle, endAngle, padAngle, sortByValue]) - return useMemo(() => { + const result = useMemo(() => { const hiddenData = data.filter(item => !hiddenIds.includes(item.id)) const dataWithArc = pie(hiddenData).map( ( @@ -150,7 +154,13 @@ export const usePieArcs = ({ } } ) - const legendData = data.map(item => ({ ...item, hidden: hiddenIds.includes(item.id) })) + const legendData: LegendDatum[] = data.map(item => ({ + id: item.id, + label: item.label, + color: item.color, + hidden: hiddenIds.includes(item.id), + data: item, + })) return { dataWithArc, legendData } }, [ @@ -163,6 +173,16 @@ export const usePieArcs = ({ outerRadius, activeOuterRadiusOffset, ]) + + // Forward the legends data if `forwardLegendData` is defined. + const legendData = result.legendData + const forwardLegendDataRef = useRef(forwardLegendData) + useEffect(() => { + if (typeof forwardLegendDataRef.current !== 'function') return + forwardLegendDataRef.current(legendData) + }, [forwardLegendDataRef, legendData]) + + return result } /** @@ -222,6 +242,7 @@ export const usePie = ({ activeId: activeIdFromProps, onActiveIdChange, defaultActiveId, + forwardLegendData, }: Pick< Partial>, | 'startAngle' @@ -234,6 +255,7 @@ export const usePie = ({ | 'activeId' | 'onActiveIdChange' | 'defaultActiveId' + | 'forwardLegendData' > & { data: Omit, 'arc'>[] radius: number @@ -258,6 +280,7 @@ export const usePie = ({ activeInnerRadiusOffset, activeOuterRadiusOffset, hiddenIds, + forwardLegendData, }) const toggleSerie = useCallback((id: DatumId) => { @@ -295,6 +318,7 @@ export const usePieFromBox = ({ activeId: activeIdFromProps, onActiveIdChange, defaultActiveId, + forwardLegendData, }: Pick< CompletePieSvgProps, | 'width' @@ -311,7 +335,7 @@ export const usePieFromBox = ({ > & Pick< Partial>, - 'activeId' | 'onActiveIdChange' | 'defaultActiveId' + 'activeId' | 'onActiveIdChange' | 'defaultActiveId' | 'forwardLegendData' > & { data: Omit, 'arc'>[] }) => { @@ -382,6 +406,7 @@ export const usePieFromBox = ({ activeInnerRadiusOffset, activeOuterRadiusOffset, hiddenIds, + forwardLegendData, }) const toggleSerie = useCallback((id: DatumId) => { diff --git a/packages/pie/src/types.ts b/packages/pie/src/types.ts index 6f570da65..6e0239a21 100644 --- a/packages/pie/src/types.ts +++ b/packages/pie/src/types.ts @@ -118,6 +118,7 @@ export type CommonPieProps = { defaultActiveId: DatumId | null legends: readonly LegendProps[] + forwardLegendData: (data: LegendDatum[]) => void role: string renderWrapper: boolean @@ -135,6 +136,14 @@ export type PieSvgCustomComponents = { arcLinkLabelComponent?: ArcLinkLabelsProps>['component'] } +export interface LegendDatum { + id: ComputedDatum['id'] + label: ComputedDatum['label'] + color: string + hidden: boolean + data: Omit, 'fill' | 'arc'> +} + export type PieSvgProps = DataProps & Dimensions & Partial> & diff --git a/packages/waffle/src/hooks.ts b/packages/waffle/src/hooks.ts index 0c72b4607..933803752 100644 --- a/packages/waffle/src/hooks.ts +++ b/packages/waffle/src/hooks.ts @@ -240,7 +240,7 @@ export const useWaffle = ({ id: datum.id, label: datum.label, color: datum.color, - // fill: datum.fill,, + // fill: datum.fill, data: datum, })) diff --git a/storybook/stories/pie/Pie.stories.tsx b/storybook/stories/pie/Pie.stories.tsx index 10748a03e..ee38ddc9e 100644 --- a/storybook/stories/pie/Pie.stories.tsx +++ b/storybook/stories/pie/Pie.stories.tsx @@ -1,8 +1,8 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import type { Meta, StoryObj } from '@storybook/react' import { animated } from '@react-spring/web' import { generateProgrammingLanguageStats } from '@nivo/generators' -import { Pie } from '@nivo/pie' +import { LegendDatum, Pie } from '@nivo/pie' import { nivoTheme } from '../nivo-theme' const meta: Meta = { @@ -281,3 +281,71 @@ const ControlledPies = () => { export const ControlledActiveId: Story = { render: () => , } + +const PieWithCustomLegend = () => { + const [customLegends, setCustomLegends] = useState[]>([]) + + const valueFormat = useCallback( + (value: number) => + `${Number(value).toLocaleString('ru-RU', { + minimumFractionDigits: 2, + })} ₽`, + [] + ) + + return ( +
+ +
+ + + + + + + + + + + + {customLegends.map(legend => { + return ( + + + + + + + + ) + })} + +
ColorIDValueFormatted ValueLabel
+ + + {legend.id} + + {legend.data.value} + {legend.data.formattedValue}{legend.label}
+
+
+ ) +} + +export const CustomLegend: Story = { + render: () => , +} diff --git a/storybook/stories/pie/PieCanvas.stories.tsx b/storybook/stories/pie/PieCanvas.stories.tsx index b070c1cbe..61c075a1b 100644 --- a/storybook/stories/pie/PieCanvas.stories.tsx +++ b/storybook/stories/pie/PieCanvas.stories.tsx @@ -1,7 +1,7 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import type { Meta, StoryObj } from '@storybook/react' import { generateProgrammingLanguageStats } from '@nivo/generators' -import { PieCanvas } from '@nivo/pie' +import { LegendDatum, PieCanvas } from '@nivo/pie' import { nivoTheme } from '../nivo-theme' const meta: Meta = { @@ -161,3 +161,71 @@ const ControlledPies = () => { export const ControlledActiveId: Story = { render: () => , } + +const PieWithCustomLegend = () => { + const [customLegends, setCustomLegends] = useState[]>([]) + + const valueFormat = useCallback( + (value: number) => + `${Number(value).toLocaleString('ru-RU', { + minimumFractionDigits: 2, + })} ₽`, + [] + ) + + return ( +
+ +
+ + + + + + + + + + + + {customLegends.map(legend => { + return ( + + + + + + + + ) + })} + +
ColorIDValueFormatted ValueLabel
+ + + {legend.id} + + {legend.data.value} + {legend.data.formattedValue}{legend.label}
+
+
+ ) +} + +export const CustomLegend: Story = { + render: () => , +} diff --git a/storybook/stories/waffle/Waffle.stories.tsx b/storybook/stories/waffle/Waffle.stories.tsx index caefcfab1..89c4b973a 100644 --- a/storybook/stories/waffle/Waffle.stories.tsx +++ b/storybook/stories/waffle/Waffle.stories.tsx @@ -70,7 +70,7 @@ export const CustomLegend: Story = { render: args => { const [legends, setLegends] = useState[]>([]) - const formatValue = useCallback((value: number) => `${value} peolpe`, []) + const formatValue = useCallback((value: number) => `${value} people`, []) return (
diff --git a/website/src/data/components/pie/meta.yml b/website/src/data/components/pie/meta.yml index 6a687f413..e0fb5b1cb 100644 --- a/website/src/data/components/pie/meta.yml +++ b/website/src/data/components/pie/meta.yml @@ -19,6 +19,8 @@ Pie: link: pie--custom-arc-label-component - label: Sync activeId between two pies link: pie--controlled-active-id + - label: Implementing a custom legend + link: pie--custom-legend description: | Generates a pie chart from an array of data, each datum must have an id and a value property. @@ -46,6 +48,8 @@ PieCanvas: link: piecanvas--using-colors-from-data - label: Sync activeId between two pies link: piecanvas--controlled-active-id + - label: Implementing a custom legend + link: piecanvas--custom-legend description: | A variation around the [Pie](self:/pie) component. Well suited for large data sets as it does not impact DOM tree depth, however you'll diff --git a/website/src/data/components/pie/props.ts b/website/src/data/components/pie/props.ts index 3f4a726cf..79dc2a929 100644 --- a/website/src/data/components/pie/props.ts +++ b/website/src/data/components/pie/props.ts @@ -642,6 +642,28 @@ const props: ChartProperty[] = [ })), }, }, + { + key: 'forwardLegendData', + group: 'Legends', + type: '(data: LegendDatum[]) => void', + required: false, + flavors: ['svg', 'canvas'], + help: 'Can be used to get the computed legend data.', + description: ` + This property allows you to implement custom + legends, bypassing the limitations of SVG/Canvas. + + For example you could have a state in the parent component, + and then pass the setter. + + Please be very careful when using this property though, + you could end up with an infinite loop if the properties + defining the data don't have a stable reference. + + For example, using a non static/memoized function for \`valueFormat\` + would lead to such issue. + `, + }, { key: 'legends', flavors: ['svg', 'canvas'],