Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pie: Add the ability to programmatically control the activeId #2465

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion packages/pie/src/Pie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -74,10 +74,15 @@ const InnerPie = <RawDatum extends MayHaveLabel>({
onMouseMove,
onMouseLeave,
tooltip = defaultProps.tooltip,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,

transitionMode = defaultProps.transitionMode,

legends = defaultProps.legends,
forwardLegendData,

role = defaultProps.role,
}: PieSvgProps<RawDatum>) => {
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
Expand Down Expand Up @@ -117,6 +122,10 @@ const InnerPie = <RawDatum extends MayHaveLabel>({
cornerRadius,
activeInnerRadiusOffset,
activeOuterRadiusOffset,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
forwardLegendData,
})

const boundDefs = bindDefs(defs, dataWithArc, fill)
Expand Down
11 changes: 10 additions & 1 deletion packages/pie/src/PieCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const InnerPieCanvas = <RawDatum extends MayHaveLabel>({
width,
height,
margin: partialMargin,
pixelRatio = 1,
pixelRatio = defaultProps.pixelRatio,

colors = defaultProps.colors,

Expand Down Expand Up @@ -67,8 +67,12 @@ const InnerPieCanvas = <RawDatum extends MayHaveLabel>({
onClick,
onMouseMove,
tooltip = defaultProps.tooltip,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,

legends = defaultProps.legends,
forwardLegendData,
}: PieCanvasProps<RawDatum>) => {
const canvasEl = useRef<HTMLCanvasElement | null>(null)
const theme = useTheme()
Expand Down Expand Up @@ -101,6 +105,10 @@ const InnerPieCanvas = <RawDatum extends MayHaveLabel>({
cornerRadius,
activeInnerRadiusOffset,
activeOuterRadiusOffset,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
forwardLegendData,
})

const getBorderColor = useInheritedColor<ComputedDatum<RawDatum>>(borderColor, theme)
Expand Down Expand Up @@ -199,6 +207,7 @@ const InnerPieCanvas = <RawDatum extends MayHaveLabel>({
centerY,
arcGenerator,
dataWithArc,
borderWidth,
getBorderColor,
enableArcLabels,
arcLabels,
Expand Down
8 changes: 3 additions & 5 deletions packages/pie/src/PieLegends.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { BoxLegendSvg } from '@nivo/legends'
import { CompletePieSvgProps, ComputedDatum, DatumId } from './types'
import { CompletePieSvgProps, DatumId, LegendDatum } from './types'

interface PieLegendsProps<RawDatum> {
width: number
height: number
legends: CompletePieSvgProps<RawDatum>['legends']
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
data: LegendDatum<RawDatum>[]
toggleSerie: (id: DatumId) => void
}

const PieLegends = <RawDatum,>({
export const PieLegends = <RawDatum,>({
width,
height,
legends,
Expand All @@ -31,5 +31,3 @@ const PieLegends = <RawDatum,>({
</>
)
}

export default PieLegends
108 changes: 98 additions & 10 deletions packages/pie/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -16,6 +16,8 @@ import {
DatumId,
PieArc,
PieCustomLayerProps,
LegendDatum,
CommonPieProps,
} from './types'

/**
Expand Down Expand Up @@ -81,6 +83,7 @@ export const usePieArcs = <RawDatum>({
activeInnerRadiusOffset,
activeOuterRadiusOffset,
hiddenIds,
forwardLegendData,
}: {
data: Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>[]
// in degrees
Expand All @@ -97,9 +100,10 @@ export const usePieArcs = <RawDatum>({
activeInnerRadiusOffset: number
activeOuterRadiusOffset: number
hiddenIds: DatumId[]
forwardLegendData?: CommonPieProps<RawDatum>['forwardLegendData']
}): {
dataWithArc: Omit<ComputedDatum<RawDatum>, 'fill'>[]
legendData: Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>[]
legendData: LegendDatum<RawDatum>[]
} => {
const pie = useMemo(() => {
const innerPie = d3Pie<Omit<ComputedDatum<RawDatum>, 'arc' | 'fill'>>()
Expand All @@ -115,7 +119,7 @@ export const usePieArcs = <RawDatum>({
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(
(
Expand Down Expand Up @@ -150,7 +154,13 @@ export const usePieArcs = <RawDatum>({
}
}
)
const legendData = data.map(item => ({ ...item, hidden: hiddenIds.includes(item.id) }))
const legendData: LegendDatum<RawDatum>[] = data.map(item => ({
id: item.id,
label: item.label,
color: item.color,
hidden: hiddenIds.includes(item.id),
data: item,
}))

return { dataWithArc, legendData }
}, [
Expand All @@ -163,6 +173,55 @@ export const usePieArcs = <RawDatum>({
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
}

/**
* Encapsulate the logic for defining/reading the active arc ID,
* which can be either controlled (handled externally), or uncontrolled
* (handled internally), we can optionally define a default value when
* it's uncontrolled.
*/
const useActiveId = ({
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId = null,
}: {
activeId?: DatumId | null
onActiveIdChange?: (id: DatumId | null) => void
defaultActiveId?: DatumId | null
}) => {
const isControlled = typeof activeIdFromProps != 'undefined'

const [internalActiveId, setInternalActiveId] = useState<DatumId | null>(
!isControlled ? defaultActiveId : null
)

const activeId = isControlled ? activeIdFromProps : internalActiveId

const setActiveId = useCallback(
(id: DatumId | null) => {
if (onActiveIdChange) {
onActiveIdChange(id)
}

if (!isControlled) {
setInternalActiveId(id)
}
},
[isControlled, onActiveIdChange, setInternalActiveId]
)

return { activeId, setActiveId }
}

/**
Expand All @@ -180,6 +239,10 @@ export const usePie = <RawDatum>({
cornerRadius = defaultProps.cornerRadius,
activeInnerRadiusOffset = defaultProps.activeInnerRadiusOffset,
activeOuterRadiusOffset = defaultProps.activeOuterRadiusOffset,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
forwardLegendData,
}: Pick<
Partial<CompletePieSvgProps<RawDatum>>,
| 'startAngle'
Expand All @@ -189,12 +252,21 @@ export const usePie = <RawDatum>({
| 'cornerRadius'
| 'activeInnerRadiusOffset'
| 'activeOuterRadiusOffset'
| 'activeId'
| 'onActiveIdChange'
| 'defaultActiveId'
| 'forwardLegendData'
> & {
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
radius: number
innerRadius: number
}) => {
const [activeId, setActiveId] = useState<DatumId | null>(null)
const { activeId, setActiveId } = useActiveId({
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
})

const [hiddenIds, setHiddenIds] = useState<DatumId[]>([])
const pieArcs = usePieArcs({
data,
Expand All @@ -208,6 +280,7 @@ export const usePie = <RawDatum>({
activeInnerRadiusOffset,
activeOuterRadiusOffset,
hiddenIds,
forwardLegendData,
})

const toggleSerie = useCallback((id: DatumId) => {
Expand Down Expand Up @@ -242,6 +315,10 @@ export const usePieFromBox = <RawDatum>({
fit = defaultProps.fit,
activeInnerRadiusOffset = defaultProps.activeInnerRadiusOffset,
activeOuterRadiusOffset = defaultProps.activeOuterRadiusOffset,
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
forwardLegendData,
}: Pick<
CompletePieSvgProps<RawDatum>,
| 'width'
Expand All @@ -255,10 +332,19 @@ export const usePieFromBox = <RawDatum>({
| 'fit'
| 'activeInnerRadiusOffset'
| 'activeOuterRadiusOffset'
> & {
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
}) => {
const [activeId, setActiveId] = useState<string | number | null>(null)
> &
Pick<
Partial<CompletePieSvgProps<RawDatum>>,
'activeId' | 'onActiveIdChange' | 'defaultActiveId' | 'forwardLegendData'
> & {
data: Omit<ComputedDatum<RawDatum>, 'arc'>[]
}) => {
const { activeId, setActiveId } = useActiveId({
activeId: activeIdFromProps,
onActiveIdChange,
defaultActiveId,
})

const [hiddenIds, setHiddenIds] = useState<DatumId[]>([])
const computedProps = useMemo(() => {
let radius = Math.min(width, height) / 2
Expand Down Expand Up @@ -306,7 +392,7 @@ export const usePieFromBox = <RawDatum>({
innerRadius,
debug: boundingBox,
}
}, [width, height, innerRadiusRatio, startAngle, endAngle, fit, cornerRadius])
}, [width, height, innerRadiusRatio, startAngle, endAngle, fit])

const pieArcs = usePieArcs({
data,
Expand All @@ -320,6 +406,7 @@ export const usePieFromBox = <RawDatum>({
activeInnerRadiusOffset,
activeOuterRadiusOffset,
hiddenIds,
forwardLegendData,
})

const toggleSerie = useCallback((id: DatumId) => {
Expand All @@ -335,6 +422,7 @@ export const usePieFromBox = <RawDatum>({

return {
arcGenerator,
activeId,
setActiveId,
toggleSerie,
...pieArcs,
Expand Down
14 changes: 13 additions & 1 deletion packages/pie/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export interface DefaultRawDatum {
value: number
}

export interface MayHaveLabel {
export interface MayHaveLabel extends Object {
label?: string | number
}

Expand Down Expand Up @@ -113,8 +113,12 @@ export type CommonPieProps<RawDatum> = {
// interactivity
isInteractive: boolean
tooltip: React.FC<PieTooltipProps<RawDatum>>
activeId: DatumId | null
onActiveIdChange: (id: DatumId | null) => void
defaultActiveId: DatumId | null

legends: readonly LegendProps[]
forwardLegendData: (data: LegendDatum<RawDatum>[]) => void

role: string
renderWrapper: boolean
Expand All @@ -132,6 +136,14 @@ export type PieSvgCustomComponents<RawDatum> = {
arcLinkLabelComponent?: ArcLinkLabelsProps<ComputedDatum<RawDatum>>['component']
}

export interface LegendDatum<RawDatum> {
id: ComputedDatum<RawDatum>['id']
label: ComputedDatum<RawDatum>['label']
color: string
hidden: boolean
data: Omit<ComputedDatum<RawDatum>, 'fill' | 'arc'>
}

export type PieSvgProps<RawDatum> = DataProps<RawDatum> &
Dimensions &
Partial<CommonPieProps<RawDatum>> &
Expand Down