diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index 2bd798fc9..8763460f6 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -132,6 +132,7 @@ declare module '@nivo/core' { > export function useTheme(): CompleteTheme + export function usePartialTheme(theme?: Theme): CompleteTheme export type MotionProps = Partial<{ animate: boolean @@ -159,7 +160,7 @@ declare module '@nivo/core' { id: string [key: string]: any }[] - fill?: { id: string; match: object | SvgFillMatcher | '*' }[] + fill?: { id: string; match: Record | SvgFillMatcher | '*' }[] } export interface CartesianMarkerProps { @@ -276,6 +277,12 @@ declare module '@nivo/core' { export function degreesToRadians(degrees: number): number export function radiansToDegrees(radians: number): number + type Accessor = T extends string ? U[T] : never + + export function getAccessorFor( + directive: string | number + ): (datum: Datum) => Value + export function useDimensions( width: number, height: number, diff --git a/packages/sunburst/package.json b/packages/sunburst/package.json index 2f0ed7372..ffe8116d0 100644 --- a/packages/sunburst/package.json +++ b/packages/sunburst/package.json @@ -32,8 +32,7 @@ "@nivo/tooltip": "0.66.0", "d3-hierarchy": "^1.1.8", "d3-shape": "^1.3.5", - "lodash": "^4.17.11", - "recompose": "^0.30.0" + "lodash": "^4.17.11" }, "devDependencies": { "@nivo/core": "0.66.0", diff --git a/packages/sunburst/src/ResponsiveSunburst.tsx b/packages/sunburst/src/ResponsiveSunburst.tsx index 435b22ccd..c6204eb05 100644 --- a/packages/sunburst/src/ResponsiveSunburst.tsx +++ b/packages/sunburst/src/ResponsiveSunburst.tsx @@ -1,12 +1,14 @@ import React from 'react' // @ts-ignore import { ResponsiveWrapper } from '@nivo/core' -import Sunburst from './Sunburst' +import { Sunburst } from './Sunburst' import { SunburstSvgProps } from './types' -export const ResponsiveSunburst = (props: Omit) => ( +export const ResponsiveSunburst = >( + props: Omit, 'width' | 'height'> +) => ( - {({ width, height }: Required>) => ( + {({ width, height }: Required, 'width' | 'height'>>) => ( )} diff --git a/packages/sunburst/src/Sunburst.tsx b/packages/sunburst/src/Sunburst.tsx index 6ffebd631..52ea45d7a 100644 --- a/packages/sunburst/src/Sunburst.tsx +++ b/packages/sunburst/src/Sunburst.tsx @@ -1,24 +1,13 @@ -import React from 'react' +import React, { useMemo } from 'react' import sortBy from 'lodash/sortBy' import cloneDeep from 'lodash/cloneDeep' -// @ts-ignore -import compose from 'recompose/compose' -// @ts-ignore -import defaultProps from 'recompose/defaultProps' -// @ts-ignore -import withPropsOnChange from 'recompose/withPropsOnChange' -// @ts-ignore -import withProps from 'recompose/withProps' -// @ts-ignore -import pure from 'recompose/pure' -import { partition as Partition, hierarchy } from 'd3-hierarchy' +import { partition as d3Partition, hierarchy } from 'd3-hierarchy' import { arc } from 'd3-shape' import { // @ts-ignore withTheme, // @ts-ignore withDimensions, - // @ts-ignore getAccessorFor, // @ts-ignore getLabelGenerator, @@ -26,13 +15,15 @@ import { Container, // @ts-ignore SvgWrapper, + useDimensions, + usePartialTheme, } from '@nivo/core' // @ts-ignore -import { getOrdinalColorScale, getInheritedColorGenerator } from '@nivo/colors' -import SunburstLabels from './SunburstLabels' -import SunburstArc from './SunburstArc' -import { defaultProps as defaultSunburstProps } from './props' -import { SunburstSvgProps, SunburstNode, TooltipHandlers } from './types' +import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors' +import { SunburstLabels } from './SunburstLabels' +import { SunburstArc } from './SunburstArc' +import { defaultProps } from './props' +import { SunburstSvgProps, SunburstNode, Data } from './types' const getAncestor = (node: any): any => { if (node.depth === 1) return node @@ -40,167 +31,137 @@ const getAncestor = (node: any): any => { return node } -const Sunburst = ({ - nodes, +export const Sunburst = >(props: SunburstSvgProps) => { + const { + data: _data, + identity, + value, - margin, - centerX, - centerY, - outerWidth, - outerHeight, + colors, + childColor, - arcGenerator, + margin: partialMargin, + width, + height, - borderWidth, - borderColor, + cornerRadius, + // arcGenerator, - // slices labels - enableSlicesLabels, - getSliceLabel, - slicesLabelsSkipAngle, - slicesLabelsTextColor, + borderWidth, + borderColor, - // theming - theme, + // slices labels + enableSlicesLabels, + sliceLabel, + slicesLabelsSkipAngle, + slicesLabelsTextColor, - role, + // theming + theme: _theme, - // interactivity - isInteractive, - tooltipFormat, - tooltip, + role, - // event handlers - onClick, - onMouseEnter, - onMouseLeave, -}: SunburstSvgProps & Required) => { - return ( - - {({ showTooltip, hideTooltip }: TooltipHandlers) => ( - - - {nodes - .filter(node => node.depth > 0) - .map((node, i) => ( - - ))} - {enableSlicesLabels && ( - - )} - - - )} - - ) -} + // interactivity + isInteractive, + tooltipFormat, + tooltip, + + // event handlers + onClick, + onMouseEnter, + onMouseLeave, + } = { ...defaultProps, ...props } + + const { margin } = useDimensions(width, height, partialMargin) + const theme = usePartialTheme(_theme) -const enhance = compose( - defaultProps(defaultSunburstProps), - withTheme(), - withDimensions(), - withPropsOnChange(['colors'], ({ colors }: Required) => ({ - getColor: getOrdinalColorScale(colors, 'id'), - })), - withProps(({ width, height }: Record) => { + const { centerX, centerY, radius } = useMemo(() => { const radius = Math.min(width, height) / 2 - const partition = Partition().size([2 * Math.PI, radius * radius]) - - return { radius, partition, centerX: width / 2, centerY: height / 2 } - }), - withPropsOnChange(['cornerRadius'], ({ cornerRadius }: { cornerRadius: number }) => ({ - arcGenerator: arc() - .startAngle(d => d.x0) - .endAngle(d => d.x1) - .innerRadius(d => Math.sqrt(d.y0)) - .outerRadius(d => Math.sqrt(d.y1)) - .cornerRadius(cornerRadius), - })), - withPropsOnChange(['identity'], ({ identity }: SunburstSvgProps) => ({ - getIdentity: getAccessorFor(identity), - })), - withPropsOnChange(['value'], ({ value }: SunburstSvgProps) => ({ - getValue: getAccessorFor(value), - })), - withPropsOnChange(['data', 'getValue'], ({ data, getValue }: Required) => ({ - data: hierarchy(data).sum(getValue as any), - })), - withPropsOnChange(['childColor', 'theme'], ({ childColor, theme }: SunburstSvgProps) => ({ - getChildColor: getInheritedColorGenerator(childColor, theme), - })), - withPropsOnChange( - ['data', 'partition', 'getIdentity', 'getChildColor'], - ({ - data, - partition, - getIdentity, - getColor, - childColor, - getChildColor, - }: Required) => { - const total = (data as any).value - - const nodes = sortBy(partition(cloneDeep(data)).descendants(), 'depth') - nodes.forEach(node => { - const ancestor = getAncestor(node).data + return { radius, centerX: width / 2, centerY: height / 2 } + }, [height, width]) + + const arcGenerator = useMemo( + () => + arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .innerRadius(d => Math.sqrt(d.y0)) + .outerRadius(d => Math.sqrt(d.y1)) + .cornerRadius(cornerRadius), + [cornerRadius] + ) + + const getColor = useOrdinalColorScale(colors, 'id') + const getChildColor = useInheritedColor(childColor, theme) + + const getSliceLabel = useMemo(() => getLabelGenerator(sliceLabel), [sliceLabel]) + const getIdentity = useMemo(() => getAccessorFor(identity), [identity]) + const getValue = useMemo(() => getAccessorFor(value), [value]) - delete node.children - delete node.data.children + const nodes = useMemo(() => { + const partition = d3Partition>().size([2 * Math.PI, radius * radius]) + const data = hierarchy(_data).sum(getValue) + const total = data.value ?? 0 - Object.assign(node.data, { - id: getIdentity(node.data), - value: node.value, - percentage: (100 * node.value) / total, + return sortBy(partition(cloneDeep(data)).descendants(), 'depth').reduce( + (acc, { children: _children, ...node }) => { + const ancestor = getAncestor(node).data + const value = node.value ?? 0 + const id = getIdentity, string>(node.data) + const data = { + id, + value, + percentage: (100 * value) / total, depth: node.depth, ancestor, - }) - - if (node.depth === 1 || childColor === 'noinherit') { - node.data.color = getColor(node.data) - } else if (node.depth > 1) { - node.data.color = getChildColor(node.parent.data) } - }) - - return { nodes } - } - ), - withPropsOnChange(['sliceLabel'], ({ sliceLabel }: SunburstSvgProps) => ({ - getSliceLabel: getLabelGenerator(sliceLabel), - })), - pure -) + const parent = acc.find( + n => node.parent && n.data.id === getIdentity(node.parent.data) + ) + const color = + node.depth === 1 || childColor === 'noinherit' + ? getColor(data) + : parent + ? getChildColor(parent.data) + : node.data.color + + return [...acc, { ...node, data: { ...data, color } }] + }, + [] as SunburstNode[] + ) + }, [_data, childColor, getChildColor, getColor, getIdentity, getValue, radius]) -const enhancedSunburst = (enhance(Sunburst as any) as unknown) as React.FC -enhancedSunburst.displayName = 'Sunburst' - -export default enhancedSunburst + return ( + + + + {nodes + .filter(node => node.depth > 0) + .map((node, i) => ( + + ))} + {enableSlicesLabels && ( + + )} + + + + ) +} diff --git a/packages/sunburst/src/SunburstArc.tsx b/packages/sunburst/src/SunburstArc.tsx index 0d6f977b6..6bf1e6663 100644 --- a/packages/sunburst/src/SunburstArc.tsx +++ b/packages/sunburst/src/SunburstArc.tsx @@ -1,37 +1,39 @@ -import React from 'react' -// @ts-ignore -import compose from 'recompose/compose' -// @ts-ignore -import withPropsOnChange from 'recompose/withPropsOnChange' -// @ts-ignore -import pure from 'recompose/pure' -import { BasicTooltip } from '@nivo/tooltip' +import React, { useMemo } from 'react' +import { BasicTooltip, useTooltip } from '@nivo/tooltip' import { SunburstArcProps } from './types' -const SunburstArc = ({ +export const SunburstArc = ({ node, - path, + arcGenerator, borderWidth, borderColor, - showTooltip, - hideTooltip, - tooltip, + tooltip: _tooltip, + tooltipFormat, onClick, onMouseEnter, onMouseLeave, -}: SunburstArcProps & { - onClick: (event: React.MouseEvent) => void -}) => { - // @ts-ignore - const handleTooltip = e => showTooltip(tooltip, e) - const handleMouseEnter = (e: React.MouseEvent) => { - onMouseEnter?.(node.data, e) - // @ts-ignore - showTooltip(tooltip, e) - } - const handleMouseLeave = (e: React.MouseEvent) => { - onMouseLeave?.(node.data, e) - hideTooltip() +}: SunburstArcProps) => { + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const path = useMemo(() => arcGenerator(node), [arcGenerator, node]) + const tooltip = useMemo( + () => ( + + ), + [_tooltip, node.data, tooltipFormat] + ) + + if (!path) { + return null } return ( @@ -40,41 +42,16 @@ const SunburstArc = ({ fill={node.data.color} stroke={borderColor} strokeWidth={borderWidth} - onMouseEnter={handleMouseEnter} - onMouseMove={handleTooltip} - onMouseLeave={handleMouseLeave} - onClick={onClick} + onMouseEnter={event => { + onMouseEnter?.(node.data, event) + showTooltipFromEvent(tooltip, event) + }} + onMouseMove={event => showTooltipFromEvent(tooltip, event)} + onMouseLeave={event => { + onMouseLeave?.(node.data, event) + hideTooltip() + }} + onClick={event => onClick?.(node.data, event)} /> ) } - -const enhance = compose( - withPropsOnChange(['node', 'arcGenerator'], ({ node, arcGenerator }: SunburstArcProps) => ({ - path: arcGenerator(node), - })), - withPropsOnChange(['node', 'onClick'], ({ node, onClick }: SunburstArcProps) => ({ - onClick: (event: React.MouseEvent) => - onClick?.(node.data, event), - })), - withPropsOnChange( - ['node', 'tooltip', 'tooltipFormat'], - ({ node, tooltip, tooltipFormat }: SunburstArcProps) => ({ - tooltip: ( - - ), - }) - ), - pure -) - -export default (enhance(SunburstArc as any) as unknown) as React.FC diff --git a/packages/sunburst/src/SunburstLabels.tsx b/packages/sunburst/src/SunburstLabels.tsx index 28ced5c66..9accf2564 100644 --- a/packages/sunburst/src/SunburstLabels.tsx +++ b/packages/sunburst/src/SunburstLabels.tsx @@ -1,58 +1,63 @@ -import React, { Component } from 'react' +import React from 'react' // @ts-ignore -import { midAngle, positionFromAngle, radiansToDegrees } from '@nivo/core' +import { midAngle, positionFromAngle, radiansToDegrees, useTheme } from '@nivo/core' +import { useInheritedColor } from '@nivo/colors' import { SunburstLabelProps } from './types' const sliceStyle = { pointerEvents: 'none', } as const -export default class SunburstLabels extends Component { - render() { - const { nodes, label, skipAngle = 0, textColor, theme } = this.props - - let centerRadius: number - - return ( - <> - {nodes - .filter(node => node.depth === 1) - .map(node => { - if (!centerRadius) { - const innerRadius = Math.sqrt(node.y0) - const outerRadius = Math.sqrt(node.y1) - centerRadius = innerRadius + (outerRadius - innerRadius) / 2 - } - - const startAngle = node.x0 - const endAngle = node.x1 - const angle = Math.abs(endAngle - startAngle) - const angleDeg = radiansToDegrees(angle) - - if (angleDeg <= skipAngle) return null - - const middleAngle = midAngle({ startAngle, endAngle }) - Math.PI / 2 - const position = positionFromAngle(middleAngle, centerRadius) - - return ( - { + const theme = useTheme() + const textColor = useInheritedColor(_textColor, theme) + + let centerRadius: number + + return ( + <> + {nodes + .filter(node => node.depth === 1) + .map(node => { + if (!centerRadius) { + const innerRadius = Math.sqrt(node.y0) + const outerRadius = Math.sqrt(node.y1) + centerRadius = innerRadius + (outerRadius - innerRadius) / 2 + } + + const startAngle = node.x0 + const endAngle = node.x1 + const angle = Math.abs(endAngle - startAngle) + const angleDeg = radiansToDegrees(angle) + + if (angleDeg <= skipAngle) return null + + const middleAngle = midAngle({ startAngle, endAngle }) - Math.PI / 2 + const position = positionFromAngle(middleAngle, centerRadius) + + return ( + + - - {label(node.data)} - - - ) - })} - - ) - } + {label(node.data)} + + + ) + })} + + ) } diff --git a/packages/sunburst/src/index.ts b/packages/sunburst/src/index.ts index 050fab2fa..bf9681965 100644 --- a/packages/sunburst/src/index.ts +++ b/packages/sunburst/src/index.ts @@ -1,4 +1,4 @@ -export { default as Sunburst } from './Sunburst' +export * from './Sunburst' export * from './ResponsiveSunburst' export * from './props' export * from './types' diff --git a/packages/sunburst/src/props.ts b/packages/sunburst/src/props.ts index 9fed32728..0ac8bcf15 100644 --- a/packages/sunburst/src/props.ts +++ b/packages/sunburst/src/props.ts @@ -1,3 +1,5 @@ +export type DefaultSunburstProps = Required + export const defaultProps = { identity: 'id', value: 'value', @@ -14,7 +16,7 @@ export const defaultProps = { // slices labels enableSlicesLabels: false, sliceLabel: 'value', - slicesLabelsTextColor: 'theme', + slicesLabelsTextColor: { theme: 'labels.text.fill' }, isInteractive: true, } diff --git a/packages/sunburst/src/types.ts b/packages/sunburst/src/types.ts index 1b1c9f194..f33dbc980 100644 --- a/packages/sunburst/src/types.ts +++ b/packages/sunburst/src/types.ts @@ -1,23 +1,24 @@ -import { OrdinalColorsInstruction, InheritedColorProp } from '@nivo/colors' -import { Theme, Dimensions, Box } from '@nivo/core' +import { Arc } from 'd3-shape' +import { HierarchyRectangularNode } from 'd3-hierarchy' +import { OrdinalColorScaleConfig, InheritedColorConfig } from '@nivo/colors' +import { Theme, MergedTheme, Dimensions, Box } from '@nivo/core' +import { DefaultSunburstProps } from './props' type NameAndColor = { - name: string + // name: string color: string } // Pretty sure this should be generic.. loc is example from website -type DataLocation = NameAndColor & { - loc: number -} +type DataLocation> = NameAndColor & Datum -type DataChildren = NameAndColor & { - children: DataChildren[] | DataLocation[] +type DataChildren> = NameAndColor & { + children: DataChildren[] | DataLocation[] } -type Data = NameAndColor & DataChildren +export type Data> = NameAndColor & DataChildren -export type SunburstNode = { +export type SunburstNodeOld = { data: { id: string color: string @@ -29,28 +30,47 @@ export type SunburstNode = { y1: number } +type ComputedNode = any> = HierarchyRectangularNode< + DataChildren +> + +export type SunburstNode = any> = Omit< + ComputedNode, + 'children' | 'data' +> & { + data: { + // children: never + color: string + id: string + value: number + percentage: number + depth: number + ancestor: SunburstNode['data'] + } +} + type CommonSunburstProps = { - data: Data + // data: Data - identity: string | ((node: SunburstNode['data']) => string) - value: string | ((node: SunburstNode['data']) => number) + identity: string | ((node: ComputedNode['data']) => string) + value: string | ((node: ComputedNode['data']) => number) margin: Box cornerRadius: number - colors: OrdinalColorsInstruction + colors: OrdinalColorScaleConfig> borderWidth: number borderColor: string - childColor: InheritedColorProp + childColor: InheritedColorConfig // slices labels enableSlicesLabels: boolean - sliceLabel: string | ((node: SunburstNode['data']) => string) + sliceLabel: string | ((node: ComputedNode['data']) => string) - slicesLabelsSkipAngle?: number - slicesLabelsTextColor?: InheritedColorProp + slicesLabelsSkipAngle: number + slicesLabelsTextColor: InheritedColorConfig role: string @@ -84,7 +104,8 @@ type ComputedSunburstProps = { partition: any // computed - arcGenerator: (node: SunburstNode) => string // computed + // arcGenerator: (node: SunburstNode) => string // computed + arcGenerator: Arc> radius: number // computed centerX: number // computed @@ -99,12 +120,14 @@ type ComputedSunburstProps = { getSliceLabel: (node: SunburstNode['data']) => string } -export type SunburstSvgProps = Dimensions & - Partial & - ComputedSunburstProps & { nodes: ComputedSunburstProps['nodes'] } +export type SunburstSvgProps> = Dimensions & + DefaultSunburstProps & + Partial & { + data: Data + } export type SunburstArcProps = Pick< - SunburstSvgProps, + SunburstSvgProps, | 'tooltip' | 'tooltipFormat' | 'onClick' @@ -113,16 +136,14 @@ export type SunburstArcProps = Pick< | 'borderWidth' | 'borderColor' > & - Pick & - TooltipHandlers & { + Pick & { node: SunburstNode - path?: string // computed } -export type SunburstLabelProps = Pick & { - label: SunburstSvgProps['getSliceLabel'] +export type SunburstLabelProps = Pick & { + label: ComputedSunburstProps['getSliceLabel'] skipAngle?: number - textColor: (payload: SunburstNode['data'], theme?: Theme) => string + textColor: CommonSunburstProps['slicesLabelsTextColor'] } export type TooltipHandlers = { diff --git a/tsconfig.monorepo.json b/tsconfig.monorepo.json index 2407c65ab..faaea1a5d 100644 --- a/tsconfig.monorepo.json +++ b/tsconfig.monorepo.json @@ -18,5 +18,6 @@ { "path": "./packages/bullet" }, { "path": "./packages/marimekko" }, { "path": "./packages/pie" }, + { "path": "./packages/sunburst" }, ] }