Skip to content

Commit

Permalink
feat(circle-packing): rename Bubble to CirclePacking
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc authored and wyze committed Apr 26, 2021
1 parent 6fb27f7 commit 43bb075
Show file tree
Hide file tree
Showing 26 changed files with 409 additions and 264 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ please use the [components explorer](http://nivo.rocks/components).
Components available through the HTTP rendering API.

- [Bar](https://nivo-api.herokuapp.com/samples/bar.svg)
- [Bubble](https://nivo-api.herokuapp.com/samples/bubble.svg)
- [CirclePacking](https://nivo-api.herokuapp.com/samples/circle-packing.svg)
- [Chord](https://nivo-api.herokuapp.com/samples/chord.svg)
- [HeatMap](https://nivo-api.herokuapp.com/samples/heatmap.svg)
- [Line](https://nivo-api.herokuapp.com/samples/line.svg)
Expand Down
18 changes: 9 additions & 9 deletions packages/circle-packing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@

[![version](https://img.shields.io/npm/v/@nivo/circle-packing.svg?style=flat-square)](https://www.npmjs.com/package/@nivo/circle-packing)

## Bubble
## CirclePacking

[documentation](http://nivo.rocks/bubble)
[documentation](http://nivo.rocks/circle-pack)

![Bubble](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/bubble.png)
![CirclePacking](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/circle-packing.png)

## BubbleHtml
## CirclePackingHtml

[documentation](http://nivo.rocks/bubble/html)
[documentation](http://nivo.rocks/circle-pack/html)

![BubbleHtml](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/bubble-html.png)
![CirclePackingHtml](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/circle-packing-html.png)

## BubbleCanvas
## CirclePackingCanvas

[documentation](http://nivo.rocks/bubble/canvas)
[documentation](http://nivo.rocks/circle-packing/canvas)

![BubbleCanvas](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/bubble-canvas.png)
![CirclePackingCanvas](https://raw.githubusercontent.com/plouc/nivo/master/packages/circle-packing/doc/circle-packing-canvas.png)
File renamed without changes
1 change: 1 addition & 0 deletions packages/circle-packing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"d3-hierarchy": "^1.1.8",
"lodash": "^4.17.11",
"react-motion": "^0.5.2",
"react-spring": "9.0.0-rc.3",
"recompose": "^0.30.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,40 @@ import {
useDimensions,
Container,
SvgWrapper,
usePropertyAccessor,
} from '@nivo/core'
import { CirclePackLayerId, CirclePackSvgProps } from './types'
import { useCirclePack } from './hooks'
import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors'
import { CirclePackLayerId, CirclePackSvgProps, ComputedDatum } from './types'
import { useCirclePacking } from './hooks'
import { Circles } from './Circles'

const defaultProps = {
id: 'id',
value: 'value',

padding: 0,
layers: ['circles', 'labels'] as CirclePackLayerId[],

colors: { scheme: 'nivo' } as OrdinalColorScaleConfig,
childColor: {
from: 'color',
modifiers: [['darker', 0.3]],
},
isInteractive: true,

animate: true,
motionConfig: 'gentle',

role: 'img',
}

const InnerCirclePack = <RawDatum,>({
const InnerCirclePacking = <RawDatum,>({
data,
id = defaultProps.id,
value = defaultProps.value,
valueFormat,

width,
height,
margin: partialMargin,

padding = defaultProps.padding,
colors = defaultProps.colors,
childColor = defaultProps.childColor as InheritedColorConfig<ComputedDatum<RawDatum>>,
layers = defaultProps.layers,

role = defaultProps.role,
}: Partial<CirclePackSvgProps<RawDatum>>) => {
const { outerWidth, outerHeight, margin, innerWidth, innerHeight } = useDimensions(
Expand All @@ -44,16 +47,26 @@ const InnerCirclePack = <RawDatum,>({
partialMargin
)

useCirclePack<RawDatum>({
const nodes = useCirclePacking<RawDatum>({
data,
id,
value,
width: innerWidth,
height: innerHeight,
padding,
colors,
childColor,
})

const layerById: Record<CirclePackLayerId, ReactNode> = {
circles: null,
labels: null,
}

if (layers.includes('circles')) {
layerById.circles = <Circles key="circles" data={nodes} />
}

const layerContext = {}

return (
Expand All @@ -79,7 +92,7 @@ const InnerCirclePack = <RawDatum,>({
)
}

export const CirclePack = <RawDatum,>({
export const CirclePacking = <RawDatum,>({
isInteractive = defaultProps.isInteractive,
animate = defaultProps.animate,
motionConfig = defaultProps.motionConfig,
Expand All @@ -92,6 +105,6 @@ export const CirclePack = <RawDatum,>({
motionConfig={motionConfig}
theme={theme}
>
<InnerCirclePack<RawDatum> isInteractive={isInteractive} {...otherProps} />
<InnerCirclePacking<RawDatum> isInteractive={isInteractive} {...otherProps} />
</Container>
)
Empty file.
Empty file.
77 changes: 77 additions & 0 deletions packages/circle-packing/src/Circles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react'
import { useTransition, animated, to, SpringValue } from 'react-spring'
import { useMotionConfig } from '@nivo/core'
import { DatumWithChildren, ComputedDatum } from './types'

/**
* A negative radius value is invalid for an SVG circle,
* this custom interpolation makes sure it's either
* positive or zero.
*/
const interpolateRadius = (radiusValue: SpringValue<number>) =>
to([radiusValue], radius => Math.max(0, radius))

export const Circles = <RawDatum extends DatumWithChildren<RawDatum>>({ data }: any) => {
const { animate, config: springConfig } = useMotionConfig()

const enter = (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: 0,
color: node.color,
opacity: 0,
})

const update = (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: node.radius,
color: node.color,
opacity: 1,
})

const leave = (node: ComputedDatum<RawDatum>) => ({
x: node.x,
y: node.y,
radius: 0,
color: node.color,
opacity: 0,
})

const transition = useTransition<
ComputedDatum<RawDatum>,
{
x: number
y: number
radius: number
color: string
opacity: number
}
>(data, {
key: datum => datum.id,
initial: update,
from: enter,
enter: update,
update,
leave,
config: springConfig,
immediate: !animate,
})

return (
<g>
{transition((transitionProps, datum) => {
return (
<animated.circle
key={datum.id}
cx={transitionProps.x}
cy={transitionProps.y}
r={interpolateRadius(transitionProps.radius)}
fill={transitionProps.color}
opacity={transitionProps.opacity}
/>
)
})}
</g>
)
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react'
import { ResponsiveWrapper } from '@nivo/core'
import { CirclePack } from './CirclePack'
import { CirclePacking } from './CirclePacking'
import { CirclePackSvgProps } from './types'

export const ResponsiveCirclePack = <RawDatum,>(
export const ResponsiveCirclePacking = <RawDatum,>(
props: Omit<CirclePackSvgProps<RawDatum>, 'width' | 'height'>
) => (
<ResponsiveWrapper>
{({ width, height }: { width: number; height: number }) => (
<CirclePack<RawDatum> width={width} height={height} {...props} />
<CirclePacking<RawDatum> width={width} height={height} {...props} />
)}
</ResponsiveWrapper>
)
87 changes: 80 additions & 7 deletions packages/circle-packing/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,94 @@
import { pack as d3Pack, hierarchy as d3Hierarchy } from 'd3-hierarchy'
import cloneDeep from 'lodash/cloneDeep'
import sortBy from 'lodash/sortBy'
import { usePropertyAccessor } from '@nivo/core'
import { DatumWithChildren, CirclePackSvgProps } from './types'
import { useInheritedColor, useOrdinalColorScale } from '@nivo/colors'
import { DatumWithChildren, CirclePackSvgProps, ComputedDatum } from './types'

export const useCirclePack = <RawDatum extends DatumWithChildren<RawDatum>>({
export const useCirclePacking = <RawDatum extends DatumWithChildren<RawDatum>>({
data,
id,
value,
width,
height,
padding,
colors,
childColor,
}: {
data: RawDatum
id: CirclePackSvgProps<RawDatum>['id']
value: CirclePackSvgProps<RawDatum>['value']
width: number
height: number
padding: CirclePackSvgProps<RawDatum>['padding']
colors: CirclePackSvgProps<RawDatum>['colors']
childColor: CirclePackSvgProps<RawDatum>['childColor']
}) => {
const getId = usePropertyAccessor<RawDatum, string | number>(id)
const getValue = usePropertyAccessor<RawDatum, number>(value)

console.log({
data,
getId,
getValue,
})
const getColor = useOrdinalColorScale<Omit<ComputedDatum<RawDatum>, 'color' | 'fill'>>(
colors,
'id'
)
const getChildColor = useInheritedColor<ComputedDatum<RawDatum>>(childColor)

// d3 mutates the data for performance reasons,
// however it does not work well with reactive programming,
// this ensures that we don't mutate the input data
const clonedData = cloneDeep(data)

const hierarchy = d3Hierarchy(clonedData).sum(getValue)

const pack = d3Pack().size([width, height]).padding(padding)

const packedData = pack(hierarchy)

// let nodes = leavesOnly ? root.leaves() : root.descendants()
const nodes = packedData.descendants()

// It's important to sort node by depth,
// it ensures that we assign a parent node
// which has already been computed, because parent nodes
// are gonna be computed first
const sortedNodes = sortBy(nodes, 'depth')

const total = hierarchy.value ?? 0

const computedNodes = sortedNodes.reduce<ComputedDatum<RawDatum>>((acc, descendant) => {
const id = getId(descendant.data)
const value = descendant.value!
const percentage = (100 * value) / total
const path = descendant.ancestors().map(ancestor => getId(ancestor.data))

let parent: ComputedDatum<RawDatum> | undefined
if (descendant.parent) {
parent = acc.find(node => node.id === getId(descendant.parent!.data))
}

const normalizedNode: ComputedDatum<RawDatum> = {
id,
path,
value,
percentage,
//formattedValue: valueFormat ? formatValue(value) : `${percentage.toFixed(2)}%`,
x: descendant.x,
y: descendant.y,
radius: descendant.r,
color: '',
data: descendant.data,
depth: descendant.depth,
height: descendant.height,
}

if (childColor !== 'noinherit' && parent && normalizedNode.depth > 1) {
normalizedNode.color = getChildColor(parent)
} else {
normalizedNode.color = getColor(normalizedNode)
}

return [...acc, normalizedNode]
}, [])

return computedNodes
}
4 changes: 2 additions & 2 deletions packages/circle-packing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ export { default as BubbleCanvas } from './BubbleCanvas'
export { default as ResponsiveBubbleCanvas } from './ResponsiveBubbleCanvas'
export * from './props'

export * from './CirclePack'
export * from './ResponsiveCirclePack'
export * from './CirclePacking'
export * from './ResponsiveCirclePacking'
export * from './types'
Loading

0 comments on commit 43bb075

Please sign in to comment.