Skip to content

Commit

Permalink
feat(pie): add ability to toggle serie via legend item (#1582)
Browse files Browse the repository at this point in the history
  • Loading branch information
wyze committed Jun 11, 2021
1 parent 9c69a5f commit 23059e0
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 72 deletions.
5 changes: 4 additions & 1 deletion packages/pie/src/Pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,14 @@ const InnerPie = <RawDatum,>({

const {
dataWithArc,
legendData,
arcGenerator,
centerX,
centerY,
radius,
innerRadius,
setActiveId,
toggleSerie,
} = usePieFromBox<RawDatum>({
data: normalizedData,
width: innerWidth,
Expand Down Expand Up @@ -189,8 +191,9 @@ const InnerPie = <RawDatum,>({
key="legends"
width={innerWidth}
height={innerHeight}
dataWithArc={dataWithArc}
data={legendData}
legends={legends}
toggleSerie={toggleSerie}
/>
)
}
Expand Down
11 changes: 7 additions & 4 deletions packages/pie/src/PieLegends.tsx
Original file line number Diff line number Diff line change
@@ -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<RawDatum> {
width: number
height: number
legends: CompletePieSvgProps<RawDatum>['legends']
dataWithArc: ComputedDatum<RawDatum>[]
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
toggleSerie: (id: DatumId) => void
}

const PieLegends = <RawDatum,>({
width,
height,
legends,
dataWithArc,
data,
toggleSerie,
}: PieLegendsProps<RawDatum>) => {
return (
<>
Expand All @@ -23,7 +25,8 @@ const PieLegends = <RawDatum,>({
{...legend}
containerWidth={width}
containerHeight={height}
data={legend.data ?? dataWithArc}
data={legend.data ?? data}
toggleSerie={legend.toggleSerie ? toggleSerie : undefined}
/>
))}
</>
Expand Down
123 changes: 74 additions & 49 deletions packages/pie/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -48,6 +48,7 @@ export const useNormalizedData = <RawDatum extends MayHaveLabel>({
const normalizedDatum: Omit<ComputedDatum<RawDatum>, 'arc' | 'color' | 'fill'> = {
id: datumId,
label: datum.label ?? datumId,
hidden: false,
value: datumValue,
formattedValue: formatValue(datumValue),
data: datum,
Expand Down Expand Up @@ -76,6 +77,7 @@ export const usePieArcs = <RawDatum>({
activeId,
activeInnerRadiusOffset,
activeOuterRadiusOffset,
hiddenIds,
}: {
data: Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>[]
// in degrees
Expand All @@ -91,7 +93,11 @@ export const usePieArcs = <RawDatum>({
activeId: null | DatumId
activeInnerRadiusOffset: number
activeOuterRadiusOffset: number
}): Omit<ComputedDatum<RawDatum>, 'fill'>[] => {
hiddenIds: DatumId[]
}): {
dataWithArc: Omit<ComputedDatum<RawDatum>, 'fill'>[]
legendData: Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>[]
} => {
const pie = useMemo(() => {
const innerPie = d3Pie<Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>>()
.value(d => d.value)
Expand All @@ -106,52 +112,54 @@ export const usePieArcs = <RawDatum>({
return innerPie
}, [startAngle, endAngle, padAngle, sortByValue])

return useMemo(
() =>
pie(data).map(
(
arc: Omit<
PieArc,
'angle' | 'angleDeg' | 'innerRadius' | 'outerRadius' | 'thickness'
> & {
data: Omit<ComputedDatum<RawDatum>, '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<ComputedDatum<RawDatum>, '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,
])
}

/**
Expand Down Expand Up @@ -184,7 +192,8 @@ export const usePie = <RawDatum>({
innerRadius: number
}) => {
const [activeId, setActiveId] = useState<DatumId | null>(null)
const dataWithArc = usePieArcs({
const [hiddenIds, setHiddenIds] = useState<DatumId[]>([])
const pieArcs = usePieArcs({
data,
startAngle,
endAngle,
Expand All @@ -195,11 +204,18 @@ export const usePie = <RawDatum>({
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 }
}

/**
Expand Down Expand Up @@ -240,6 +256,7 @@ export const usePieFromBox = <RawDatum>({
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
}) => {
const [activeId, setActiveId] = useState<string | number | null>(null)
const [hiddenIds, setHiddenIds] = useState<DatumId[]>([])
const computedProps = useMemo(() => {
let radius = Math.min(width, height) / 2
let innerRadius = radius * Math.min(innerRadiusRatio, 1)
Expand Down Expand Up @@ -288,7 +305,7 @@ export const usePieFromBox = <RawDatum>({
}
}, [width, height, innerRadiusRatio, startAngle, endAngle, fit, cornerRadius])

const dataWithArc = usePieArcs({
const pieArcs = usePieArcs({
data,
startAngle,
endAngle,
Expand All @@ -299,17 +316,25 @@ export const usePieFromBox = <RawDatum>({
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,
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/pie/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface ComputedDatum<RawDatum> {
// contains the raw datum as passed to the chart
data: RawDatum
arc: PieArc
hidden: boolean
}

export interface DataProps<RawDatum> {
Expand Down
30 changes: 13 additions & 17 deletions packages/pie/stories/pie.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down Expand Up @@ -75,11 +76,6 @@ stories.add('custom arc link label', () => (
arcLinkLabelsColor={{
from: 'color',
}}
radialLabelsLinkStrokeWidth={3}
radialLabelsTextColor={{
from: 'color',
modifiers: [['darker', 1.2]],
}}
enableArcLabels={false}
/>
))
Expand All @@ -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 }}
\`\`\`
Expand All @@ -123,7 +119,7 @@ const CenteredMetric = ({ dataWithArc, centerX, centerY }) => {
dominantBaseline="central"
style={{
fontSize: '52px',
fontWeight: '600',
fontWeight: 600,
}}
>
{total}
Expand All @@ -135,8 +131,8 @@ stories.add('adding a metric in the center using a custom layer', () => (
<Pie
{...commonProperties}
innerRadius={0.8}
enableSliceLabels={false}
radialLabel={d => `${d.id} (${d.formattedValue})`}
enableArcLabels={false}
arcLinkLabel={d => `${d.id} (${d.formattedValue})`}
activeInnerRadiusOffset={commonProperties.activeOuterRadiusOffset}
layers={['arcs', 'arcLabels', 'arcLinkLabels', 'legends', CenteredMetric]}
/>
Expand All @@ -145,7 +141,7 @@ stories.add('adding a metric in the center using a custom layer', () => (
stories.add('formatted values', () => (
<Pie
{...commonProperties}
sliceLabelsRadiusOffset={0.7}
arcLabelsRadiusOffset={0.7}
valueFormat={value =>
`${Number(value).toLocaleString('ru-RU', {
minimumFractionDigits: 2,
Expand Down
Loading

0 comments on commit 23059e0

Please sign in to comment.