From db13a050151852467e843ddda5317c0a8c3758d9 Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Wed, 9 Jun 2021 09:11:00 -0500 Subject: [PATCH] feat(pie): add ability to toggle serie via legend item --- packages/pie/src/Pie.tsx | 5 +- packages/pie/src/PieLegends.tsx | 11 ++- packages/pie/src/hooks.ts | 123 ++++++++++++++++----------- packages/pie/src/types.ts | 1 + packages/pie/stories/pie.stories.tsx | 30 +++---- packages/pie/tests/Pie.test.tsx | 35 +++++++- 6 files changed, 133 insertions(+), 72 deletions(-) diff --git a/packages/pie/src/Pie.tsx b/packages/pie/src/Pie.tsx index 787f204209..8de8f4a24c 100644 --- a/packages/pie/src/Pie.tsx +++ b/packages/pie/src/Pie.tsx @@ -96,12 +96,14 @@ const InnerPie = ({ const { dataWithArc, + legendData, arcGenerator, centerX, centerY, radius, innerRadius, setActiveId, + toggleSerie, } = usePieFromBox({ data: normalizedData, width: innerWidth, @@ -189,8 +191,9 @@ const InnerPie = ({ key="legends" width={innerWidth} height={innerHeight} - dataWithArc={dataWithArc} + data={legendData} legends={legends} + toggleSerie={toggleSerie} /> ) } diff --git a/packages/pie/src/PieLegends.tsx b/packages/pie/src/PieLegends.tsx index 87d46f733b..87a9ad1655 100644 --- a/packages/pie/src/PieLegends.tsx +++ b/packages/pie/src/PieLegends.tsx @@ -1,19 +1,21 @@ import React from 'react' import { BoxLegendSvg } from '@nivo/legends' -import { CompletePieSvgProps, ComputedDatum } from './types' +import { CompletePieSvgProps, ComputedDatum, DatumId } from './types' interface PieLegendsProps { width: number height: number legends: CompletePieSvgProps['legends'] - dataWithArc: ComputedDatum[] + data: Omit, 'arc'>[] + toggleSerie: (id: DatumId) => void } const PieLegends = ({ width, height, legends, - dataWithArc, + data, + toggleSerie, }: PieLegendsProps) => { return ( <> @@ -23,7 +25,8 @@ const PieLegends = ({ {...legend} containerWidth={width} containerHeight={height} - data={legend.data ?? dataWithArc} + data={legend.data ?? data} + toggleSerie={legend.toggleSerie ? toggleSerie : undefined} /> ))} diff --git a/packages/pie/src/hooks.ts b/packages/pie/src/hooks.ts index 65d02d3583..0320e89eae 100644 --- a/packages/pie/src/hooks.ts +++ b/packages/pie/src/hooks.ts @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { pie as d3Pie } from 'd3-shape' import { ArcGenerator, useArcGenerator, computeArcBoundingBox } from '@nivo/arcs' import { @@ -48,6 +48,7 @@ export const useNormalizedData = ({ const normalizedDatum: Omit, 'arc' | 'color' | 'fill'> = { id: datumId, label: datum.label ?? datumId, + hidden: false, value: datumValue, formattedValue: formatValue(datumValue), data: datum, @@ -76,6 +77,7 @@ export const usePieArcs = ({ activeId, activeInnerRadiusOffset, activeOuterRadiusOffset, + hiddenIds, }: { data: Omit, 'arc' | 'fill'>[] // in degrees @@ -91,7 +93,11 @@ export const usePieArcs = ({ activeId: null | DatumId activeInnerRadiusOffset: number activeOuterRadiusOffset: number -}): Omit, 'fill'>[] => { + hiddenIds: DatumId[] +}): { + dataWithArc: Omit, 'fill'>[] + legendData: Omit, 'arc' | 'fill'>[] +} => { const pie = useMemo(() => { const innerPie = d3Pie, 'arc' | 'fill'>>() .value(d => d.value) @@ -106,52 +112,54 @@ export const usePieArcs = ({ return innerPie }, [startAngle, endAngle, padAngle, sortByValue]) - return useMemo( - () => - pie(data).map( - ( - arc: Omit< - PieArc, - 'angle' | 'angleDeg' | 'innerRadius' | 'outerRadius' | 'thickness' - > & { - data: Omit, 'arc' | 'fill'> - } - ) => { - const angle = Math.abs(arc.endAngle - arc.startAngle) + return useMemo(() => { + const hiddenData = data.filter(item => !hiddenIds.includes(item.id)) + const dataWithArc = pie(hiddenData).map( + ( + arc: Omit< + PieArc, + 'angle' | 'angleDeg' | 'innerRadius' | 'outerRadius' | 'thickness' + > & { + data: Omit, 'arc' | 'fill'> + } + ) => { + const angle = Math.abs(arc.endAngle - arc.startAngle) - return { - ...arc.data, - arc: { - index: arc.index, - startAngle: arc.startAngle, - endAngle: arc.endAngle, - innerRadius: - activeId === arc.data.id - ? innerRadius - activeInnerRadiusOffset - : innerRadius, - outerRadius: - activeId === arc.data.id - ? outerRadius + activeOuterRadiusOffset - : outerRadius, - thickness: outerRadius - innerRadius, - padAngle: arc.padAngle, - angle, - angleDeg: radiansToDegrees(angle), - }, - } + return { + ...arc.data, + arc: { + index: arc.index, + startAngle: arc.startAngle, + endAngle: arc.endAngle, + innerRadius: + activeId === arc.data.id + ? innerRadius - activeInnerRadiusOffset + : innerRadius, + outerRadius: + activeId === arc.data.id + ? outerRadius + activeOuterRadiusOffset + : outerRadius, + thickness: outerRadius - innerRadius, + padAngle: arc.padAngle, + angle, + angleDeg: radiansToDegrees(angle), + }, } - ), + } + ) + const legendData = data.map(item => ({ ...item, hidden: hiddenIds.includes(item.id) })) - [ - pie, - data, - innerRadius, - outerRadius, - activeId, - activeInnerRadiusOffset, - activeInnerRadiusOffset, - ] - ) + return { dataWithArc, legendData } + }, [ + pie, + data, + hiddenIds, + activeId, + innerRadius, + activeInnerRadiusOffset, + outerRadius, + activeOuterRadiusOffset, + ]) } /** @@ -184,7 +192,8 @@ export const usePie = ({ innerRadius: number }) => { const [activeId, setActiveId] = useState(null) - const dataWithArc = usePieArcs({ + const [hiddenIds, setHiddenIds] = useState([]) + const pieArcs = usePieArcs({ data, startAngle, endAngle, @@ -195,11 +204,18 @@ export const usePie = ({ activeId, activeInnerRadiusOffset, activeOuterRadiusOffset, + hiddenIds, }) + const toggleSerie = useCallback((id: DatumId) => { + setHiddenIds(state => + state.indexOf(id) > -1 ? state.filter(item => item !== id) : [...state, id] + ) + }, []) + const arcGenerator = useArcGenerator({ cornerRadius, padAngle: degreesToRadians(padAngle) }) - return { dataWithArc, arcGenerator, setActiveId } + return { ...pieArcs, arcGenerator, setActiveId, toggleSerie } } /** @@ -240,6 +256,7 @@ export const usePieFromBox = ({ data: Omit, 'arc'>[] }) => { const [activeId, setActiveId] = useState(null) + const [hiddenIds, setHiddenIds] = useState([]) const computedProps = useMemo(() => { let radius = Math.min(width, height) / 2 let innerRadius = radius * Math.min(innerRadiusRatio, 1) @@ -288,7 +305,7 @@ export const usePieFromBox = ({ } }, [width, height, innerRadiusRatio, startAngle, endAngle, fit, cornerRadius]) - const dataWithArc = usePieArcs({ + const pieArcs = usePieArcs({ data, startAngle, endAngle, @@ -299,17 +316,25 @@ export const usePieFromBox = ({ activeId, activeInnerRadiusOffset, activeOuterRadiusOffset, + hiddenIds, }) + const toggleSerie = useCallback((id: DatumId) => { + setHiddenIds(state => + state.indexOf(id) > -1 ? state.filter(item => item !== id) : [...state, id] + ) + }, []) + const arcGenerator = useArcGenerator({ cornerRadius, padAngle: degreesToRadians(padAngle), }) return { - dataWithArc, arcGenerator, setActiveId, + toggleSerie, + ...pieArcs, ...computedProps, } } diff --git a/packages/pie/src/types.ts b/packages/pie/src/types.ts index a38d979cde..eb5657ab1f 100644 --- a/packages/pie/src/types.ts +++ b/packages/pie/src/types.ts @@ -50,6 +50,7 @@ export interface ComputedDatum { // contains the raw datum as passed to the chart data: RawDatum arc: PieArc + hidden: boolean } export interface DataProps { diff --git a/packages/pie/stories/pie.stories.tsx b/packages/pie/stories/pie.stories.tsx index e8e2d478dc..2ab13fe114 100644 --- a/packages/pie/stories/pie.stories.tsx +++ b/packages/pie/stories/pie.stories.tsx @@ -19,17 +19,18 @@ const commonProperties = { const legends = [ { - anchor: 'bottom', - direction: 'row', + anchor: 'bottom' as const, + direction: 'row' as const, + toggleSerie: true, translateY: 56, itemWidth: 100, itemHeight: 18, itemTextColor: '#999', symbolSize: 18, - symbolShape: 'circle', + symbolShape: 'circle' as const, effects: [ { - on: 'hover', + on: 'hover' as const, style: { itemTextColor: '#000', }, @@ -75,11 +76,6 @@ stories.add('custom arc link label', () => ( arcLinkLabelsColor={{ from: 'color', }} - radialLabelsLinkStrokeWidth={3} - radialLabelsTextColor={{ - from: 'color', - modifiers: [['darker', 1.2]], - }} enableArcLabels={false} /> )) @@ -92,15 +88,15 @@ stories.add( text: ` It is possible to use colors coming from the provided dataset instead of using a color scheme, to do so, you should pass: - + \`\`\` colors={{ datum: 'data.color' }} \`\`\` - + given that each data point you pass has a \`color\` property. - + It's also possible to pass a function if you want to handle more advanced computation: - + \`\`\` colors={(datum) => datum.color }} \`\`\` @@ -123,7 +119,7 @@ const CenteredMetric = ({ dataWithArc, centerX, centerY }) => { dominantBaseline="central" style={{ fontSize: '52px', - fontWeight: '600', + fontWeight: 600, }} > {total} @@ -135,8 +131,8 @@ stories.add('adding a metric in the center using a custom layer', () => ( `${d.id} (${d.formattedValue})`} + enableArcLabels={false} + arcLinkLabel={d => `${d.id} (${d.formattedValue})`} activeInnerRadiusOffset={commonProperties.activeOuterRadiusOffset} layers={['arcs', 'arcLabels', 'arcLinkLabels', 'legends', CenteredMetric]} /> @@ -145,7 +141,7 @@ stories.add('adding a metric in the center using a custom layer', () => ( stories.add('formatted values', () => ( `${Number(value).toLocaleString('ru-RU', { minimumFractionDigits: 2, diff --git a/packages/pie/tests/Pie.test.tsx b/packages/pie/tests/Pie.test.tsx index 8e2efd0b57..4cfb0fd8fa 100644 --- a/packages/pie/tests/Pie.test.tsx +++ b/packages/pie/tests/Pie.test.tsx @@ -1,6 +1,5 @@ import React from 'react' import { mount } from 'enzyme' -import { animated } from '@react-spring/web' import { radiansToDegrees } from '@nivo/core' import { Pie } from '../src/index' @@ -581,6 +580,40 @@ describe('Pie', () => { ) }) }) + + it('should toggle serie via legend', done => { + const wrapper = mount( + + ) + + const legendItems = wrapper.find('LegendSvgItem') + const shapes = wrapper.find('ArcShape') + + expect(shapes.at(0).prop('style').opacity).toMatchInlineSnapshot(`1`) + + legendItems.at(0).find('rect').at(0).simulate('click') + + // TODO: Figure out why pie isn't respecting animate property + setTimeout(() => { + expect(shapes.at(0).prop('style').opacity).toMatchInlineSnapshot(`0`) + + done() + }, 1000) + }) }) describe('interactivity', () => {