diff --git a/packages/circle-packing/src/CirclePacking.tsx b/packages/circle-packing/src/CirclePacking.tsx index f6ec606b0..b8d79f278 100644 --- a/packages/circle-packing/src/CirclePacking.tsx +++ b/packages/circle-packing/src/CirclePacking.tsx @@ -38,11 +38,13 @@ const InnerCirclePacking = ({ >, colorBy = defaultProps.colorBy, childColor = defaultProps.childColor as InheritedColorConfig>, + circleComponent = CircleSvg, enableLabels = defaultProps.enableLabels, label = defaultProps.label, labelsFilter, labelsSkipRadius = defaultProps.labelsSkipRadius, labelsTextColor = defaultProps.labelsTextColor as InheritedColorConfig>, + labelsComponent = LabelSvg, layers = defaultProps.layers, isInteractive, onMouseEnter, @@ -87,7 +89,7 @@ const InnerCirclePacking = ({ onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick} - component={CircleSvg} + component={circleComponent} tooltip={tooltip} /> ) @@ -102,7 +104,7 @@ const InnerCirclePacking = ({ filter={labelsFilter} skipRadius={labelsSkipRadius} textColor={labelsTextColor} - component={LabelSvg} + component={labelsComponent} /> ) } diff --git a/packages/circle-packing/src/CirclePackingHtml.tsx b/packages/circle-packing/src/CirclePackingHtml.tsx index 49b6047e5..0b82c3491 100644 --- a/packages/circle-packing/src/CirclePackingHtml.tsx +++ b/packages/circle-packing/src/CirclePackingHtml.tsx @@ -32,11 +32,13 @@ export const InnerCirclePackingHtml = ({ >, colorBy = defaultProps.colorBy, childColor = defaultProps.childColor as InheritedColorConfig>, + circleComponent = CircleHtml, enableLabels = defaultProps.enableLabels, label = defaultProps.label, labelsFilter, labelsSkipRadius = defaultProps.labelsSkipRadius, labelsTextColor = defaultProps.labelsTextColor as InheritedColorConfig>, + labelsComponent = LabelHtml, layers = defaultProps.layers, isInteractive, onMouseEnter, @@ -81,7 +83,7 @@ export const InnerCirclePackingHtml = ({ onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick} - component={CircleHtml} + component={circleComponent} tooltip={tooltip} /> ) @@ -96,7 +98,7 @@ export const InnerCirclePackingHtml = ({ filter={labelsFilter} skipRadius={labelsSkipRadius} textColor={labelsTextColor} - component={LabelHtml} + component={labelsComponent} /> ) } diff --git a/packages/circle-packing/src/ResponsiveCirclePacking.tsx b/packages/circle-packing/src/ResponsiveCirclePacking.tsx index ebd704535..ab2ece24f 100644 --- a/packages/circle-packing/src/ResponsiveCirclePacking.tsx +++ b/packages/circle-packing/src/ResponsiveCirclePacking.tsx @@ -3,8 +3,13 @@ import { ResponsiveWrapper } from '@nivo/core' import { CirclePacking } from './CirclePacking' import { CirclePackSvgProps } from './types' +type ResponsiveCirclePackingProps = Partial< + Omit, 'data' | 'width' | 'height'> +> & + Pick, 'data'> + export const ResponsiveCirclePacking = ( - props: Omit, 'width' | 'height'> + props: ResponsiveCirclePackingProps ) => ( {({ width, height }: { width: number; height: number }) => ( diff --git a/packages/circle-packing/src/ResponsiveCirclePackingCanvas.tsx b/packages/circle-packing/src/ResponsiveCirclePackingCanvas.tsx index 6af3fbdf9..25c005fdb 100644 --- a/packages/circle-packing/src/ResponsiveCirclePackingCanvas.tsx +++ b/packages/circle-packing/src/ResponsiveCirclePackingCanvas.tsx @@ -3,9 +3,13 @@ import { ResponsiveWrapper } from '@nivo/core' import { CirclePackingCanvasProps } from './types' import { CirclePackingCanvas } from './CirclePackingCanvas' +type ResponsiveCirclePackingCanvasProps = Partial< + Omit, 'data' | 'width' | 'height'> +> & + Pick, 'data'> + export const ResponsiveCirclePackingCanvas = ( - props: Partial, 'data' | 'width' | 'height'>> & - Pick, 'data'> + props: ResponsiveCirclePackingCanvasProps ) => ( {({ width, height }: { width: number; height: number }) => ( diff --git a/packages/circle-packing/src/ResponsiveCirclePackingHtml.tsx b/packages/circle-packing/src/ResponsiveCirclePackingHtml.tsx new file mode 100644 index 000000000..87ca87e78 --- /dev/null +++ b/packages/circle-packing/src/ResponsiveCirclePackingHtml.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { ResponsiveWrapper } from '@nivo/core' +import { CirclePackingHtmlProps } from './types' +import { CirclePackingHtml } from './CirclePackingHtml' + +type ResponsiveCirclePackingHtmlProps = Partial< + Omit, 'data' | 'width' | 'height'> +> & + Pick, 'data'> + +export const ResponsiveCirclePackingHtml = ( + props: ResponsiveCirclePackingHtmlProps +) => ( + + {({ width, height }: { width: number; height: number }) => ( + width={width} height={height} {...props} /> + )} + +) diff --git a/packages/circle-packing/src/types.ts b/packages/circle-packing/src/types.ts index 329e7e04a..8d1ea7327 100644 --- a/packages/circle-packing/src/types.ts +++ b/packages/circle-packing/src/types.ts @@ -71,12 +71,14 @@ export interface CirclePackingCommonProps { childColor: InheritedColorConfig> borderWidth: number borderColor: InheritedColorConfig> + circleComponent: CircleComponent enableLabels: boolean label: PropertyAccessor, string | number> labelsFilter?: (label: ComputedLabel) => boolean labelsSkipRadius: number labelsTextColor: InheritedColorConfig> + labelsComponent: LabelComponent layers: CirclePackingLayer[] diff --git a/packages/circle-packing/tests/CirclePacking.test.tsx b/packages/circle-packing/tests/CirclePacking.test.tsx index eba539334..146c6fecf 100644 --- a/packages/circle-packing/tests/CirclePacking.test.tsx +++ b/packages/circle-packing/tests/CirclePacking.test.tsx @@ -18,79 +18,112 @@ const testCases = [ testCases.forEach(testCase => { describe(testCase.Chart, () => { - it('should render as much node as items', () => { - const wrapper = mount( - width={600} height={600} data={sampleData} /> - ) - - expect(wrapper.find(testCase.circle).length).toBe(11) - }) - - it(`should skip parent nodes if 'leavesOnly' is 'true'`, () => { - const wrapper = mount( - - width={600} - height={600} - data={sampleData} - leavesOnly={true} - /> - ) - - expect(wrapper.find(testCase.circle).length).toBe(7) - }) - - it(`should render as much labels as nodes if 'enableLabels' is 'true'`, () => { - const wrapper = mount( - - width={600} - height={600} - data={sampleData} - enableLabels={true} - /> - ) - - expect(wrapper.find(testCase.label).length).toBe(11) - }) - - it(`should render no label if 'enableLabels' is 'false'`, () => { - const wrapper = mount( - - width={600} - height={600} - data={sampleData} - enableLabels={false} - /> - ) - - expect(wrapper.find(testCase.label).length).toBe(0) - }) - - it(`should allow to skip labels using 'labelsSkipRadius' if radius is lower than given value`, () => { - const wrapper = mount( - - width={600} - height={600} - data={sampleData} - enableLabels={true} - labelsSkipRadius={24} - /> - ) - - expect(wrapper.find(testCase.label).length).toBe(10) + describe('circles', () => { + it('should render as much circles as items', () => { + const wrapper = mount( + width={600} height={600} data={sampleData} /> + ) + + expect(wrapper.find(testCase.circle).length).toBe(11) + }) + + it(`should only render leaf nodes if 'leavesOnly' is 'true'`, () => { + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + leavesOnly={true} + /> + ) + + expect(wrapper.find(testCase.circle).length).toBe(7) + }) + + it('should support a custom circle component', () => { + const CustomCircle = () => + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + circleComponent={CustomCircle} + /> + ) + + expect(wrapper.find(CustomCircle).length).toBe(11) + }) }) - it(`should allow to filter labels using a custom filter function`, () => { - const wrapper = mount( - - width={600} - height={600} - data={sampleData} - enableLabels={true} - labelsFilter={label => label.node.depth === 1} - /> - ) - - expect(wrapper.find(testCase.label).length).toBe(3) + describe('labels', () => { + it(`should render as much labels as nodes if 'enableLabels' is 'true'`, () => { + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + enableLabels={true} + /> + ) + + expect(wrapper.find(testCase.label).length).toBe(11) + }) + + it(`should render no label if 'enableLabels' is 'false'`, () => { + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + enableLabels={false} + /> + ) + + expect(wrapper.find(testCase.label).length).toBe(0) + }) + + it(`should allow to skip labels using 'labelsSkipRadius' if radius is lower than given value`, () => { + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + enableLabels={true} + labelsSkipRadius={24} + /> + ) + + expect(wrapper.find(testCase.label).length).toBe(10) + }) + + it(`should allow to filter labels using a custom filter function`, () => { + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + enableLabels={true} + labelsFilter={label => label.node.depth === 1} + /> + ) + + expect(wrapper.find(testCase.label).length).toBe(3) + }) + + it('should support a custom label component', () => { + const CustomLabel = () => + const wrapper = mount( + + width={600} + height={600} + data={sampleData} + enableLabels={true} + labelsComponent={CustomLabel} + /> + ) + + expect(wrapper.find(CustomLabel).length).toBe(11) + }) }) }) }) diff --git a/website/src/data/components/circle-packing/mapper.js b/website/src/data/components/circle-packing/mapper.js index 25c9f9dcc..51eea734d 100644 --- a/website/src/data/components/circle-packing/mapper.js +++ b/website/src/data/components/circle-packing/mapper.js @@ -1,6 +1,7 @@ -import { settingsMapper } from '../../../lib/settings' +import { mapFormat, settingsMapper } from '../../../lib/settings' export default settingsMapper({ + valueFormat: mapFormat, label: value => { if (value === `d => \`\${d.id}: \${d.value}\``) return d => `${d.id}: ${d.value}` return value diff --git a/website/src/data/components/circle-packing/props.js b/website/src/data/components/circle-packing/props.js index 2f1837df5..6d95f8993 100644 --- a/website/src/data/components/circle-packing/props.js +++ b/website/src/data/components/circle-packing/props.js @@ -71,6 +71,21 @@ const props = [ required: false, defaultValue: defaultProps.value, }, + { + key: 'valueFormat', + group: 'Base', + help: 'Optional formatter for values.', + description: ` + The formatted value can then be used for labels & tooltips. + + Under the hood, nivo uses [d3-format](https://github.com/d3/d3-format), + please have a look at it for available formats, you can also pass a function + which will receive the raw value and should return the formatted one. + `, + required: false, + type: 'string | (value: number) => string | number', + controlType: 'valueFormat', + }, { key: 'padding', help: 'Padding between each circle.', @@ -205,6 +220,12 @@ const props = [ group: 'Style', }, ...defsProperties('Style', ['svg']), + { + key: 'circleComponent', + help: 'Custom circle component.', + group: 'Style', + required: false, + }, { key: 'enableLabels', help: 'Enable/disable labels.', @@ -228,10 +249,12 @@ const props = [ controlType: 'choices', group: 'Labels', controlOptions: { - choices: ['id', 'value', `d => \`\${d.id}: \${d.value}\``].map(choice => ({ - label: choice, - value: choice, - })), + choices: ['id', 'value', 'formattedValue', `d => \`\${d.id}: \${d.value}\``].map( + choice => ({ + label: choice, + value: choice, + }) + ), }, }, { @@ -261,6 +284,12 @@ const props = [ controlType: 'inheritedColor', group: 'Labels', }, + { + key: 'labelsComponent', + help: 'Custom label component.', + group: 'Labels', + required: false, + }, { key: 'isInteractive', flavors: ['svg', 'html', 'canvas'], diff --git a/website/src/pages/circle-packing/api.js b/website/src/pages/circle-packing/api.js index c2e0090ad..57dbabd63 100644 --- a/website/src/pages/circle-packing/api.js +++ b/website/src/pages/circle-packing/api.js @@ -35,6 +35,7 @@ const CirclePackingApi = () => { root: JSON.stringify(root, null, ' '), identity: 'name', value: 'loc', + valueFormat: { format: '', enabled: false }, colors: { scheme: 'nivo' }, colorBy: 'depth', padding: 1, diff --git a/website/src/pages/circle-packing/canvas.js b/website/src/pages/circle-packing/canvas.js index d78344032..b77893e61 100644 --- a/website/src/pages/circle-packing/canvas.js +++ b/website/src/pages/circle-packing/canvas.js @@ -30,6 +30,7 @@ const initialProperties = { typeof window !== 'undefined' && window.devicePixelRatio ? window.devicePixelRatio : 1, id: 'name', value: 'value', + valueFormat: { format: '', enabled: false }, colors: { scheme: 'spectral' }, childColor: 'noinherit', padding: 1, diff --git a/website/src/pages/circle-packing/html.js b/website/src/pages/circle-packing/html.js index 381b44e66..1eb00e94c 100644 --- a/website/src/pages/circle-packing/html.js +++ b/website/src/pages/circle-packing/html.js @@ -17,6 +17,7 @@ const initialProperties = { }, identity: 'name', value: 'loc', + valueFormat: { format: '', enabled: false }, colors: { scheme: 'spectral' }, colorBy: 'depth', //childColor: { diff --git a/website/src/pages/circle-packing/index.js b/website/src/pages/circle-packing/index.js index b8ff940ba..9e6ee4a69 100644 --- a/website/src/pages/circle-packing/index.js +++ b/website/src/pages/circle-packing/index.js @@ -16,6 +16,7 @@ const initialProperties = { }, id: 'name', value: 'loc', + valueFormat: { format: '', enabled: false }, colors: { scheme: 'nivo' }, colorBy: 'depth', childColor: 'noinherit',