From 3a6537b011955ad70a4e1bcdaff8ac0afc1cf0de Mon Sep 17 00:00:00 2001 From: Neil Kistner Date: Mon, 30 Nov 2020 21:13:56 -0600 Subject: [PATCH] feat(sunburst): add layers support --- packages/sunburst/src/Sunburst.tsx | 75 ++++++++++++++----- packages/sunburst/src/hooks.ts | 38 +++++++++- packages/sunburst/src/props.ts | 11 ++- packages/sunburst/src/types.ts | 16 ++++ .../sunburst/stories/sunburst.stories.tsx | 23 ++++++ packages/sunburst/tests/Sunburst.test.tsx | 31 ++++++++ website/src/data/components/sunburst/meta.yml | 2 + website/src/data/components/sunburst/props.js | 28 +++++++ 8 files changed, 198 insertions(+), 26 deletions(-) diff --git a/packages/sunburst/src/Sunburst.tsx b/packages/sunburst/src/Sunburst.tsx index c98cb3bb7..04a1d8761 100644 --- a/packages/sunburst/src/Sunburst.tsx +++ b/packages/sunburst/src/Sunburst.tsx @@ -1,11 +1,11 @@ -import React, { useMemo } from 'react' +import React, { Fragment, ReactNode, createElement, useMemo } from 'react' // @ts-ignore import { Container, SvgWrapper, useDimensions, bindDefs } from '@nivo/core' import { SunburstLabels } from './SunburstLabels' import { SunburstArc } from './SunburstArc' import { defaultProps } from './props' -import { useSunburst } from './hooks' -import { SvgProps } from './types' +import { useSunburst, useSunburstLayerContext } from './hooks' +import { SvgProps, SunburstLayerId, SunburstLayer } from './types' const InnerSunburst = (props: SvgProps) => { const { @@ -14,6 +14,8 @@ const InnerSunburst = (props: SvgProps) => { value, valueFormat, + layers = defaultProps.layers as SunburstLayer[], + colors, childColor, @@ -79,15 +81,14 @@ const InnerSunburst = (props: SvgProps) => { targetKey: 'data.fill', }) - return ( - - + const layerById: Record = { + slices: null, + sliceLabels: null, + } + + if (layers.includes('slices')) { + layerById.slices = ( + {filteredNodes.map(node => ( key={node.data.id} @@ -103,15 +104,49 @@ const InnerSunburst = (props: SvgProps) => { onMouseMove={onMouseMove} /> ))} - {enableSliceLabels && ( - - nodes={nodes} - label={sliceLabel} - skipAngle={sliceLabelsSkipAngle} - textColor={sliceLabelsTextColor} - /> - )} + ) + } + + if (enableSliceLabels && layers.includes('sliceLabels')) { + layerById.sliceLabels = ( + + key="sliceLabels" + nodes={nodes} + label={sliceLabel} + skipAngle={sliceLabelsSkipAngle} + textColor={sliceLabelsTextColor} + /> + ) + } + + const layerContext = useSunburstLayerContext({ + nodes: filteredNodes, + arcGenerator, + centerX, + centerY, + radius, + }) + + return ( + + {layers.map((layer, i) => { + if (layerById[layer as SunburstLayerId] !== undefined) { + return layerById[layer as SunburstLayerId] + } + + if (typeof layer === 'function') { + return {createElement(layer, layerContext)} + } + + return null + })} ) } diff --git a/packages/sunburst/src/hooks.ts b/packages/sunburst/src/hooks.ts index f01c704b5..0aa710dda 100644 --- a/packages/sunburst/src/hooks.ts +++ b/packages/sunburst/src/hooks.ts @@ -3,11 +3,18 @@ import sortBy from 'lodash/sortBy' import cloneDeep from 'lodash/cloneDeep' import React, { createElement, useCallback, useMemo } from 'react' import { getAccessorFor, useTheme, useValueFormatter } from '@nivo/core' -import { arc } from 'd3-shape' +import { arc, Arc } from 'd3-shape' import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors' import { useTooltip } from '@nivo/tooltip' import { partition as d3Partition, hierarchy as d3Hierarchy } from 'd3-hierarchy' -import { CommonProps, ComputedDatum, DataProps, NormalizedDatum, MouseEventHandlers } from './types' +import { + CommonProps, + ComputedDatum, + DataProps, + NormalizedDatum, + MouseEventHandlers, + SunburstCustomLayerProps, +} from './types' type MaybeColor = { color?: string } @@ -182,3 +189,30 @@ export const useSunburst = ({ return { arcGenerator, nodes } } + +/** + * Memoize the context to pass to custom layers. + */ +export const useSunburstLayerContext = ({ + nodes, + arcGenerator, + centerX, + centerY, + radius, +}: { + nodes: ComputedDatum[] + arcGenerator: Arc> + centerX: number + centerY: number + radius: number +}): SunburstCustomLayerProps => + useMemo( + () => ({ + nodes, + arcGenerator, + centerX, + centerY, + radius, + }), + [nodes, arcGenerator, centerX, centerY, radius] + ) diff --git a/packages/sunburst/src/props.ts b/packages/sunburst/src/props.ts index 4d6ec537d..abc3cbd5f 100644 --- a/packages/sunburst/src/props.ts +++ b/packages/sunburst/src/props.ts @@ -1,6 +1,6 @@ +import { OrdinalColorScaleConfig } from '@nivo/colors' import { SunburstTooltip } from './SunburstTooltip' - -export type DefaultSunburstProps = Required +import { SunburstLayerId } from './types' export const defaultProps = { id: 'id', @@ -8,7 +8,9 @@ export const defaultProps = { cornerRadius: 0, - colors: { scheme: 'nivo' }, + layers: ['slices', 'sliceLabels'] as SunburstLayerId[], + + colors: ({ scheme: 'nivo' } as unknown) as OrdinalColorScaleConfig, borderWidth: 1, borderColor: 'white', @@ -18,6 +20,7 @@ export const defaultProps = { // slices labels enableSliceLabels: false, sliceLabel: 'formattedValue', + sliceLabelsSkipAngle: 0, sliceLabelsTextColor: { theme: 'labels.text.fill' }, isInteractive: true, @@ -25,4 +28,4 @@ export const defaultProps = { motionConfig: 'gentle', tooltip: SunburstTooltip, -} as const +} diff --git a/packages/sunburst/src/types.ts b/packages/sunburst/src/types.ts index 6956d41da..23d4a52d7 100644 --- a/packages/sunburst/src/types.ts +++ b/packages/sunburst/src/types.ts @@ -16,6 +16,20 @@ export type DatumValue = number export type DatumPropertyAccessor = (datum: RawDatum) => T export type LabelAccessorFunction = (datum: RawDatum) => string | number +export type SunburstLayerId = 'slices' | 'sliceLabels' + +export interface SunburstCustomLayerProps { + nodes: ComputedDatum[] + centerX: number + centerY: number + radius: number + arcGenerator: Arc> +} + +export type SunburstCustomLayer = React.FC> + +export type SunburstLayer = SunburstLayerId | SunburstCustomLayer + export interface DataProps { data: RawDatum id?: string | number | DatumPropertyAccessor @@ -52,6 +66,8 @@ export interface ComputedDatum { } export type CommonProps = { + layers: SunburstLayer[] + margin: Box cornerRadius: number diff --git a/packages/sunburst/stories/sunburst.stories.tsx b/packages/sunburst/stories/sunburst.stories.tsx index 3428c08c0..39f0593a1 100644 --- a/packages/sunburst/stories/sunburst.stories.tsx +++ b/packages/sunburst/stories/sunburst.stories.tsx @@ -154,3 +154,26 @@ stories.add( }, } ) + +const CenteredMetric = ({ nodes, centerX, centerY }) => { + const total = nodes.reduce((total, datum) => total + datum.value, 0) + + return ( + + {Number.parseFloat(total).toExponential(2)} + + ) +} + +stories.add('adding a metric in the center using a custom layer', () => ( + +)) diff --git a/packages/sunburst/tests/Sunburst.test.tsx b/packages/sunburst/tests/Sunburst.test.tsx index a1a16e53a..58bb23e6d 100644 --- a/packages/sunburst/tests/Sunburst.test.tsx +++ b/packages/sunburst/tests/Sunburst.test.tsx @@ -568,4 +568,35 @@ describe('Sunburst', () => { expect(tooltip.text()).toEqual('B') }) }) + + describe('layers', () => { + it('should support disabling a layer', () => { + const wrapper = mount() + expect(wrapper.find('SunburstArc')).toHaveLength(5) + + wrapper.setProps({ layers: ['sliceLabels'] }) + expect(wrapper.find('SunburstArc')).toHaveLength(0) + }) + + it('should support adding a custom layer', () => { + const CustomLayer = () => null + + const wrapper = mount( + + ) + + const customLayer = wrapper.find(CustomLayer) + + expect(customLayer.prop('nodes')).toHaveLength(5) + expect(customLayer.prop('centerX')).toEqual(200) + expect(customLayer.prop('centerY')).toEqual(200) + expect(customLayer.prop('arcGenerator')).toBeDefined() + expect(customLayer.prop('radius')).toEqual(200) + }) + }) }) diff --git a/website/src/data/components/sunburst/meta.yml b/website/src/data/components/sunburst/meta.yml index 3b83370f2..55220b5a6 100644 --- a/website/src/data/components/sunburst/meta.yml +++ b/website/src/data/components/sunburst/meta.yml @@ -28,6 +28,8 @@ Sunburst: link: sunburst--patterns-gradients - label: drill down to children link: sunburst--children-drill-down + - label: adding a metric in the center using a custom layer + link: sunburst--adding-a-metric-in-the-center-using-a-custom-layer description: | The responsive alternative of this component is `ResponsiveSunburst`. diff --git a/website/src/data/components/sunburst/props.js b/website/src/data/components/sunburst/props.js index 10359b289..49268b297 100644 --- a/website/src/data/components/sunburst/props.js +++ b/website/src/data/components/sunburst/props.js @@ -145,6 +145,34 @@ const props = [ step: 1, }, }, + { + key: 'layers', + group: 'Customization', + help: 'Defines the order of layers and add custom layers.', + description: ` + You can also use this to insert extra layers + to the chart, the extra layer must be a function. + + The layer component which will receive the chart's + context & computed data and must return a valid SVG element + for the \`Sunburst\` component. + + The context passed to layers has the following structure: + + \`\`\` + { + nodes: ComputedDatum[], + arcGenerator: Function + centerX: number + centerY: number + radius: number + } + \`\`\` + `, + required: false, + type: 'Array', + defaultValue: defaultProps.layers, + }, { key: 'isInteractive', flavors: ['svg'],