diff --git a/packages/arcs/src/centers.ts b/packages/arcs/src/centers.ts index 8432cbfd2..01a9fc06f 100644 --- a/packages/arcs/src/centers.ts +++ b/packages/arcs/src/centers.ts @@ -45,6 +45,11 @@ export const useArcCentersTransition = ) => { + // center label of root node + const dataWithCenteredRoot = data.map(d => + d.arc.innerRadius === 0 ? { ...d, arc: { ...d.arc, outerRadius: 0 } } : d + ) + const { animate, config: springConfig } = useMotionConfig() const phases = useArcTransitionMode(mode, extra) @@ -58,7 +63,7 @@ export const useArcCentersTransition = (data, { + >(dataWithCenteredRoot, { keys: datum => datum.id, initial: phases.update, from: phases.enter, diff --git a/packages/sunburst/package.json b/packages/sunburst/package.json index 6c6c7bd4c..f382bf370 100644 --- a/packages/sunburst/package.json +++ b/packages/sunburst/package.json @@ -33,11 +33,13 @@ "@nivo/colors": "0.74.0", "@nivo/tooltip": "0.74.0", "d3-hierarchy": "^1.1.8", + "d3-scale": "^3.2.3", "lodash": "^4.17.21" }, "devDependencies": { "@nivo/core": "0.74.0", - "@types/d3-hierarchy": "^1.1.7" + "@types/d3-hierarchy": "^1.1.7", + "@types/d3-scale": "^3.2.3" }, "peerDependencies": { "@nivo/core": "0.74.0", diff --git a/packages/sunburst/src/Sunburst.tsx b/packages/sunburst/src/Sunburst.tsx index e3540ea3a..402b74de6 100644 --- a/packages/sunburst/src/Sunburst.tsx +++ b/packages/sunburst/src/Sunburst.tsx @@ -27,6 +27,8 @@ const InnerSunburst = ({ value = defaultProps.value, valueFormat, cornerRadius = defaultProps.cornerRadius, + innerRadiusRatio = defaultProps.innerRadiusRatio, + renderRootNode = defaultProps.renderRootNode, layers = defaultProps.layers as SunburstLayer[], colors = defaultProps.colors, colorBy = defaultProps.colorBy, @@ -73,6 +75,8 @@ const InnerSunburst = ({ valueFormat, radius, cornerRadius, + innerRadiusRatio, + renderRootNode, colors, colorBy, inheritColorFromParent, diff --git a/packages/sunburst/src/hooks.ts b/packages/sunburst/src/hooks.ts index f0318c9dc..3c6b685e5 100644 --- a/packages/sunburst/src/hooks.ts +++ b/packages/sunburst/src/hooks.ts @@ -1,5 +1,6 @@ import { useMemo } from 'react' import { partition as d3Partition, hierarchy as d3Hierarchy } from 'd3-hierarchy' +import { scaleRadial as d3ScaleRadial } from 'd3-scale' import cloneDeep from 'lodash/cloneDeep' import sortBy from 'lodash/sortBy' import { usePropertyAccessor, useTheme, useValueFormatter } from '@nivo/core' @@ -21,6 +22,8 @@ export const useSunburst = ({ valueFormat, radius, cornerRadius = defaultProps.cornerRadius, + innerRadiusRatio = defaultProps.innerRadiusRatio, + renderRootNode = defaultProps.renderRootNode, colors = defaultProps.colors, colorBy = defaultProps.colorBy, inheritColorFromParent = defaultProps.inheritColorFromParent, @@ -32,6 +35,8 @@ export const useSunburst = ({ valueFormat?: DataProps['valueFormat'] radius: number cornerRadius?: SunburstCommonProps['cornerRadius'] + innerRadiusRatio?: SunburstCommonProps['innerRadiusRatio'] + renderRootNode?: SunburstCommonProps['renderRootNode'] colors?: SunburstCommonProps['colors'] colorBy?: SunburstCommonProps['colorBy'] inheritColorFromParent?: SunburstCommonProps['inheritColorFromParent'] @@ -57,8 +62,10 @@ export const useSunburst = ({ const hierarchy = d3Hierarchy(clonedData).sum(getValue) const partition = d3Partition().size([2 * Math.PI, radius * radius]) - // exclude root node - const descendants = partition(hierarchy).descendants().slice(1) + + const descendants = renderRootNode + ? partition(hierarchy).descendants() + : partition(hierarchy).descendants().slice(1) const total = hierarchy.value ?? 0 @@ -68,6 +75,12 @@ export const useSunburst = ({ // are going to be computed first const sortedNodes = sortBy(descendants, 'depth') + const innerRadiusOffset = radius * Math.min(innerRadiusRatio, 1) + + const maxDepth = Math.max(...sortedNodes.map(n => n.depth)) + + const scale = d3ScaleRadial().domain([0, maxDepth]).range([innerRadiusOffset, radius]) + return sortedNodes.reduce[]>((acc, descendant) => { const id = getId(descendant.data) // d3 hierarchy node value is optional by default as it depends on @@ -82,8 +95,12 @@ export const useSunburst = ({ const arc: Arc = { startAngle: descendant.x0, endAngle: descendant.x1, - innerRadius: Math.sqrt(descendant.y0), - outerRadius: Math.sqrt(descendant.y1), + innerRadius: + renderRootNode && descendant.depth === 0 ? 0 : scale(descendant.depth - 1), + outerRadius: + renderRootNode && descendant.depth === 0 + ? innerRadiusOffset + : scale(descendant.depth), } let parent: ComputedDatum | undefined @@ -125,6 +142,8 @@ export const useSunburst = ({ getColor, inheritColorFromParent, getChildColor, + innerRadiusRatio, + renderRootNode, ]) const arcGenerator = useArcGenerator({ cornerRadius }) diff --git a/packages/sunburst/src/props.ts b/packages/sunburst/src/props.ts index 9b6a7f11f..693c4382e 100644 --- a/packages/sunburst/src/props.ts +++ b/packages/sunburst/src/props.ts @@ -7,6 +7,8 @@ export const defaultProps = { id: 'id', value: 'value', cornerRadius: 0, + innerRadiusRatio: 0.4, + renderRootNode: false, layers: ['arcs', 'arcLabels'] as SunburstLayerId[], colors: ({ scheme: 'nivo' } as unknown) as OrdinalColorScaleConfig, colorBy: 'id' as const, diff --git a/packages/sunburst/src/types.ts b/packages/sunburst/src/types.ts index de42cad5c..e197b7146 100644 --- a/packages/sunburst/src/types.ts +++ b/packages/sunburst/src/types.ts @@ -63,6 +63,8 @@ export type SunburstCommonProps = { height: number margin?: Box cornerRadius: number + innerRadiusRatio: number + renderRootNode: boolean theme: Theme colors: OrdinalColorScaleConfig, 'color' | 'fill'>> colorBy: 'id' | 'depth' diff --git a/packages/sunburst/stories/sunburst.stories.tsx b/packages/sunburst/stories/sunburst.stories.tsx index 1c5736bad..3d5cf821c 100644 --- a/packages/sunburst/stories/sunburst.stories.tsx +++ b/packages/sunburst/stories/sunburst.stories.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { storiesOf } from '@storybook/react' import { action } from '@storybook/addon-actions' -import { withKnobs, boolean, select } from '@storybook/addon-knobs' +import { withKnobs, boolean, select, number } from '@storybook/addon-knobs' // @ts-ignore import { linearGradientDef, patternDotsDef, useTheme } from '@nivo/core' // @ts-ignore @@ -223,3 +223,16 @@ const CenteredMetric = ({ nodes, centerX, centerY }: SunburstCustomLayerProps ( {...commonProperties} layers={['arcs', 'arcLabels', CenteredMetric]} /> )) + +stories.add('with root node', () => ( + + {...commonProperties} + innerRadiusRatio={number('innerRadiusRatio', 0.25, { + range: true, + min: 0.0, + max: 0.95, + step: 0.05, + })} + renderRootNode={boolean('renderRootNode', true)} + /> +)) diff --git a/website/src/data/components/sunburst/props.ts b/website/src/data/components/sunburst/props.ts index a3edcbb83..01aa8f134 100644 --- a/website/src/data/components/sunburst/props.ts +++ b/website/src/data/components/sunburst/props.ts @@ -103,6 +103,29 @@ const props: ChartProperty[] = [ step: 1, }, }, + { + key: 'innerRadiusRatio', + help: `Size of the center circle. Value should be between 0~1 as it's a ratio from original radius.`, + type: 'number', + required: false, + defaultValue: defaultProps.innerRadiusRatio, + controlType: 'range', + group: 'Base', + controlOptions: { + min: 0, + max: 0.95, + step: 0.05, + }, + }, + { + key: 'renderRootNode', + help: `Render the root node. By default, the root node is omitted.`, + type: 'boolean', + required: false, + defaultValue: defaultProps.renderRootNode, + controlType: 'switch', + group: 'Base', + }, { key: 'width', enableControlForFlavors: ['api'], diff --git a/website/src/pages/sunburst/api.js b/website/src/pages/sunburst/api.js index e4b78cf80..9756a58ba 100644 --- a/website/src/pages/sunburst/api.js +++ b/website/src/pages/sunburst/api.js @@ -34,6 +34,8 @@ const SunburstApi = () => { value: 'loc', valueFormat: { format: '', enabled: false }, cornerRadius: 2, + innerRadiusRatio: 0.4, + renderRootNode: false, borderWidth: 1, borderColor: 'white', colors: { scheme: 'nivo' }, diff --git a/website/src/pages/sunburst/index.js b/website/src/pages/sunburst/index.js index 8eb989281..7859fd1ae 100644 --- a/website/src/pages/sunburst/index.js +++ b/website/src/pages/sunburst/index.js @@ -24,6 +24,8 @@ const initialProperties = { value: 'loc', valueFormat: { format: '', enabled: false }, cornerRadius: 2, + innerRadiusRatio: 0.4, + renderRootNode: false, borderWidth: 1, borderColor: { theme: 'background' }, colors: { scheme: 'nivo' }, diff --git a/yarn.lock b/yarn.lock index 2f65b47d7..867c8a29f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6457,6 +6457,13 @@ dependencies: "@types/d3-time" "*" +"@types/d3-scale@^3.2.3": + version "3.3.2" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-3.3.2.tgz#18c94e90f4f1c6b1ee14a70f14bfca2bd1c61d06" + integrity sha512-gGqr7x1ost9px3FvIfUMi5XA/F/yAf4UkUDtdQhpH92XCT0Oa7zkkRzY61gPVJq+DxpHn/btouw5ohWkbBsCzQ== + dependencies: + "@types/d3-time" "^2" + "@types/d3-shape@^1": version "1.3.8" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.8.tgz#c3c15ec7436b4ce24e38de517586850f1fea8e89" @@ -6491,6 +6498,11 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-1.1.1.tgz#6cf3a4242c3bbac00440dfb8ba7884f16bedfcbf" integrity sha512-ULX7LoqXTCYtM+tLYOaeAJK7IwCT+4Gxlm2MaH0ErKLi07R5lh8NHCAyWcDkCCmx1AfRcBEV6H9QE9R25uP7jw== +"@types/d3-time@^2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" + integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== + "@types/debug@^0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-0.0.30.tgz#dc1e40f7af3b9c815013a7860e6252f6352a84df" @@ -22270,7 +22282,7 @@ react-docgen@^5.0.0: node-dir "^0.1.10" strip-indent "^3.0.0" -react-dom@17.0.2, react-dom@^16.8.3, react-dom@^17.0.2: +react-dom@17.0.2, react-dom@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== @@ -22279,6 +22291,16 @@ react-dom@17.0.2, react-dom@^16.8.3, react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-dom@^16.8.3: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" + integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + scheduler "^0.19.1" + react-draggable@^4.0.3: version "4.2.0" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.2.0.tgz#40cc5209082ca7d613104bf6daf31372cc0e1114" @@ -22584,7 +22606,7 @@ react-transition-group@^4.3.0: loose-envify "^1.4.0" prop-types "^15.6.2" -react@17.0.2, react@^16.8.3, react@^16.9.17, react@^17.0.2: +react@17.0.2, react@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== @@ -22592,6 +22614,15 @@ react@17.0.2, react@^16.8.3, react@^16.9.17, react@^17.0.2: loose-envify "^1.1.0" object-assign "^4.1.1" +react@^16.8.3, react@^16.9.17: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + prop-types "^15.6.2" + reactcss@^1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" @@ -23593,6 +23624,14 @@ saxes@^5.0.0: dependencies: xmlchars "^2.2.0" +scheduler@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196" + integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + scheduler@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"