Skip to content

Commit 458ba58

Browse files
committedJan 1, 2022
feat(chord): migrate ribbons and arcs transitions to react-spring
1 parent 16c316b commit 458ba58

15 files changed

+382
-410
lines changed
 

‎packages/chord/_old_index.d.ts

-108
This file was deleted.

‎packages/chord/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@nivo/colors": "0.77.0",
3434
"@nivo/legends": "0.77.0",
3535
"@nivo/tooltip": "0.77.0",
36+
"@react-spring/web": "9.3.1",
3637
"d3-chord": "^1.0.6",
3738
"d3-shape": "^1.3.5",
3839
"lodash": "^4.17.21",

‎packages/chord/src/Chord.tsx

+2-4
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,7 @@ const InnerChord = ({
9797
})
9898

9999
const theme = useTheme()
100-
const getLabelTextColor = useInheritedColor(labelTextColor, theme)
101100
const getArcBorderColor = useInheritedColor(arcBorderColor, theme)
102-
const getRibbonBorderColor = useInheritedColor(ribbonBorderColor, theme)
103101

104102
const customLayerProps = useCustomLayerProps({
105103
center,
@@ -132,7 +130,7 @@ const InnerChord = ({
132130
ribbons={ribbons}
133131
ribbonGenerator={ribbonGenerator}
134132
borderWidth={ribbonBorderWidth}
135-
getBorderColor={getRibbonBorderColor}
133+
borderColor={ribbonBorderColor}
136134
getOpacity={getRibbonOpacity}
137135
blendMode={ribbonBlendMode}
138136
setCurrent={setCurrentRibbon}
@@ -175,7 +173,7 @@ const InnerChord = ({
175173
arcs={arcs}
176174
radius={radius + labelOffset}
177175
rotation={labelRotation}
178-
getColor={getLabelTextColor}
176+
color={labelTextColor}
179177
/>
180178
</g>
181179
)

‎packages/chord/src/ChordArc.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { createElement, memo, useMemo, MouseEvent } from 'react'
22
import { useTooltip } from '@nivo/tooltip'
3-
import { ArcDatum, ChordCommonProps } from './types'
3+
import { ArcDatum, ArcGenerator, ChordCommonProps } from './types'
44

55
interface ChordArcProps {
66
arc: ArcDatum
77
startAngle: number
88
endAngle: number
9-
arcGenerator: any
9+
arcGenerator: ArcGenerator
1010
borderWidth: number
1111
getBorderColor: (arc: ArcDatum) => string
1212
opacity: number
@@ -75,7 +75,7 @@ export const ChordArc = memo(
7575

7676
return (
7777
<path
78-
d={arcGenerator({ startAngle, endAngle })}
78+
d={arcGenerator({ startAngle, endAngle }) || ''}
7979
fill={arc.color}
8080
fillOpacity={opacity}
8181
strokeWidth={borderWidth}

‎packages/chord/src/ChordArcs.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { TransitionMotion, spring } from 'react-motion'
33
import { interpolateColor } from '@nivo/colors'
44
import { useMotionConfig } from '@nivo/core'
55
import { ChordArc } from './ChordArc'
6-
import { ArcDatum, ChordCommonProps } from './types'
6+
import { ArcDatum, ArcGenerator, ChordCommonProps } from './types'
77

88
interface ChordArcsProps {
99
arcs: ArcDatum[]
10-
arcGenerator: any
10+
arcGenerator: ArcGenerator
1111
borderWidth: ChordCommonProps['arcBorderWidth']
1212
getBorderColor: (arc: ArcDatum) => string
1313
getOpacity: (arc: ArcDatum) => number

‎packages/chord/src/ChordCanvas.tsx

+5-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { createElement, useRef, useEffect, useCallback, MouseEvent } from 'react
22
import {
33
useDimensions,
44
useTheme,
5+
// @ts-ignore
56
midAngle,
7+
// @ts-ignore
68
getPolarLabelProps,
79
degreesToRadians,
810
getRelativeCursor,
@@ -38,7 +40,7 @@ const getArcFromMouseEvent = ({
3840
const centerX = margin.left + center[0]
3941
const centerY = margin.top + center[1]
4042

41-
return findArcUnderCursor(centerX, centerY, radius, innerRadius, arcs, x, y)
43+
return findArcUnderCursor(centerX, centerY, radius, innerRadius, arcs as any[], x, y)
4244
}
4345

4446
type InnerChordCanvasProps = Omit<ChordCanvasProps, 'renderWrapper' | 'theme'>
@@ -156,10 +158,7 @@ const InnerChordCanvas = ({
156158
ctx.fill()
157159

158160
if (ribbonBorderWidth > 0) {
159-
ctx.strokeStyle = getRibbonBorderColor({
160-
...ribbon,
161-
color: ribbon.source.color,
162-
})
161+
ctx.strokeStyle = getRibbonBorderColor(ribbon.source)
163162
ctx.lineWidth = ribbonBorderWidth
164163
ctx.stroke()
165164
}
@@ -214,7 +213,7 @@ const InnerChordCanvas = ({
214213

215214
ctx.textAlign = props.align
216215
ctx.textBaseline = props.baseline
217-
ctx.fillStyle = getLabelTextColor(arc, theme)
216+
ctx.fillStyle = getLabelTextColor(arc)
218217
ctx.fillText(arc.label, 0, 0)
219218

220219
ctx.restore()

‎packages/chord/src/ChordLabels.tsx

+99-69
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,116 @@
1-
import { memo } from 'react'
2-
import { TransitionMotion, spring } from 'react-motion'
3-
import { midAngle, getPolarLabelProps, useTheme } from '@nivo/core'
1+
import { memo, useMemo } from 'react'
2+
import { useTransition, animated, to } from '@react-spring/web'
3+
import {
4+
// @ts-ignore
5+
midAngle,
6+
// @ts-ignore
7+
getPolarLabelProps,
8+
useTheme,
9+
} from '@nivo/core'
410
import { useMotionConfig } from '@nivo/core'
5-
import { ArcDatum } from './types'
11+
import { ArcDatum, ChordCommonProps } from './types'
12+
import { useInheritedColor } from '@nivo/colors'
613

714
interface ChordLabelsProps {
815
arcs: ArcDatum[]
916
radius: number
1017
rotation: number
11-
getColor: (arc: ArcDatum) => string
18+
color: ChordCommonProps['labelTextColor']
1219
}
1320

14-
export const ChordLabels = memo(({ arcs, radius, rotation, getColor }: ChordLabelsProps) => {
15-
const theme = useTheme()
16-
const { animate, springConfig } = useMotionConfig()
17-
18-
if (animate) {
19-
return (
20-
<>
21-
{arcs.map(arc => {
22-
const color = getColor(arc, theme)
23-
const angle = midAngle(arc)
24-
const textProps = getPolarLabelProps(radius, angle, rotation)
21+
export const ChordLabels = memo(({ arcs, radius, rotation, color }: ChordLabelsProps) => {
22+
const { animate, config: springConfig } = useMotionConfig()
2523

26-
return (
27-
<text
28-
key={arc.id}
29-
transform={`translate(${textProps.x}, ${textProps.y}) rotate(${textProps.rotate})`}
30-
style={{
31-
...theme.labels.text,
32-
pointerEvents: 'none',
33-
fill: color,
34-
}}
35-
textAnchor={textProps.align}
36-
dominantBaseline={textProps.baseline}
37-
>
38-
{arc.label}
39-
</text>
40-
)
41-
})}
42-
</>
43-
)
44-
}
24+
const theme = useTheme()
25+
const getColor = useInheritedColor(color, theme)
4526

46-
return (
47-
<TransitionMotion
48-
styles={arcs.map(arc => {
27+
const labels = useMemo(
28+
() =>
29+
arcs.map(arc => {
4930
const angle = midAngle(arc)
31+
const textProps = getPolarLabelProps(radius, angle, rotation)
5032

5133
return {
52-
key: arc.id,
53-
data: arc,
54-
style: {
55-
angle: spring(angle, springConfig),
56-
},
34+
id: arc.id,
35+
label: arc.label,
36+
x: textProps.x,
37+
y: textProps.y,
38+
rotation: textProps.rotate,
39+
color: getColor(arc),
40+
textAnchor: textProps.align,
41+
dominantBaseline: textProps.baseline,
5742
}
58-
})}
59-
>
60-
{interpolatedStyles => (
61-
<>
62-
{interpolatedStyles.map(({ key, style, data: arc }) => {
63-
const color = getColor(arc, theme)
64-
const textProps = getPolarLabelProps(radius, style.angle, rotation)
43+
}),
44+
[arcs, radius, radius, rotation, getColor]
45+
)
46+
47+
const transition = useTransition<
48+
typeof labels[number],
49+
{
50+
x: number
51+
y: number
52+
rotation: number
53+
color: string
54+
}
55+
>(labels, {
56+
keys: label => label.id,
57+
initial: label => {
58+
return {
59+
x: label.x,
60+
y: label.y,
61+
rotation: label.rotation,
62+
color: label.color,
63+
}
64+
},
65+
from: label => {
66+
return {
67+
x: label.x,
68+
y: label.y,
69+
rotation: label.rotation,
70+
color: label.color,
71+
}
72+
},
73+
enter: label => {
74+
return {
75+
x: label.x,
76+
y: label.y,
77+
rotation: label.rotation,
78+
color: label.color,
79+
}
80+
},
81+
update: label => {
82+
return {
83+
x: label.x,
84+
y: label.y,
85+
rotation: label.rotation,
86+
color: label.color,
87+
}
88+
},
89+
expires: true,
90+
config: springConfig,
91+
immediate: !animate,
92+
})
6593

66-
return (
67-
<text
68-
key={key}
69-
transform={`translate(${textProps.x}, ${textProps.y}) rotate(${textProps.rotate})`}
70-
style={{
71-
...theme.labels.text,
72-
pointerEvents: 'none',
73-
fill: color,
74-
}}
75-
textAnchor={textProps.align}
76-
dominantBaseline={textProps.baseline}
77-
>
78-
{arc.label}
79-
</text>
80-
)
81-
})}
82-
</>
83-
)}
84-
</TransitionMotion>
94+
return (
95+
<>
96+
{transition((transitionProps, label) => (
97+
<animated.text
98+
key={label.id}
99+
style={{
100+
...theme.labels.text,
101+
pointerEvents: 'none',
102+
fill: transitionProps.color,
103+
}}
104+
transform={to(
105+
[transitionProps.x, transitionProps.y, transitionProps.rotation],
106+
(x, y, rotation) => `translate(${x}, ${y}) rotate(${rotation})`
107+
)}
108+
textAnchor={label.textAnchor}
109+
dominantBaseline={label.dominantBaseline}
110+
>
111+
{label.label}
112+
</animated.text>
113+
))}
114+
</>
85115
)
86116
})

‎packages/chord/src/ChordRibbon.tsx

+23-31
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import { createElement, memo, useMemo, MouseEvent } from 'react'
2+
import { SpringValues, animated } from '@react-spring/web'
23
import { useTooltip } from '@nivo/tooltip'
3-
import { ChordCommonProps, ChordSvgProps, RibbonDatum } from './types'
4+
import {
5+
ChordCommonProps,
6+
ChordSvgProps,
7+
RibbonAnimatedProps,
8+
RibbonDatum,
9+
RibbonGenerator,
10+
} from './types'
11+
import { computeRibbonPath } from './compute'
412

513
interface ChordRibbonProps {
614
ribbon: RibbonDatum
7-
ribbonGenerator: any
8-
sourceStartAngle: number
9-
sourceEndAngle: number
10-
targetStartAngle: number
11-
targetEndAngle: number
12-
color: string
15+
ribbonGenerator: RibbonGenerator
16+
animatedProps: SpringValues<RibbonAnimatedProps>
17+
borderWidth: ChordCommonProps['ribbonBorderWidth']
1318
blendMode: NonNullable<ChordSvgProps['ribbonBlendMode']>
14-
opacity: number
15-
borderWidth: number
16-
getBorderColor: (ribbon: RibbonDatum) => string
1719
setCurrent: (ribbon: RibbonDatum | null) => void
1820
isInteractive: ChordCommonProps['isInteractive']
1921
tooltip: NonNullable<ChordSvgProps['ribbonTooltip']>
@@ -27,14 +29,8 @@ export const ChordRibbon = memo(
2729
({
2830
ribbon,
2931
ribbonGenerator,
30-
sourceStartAngle,
31-
sourceEndAngle,
32-
targetStartAngle,
33-
targetEndAngle,
34-
color,
35-
opacity,
32+
animatedProps,
3633
borderWidth,
37-
getBorderColor,
3834
blendMode,
3935
isInteractive,
4036
setCurrent,
@@ -82,22 +78,18 @@ export const ChordRibbon = memo(
8278
}, [isInteractive, ribbon, onClick])
8379

8480
return (
85-
<path
86-
d={ribbonGenerator({
87-
source: {
88-
startAngle: sourceStartAngle,
89-
endAngle: sourceEndAngle,
90-
},
91-
target: {
92-
startAngle: targetStartAngle,
93-
endAngle: targetEndAngle,
94-
},
81+
<animated.path
82+
d={computeRibbonPath({
83+
sourceStartAngle: animatedProps.sourceStartAngle,
84+
sourceEndAngle: animatedProps.sourceEndAngle,
85+
targetStartAngle: animatedProps.targetStartAngle,
86+
targetEndAngle: animatedProps.targetEndAngle,
87+
ribbonGenerator,
9588
})}
96-
fill={color}
97-
fillOpacity={opacity}
89+
fill={animatedProps.color}
90+
opacity={animatedProps.opacity}
9891
strokeWidth={borderWidth}
99-
stroke={getBorderColor({ ...ribbon, color })}
100-
strokeOpacity={opacity}
92+
stroke={animatedProps.borderColor}
10193
style={{ mixBlendMode: blendMode }}
10294
onMouseEnter={handleMouseEnter}
10395
onMouseMove={handleMouseMove}

‎packages/chord/src/ChordRibbons.tsx

+82-133
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,31 @@
11
import { memo } from 'react'
2-
import mapValues from 'lodash/mapValues'
3-
import { TransitionMotion, spring } from 'react-motion'
4-
import { midAngle, useMotionConfig } from '@nivo/core'
5-
import { interpolateColor, getInterpolatedColor } from '@nivo/colors'
2+
import { useTransition } from '@react-spring/web'
3+
import {
4+
useTheme,
5+
// @ts-ignore
6+
midAngle,
7+
useMotionConfig,
8+
} from '@nivo/core'
9+
import { useInheritedColor } from '@nivo/colors'
610
import { ChordRibbon } from './ChordRibbon'
7-
import { ChordCommonProps, ChordSvgProps, RibbonDatum } from './types'
11+
import {
12+
ChordCommonProps,
13+
ChordSvgProps,
14+
RibbonDatum,
15+
RibbonAnimatedProps,
16+
RibbonGenerator,
17+
ArcDatum,
18+
} from './types'
819

920
/**
1021
* Used to get ribbon angles, instead of using source and target arcs,
1122
* we sort arcs by value to have smooth transitions, otherwise,
1223
* if source|target arc value becomes greater than the other,
1324
* the ribbon will be reversed.
14-
*
15-
* @param {Object} source
16-
* @param {Object} target
17-
* @param {boolean} useMiddleAngle
18-
* @param {Object} [springConfig]
19-
* @return {Object}
2025
*/
21-
const getRibbonAngles = ({ source, target }, useMiddleAngle, springConfig) => {
22-
let firstArc
23-
let secondArc
26+
const getRibbonAngles = ({ source, target }: RibbonDatum, useMiddleAngle: boolean) => {
27+
let firstArc: ArcDatum
28+
let secondArc: ArcDatum
2429
if (source.startAngle < target.startAngle) {
2530
firstArc = source
2631
secondArc = target
@@ -29,50 +34,31 @@ const getRibbonAngles = ({ source, target }, useMiddleAngle, springConfig) => {
2934
secondArc = source
3035
}
3136

32-
let angles
33-
if (useMiddleAngle === true) {
37+
if (useMiddleAngle) {
3438
const firstMiddleAngle = midAngle(firstArc)
3539
const secondMiddleAngle = midAngle(secondArc)
3640

37-
angles = {
41+
return {
3842
sourceStartAngle: firstMiddleAngle,
3943
sourceEndAngle: firstMiddleAngle,
4044
targetStartAngle: secondMiddleAngle,
4145
targetEndAngle: secondMiddleAngle,
4246
}
43-
} else {
44-
angles = {
45-
sourceStartAngle: firstArc.startAngle,
46-
sourceEndAngle: firstArc.endAngle,
47-
targetStartAngle: secondArc.startAngle,
48-
targetEndAngle: secondArc.endAngle,
49-
}
5047
}
5148

52-
if (!springConfig) return angles
53-
54-
return mapValues(angles, angle => spring(angle, springConfig))
49+
return {
50+
sourceStartAngle: firstArc.startAngle,
51+
sourceEndAngle: firstArc.endAngle,
52+
targetStartAngle: secondArc.startAngle,
53+
targetEndAngle: secondArc.endAngle,
54+
}
5555
}
5656

57-
const ribbonWillEnter = ({ data: ribbon }) => ({
58-
...getRibbonAngles(ribbon, true),
59-
opacity: 0,
60-
...interpolateColor(ribbon.source.color),
61-
})
62-
63-
const ribbonWillLeave =
64-
springConfig =>
65-
({ data: ribbon }) => ({
66-
...getRibbonAngles(ribbon, true, springConfig),
67-
opacity: 0,
68-
...interpolateColor(ribbon.source.color, springConfig),
69-
})
70-
7157
interface ChordRibbonsProps {
7258
ribbons: RibbonDatum[]
73-
ribbonGenerator: any
59+
ribbonGenerator: RibbonGenerator
7460
borderWidth: ChordCommonProps['ribbonBorderWidth']
75-
getBorderColor: (ribbon: RibbonDatum) => string
61+
borderColor: ChordCommonProps['ribbonBorderColor']
7662
getOpacity: (ribbon: RibbonDatum) => number
7763
blendMode: NonNullable<ChordSvgProps['ribbonBlendMode']>
7864
isInteractive: ChordCommonProps['isInteractive']
@@ -89,7 +75,7 @@ export const ChordRibbons = memo(
8975
ribbons,
9076
ribbonGenerator,
9177
borderWidth,
92-
getBorderColor,
78+
borderColor,
9379
getOpacity,
9480
blendMode,
9581
isInteractive,
@@ -100,99 +86,62 @@ export const ChordRibbons = memo(
10086
onClick,
10187
tooltip,
10288
}: ChordRibbonsProps) => {
103-
const { animate, springConfig: _springConfig } = useMotionConfig()
89+
const { animate, config: springConfig } = useMotionConfig()
10490

105-
if (animate !== true) {
106-
return (
107-
<g>
108-
{ribbons.map(ribbon => {
109-
return (
110-
<ChordRibbon
111-
key={ribbon.id}
112-
ribbon={ribbon}
113-
ribbonGenerator={ribbonGenerator}
114-
sourceStartAngle={ribbon.source.startAngle}
115-
sourceEndAngle={ribbon.source.endAngle}
116-
targetStartAngle={ribbon.target.startAngle}
117-
targetEndAngle={ribbon.target.endAngle}
118-
color={ribbon.source.color}
119-
blendMode={blendMode}
120-
opacity={getOpacity(ribbon)}
121-
borderWidth={borderWidth}
122-
getBorderColor={getBorderColor}
123-
isInteractive={isInteractive}
124-
setCurrent={setCurrent}
125-
onMouseEnter={onMouseEnter}
126-
onMouseMove={onMouseMove}
127-
onMouseLeave={onMouseLeave}
128-
onClick={onClick}
129-
tooltip={tooltip}
130-
/>
131-
)
132-
})}
133-
</g>
134-
)
135-
}
91+
const theme = useTheme()
92+
const getBorderColor = useInheritedColor(borderColor, theme)
13693

137-
const springConfig = {
138-
..._springConfig,
139-
precision: 0.001,
140-
}
94+
const transition = useTransition<RibbonDatum, RibbonAnimatedProps>(ribbons, {
95+
keys: ribbon => ribbon.id,
96+
initial: ribbon => ({
97+
...getRibbonAngles(ribbon, false),
98+
color: ribbon.source.color,
99+
opacity: getOpacity(ribbon),
100+
borderColor: getBorderColor(ribbon.source),
101+
}),
102+
from: ribbon => ({
103+
...getRibbonAngles(ribbon, false),
104+
color: ribbon.source.color,
105+
opacity: 0,
106+
borderColor: getBorderColor(ribbon.source),
107+
}),
108+
update: ribbon => ({
109+
...getRibbonAngles(ribbon, false),
110+
color: ribbon.source.color,
111+
opacity: getOpacity(ribbon),
112+
borderColor: getBorderColor(ribbon.source),
113+
}),
114+
leave: ribbon => ({
115+
...getRibbonAngles(ribbon, false),
116+
color: ribbon.source.color,
117+
opacity: 0,
118+
borderColor: getBorderColor(ribbon.source),
119+
}),
120+
expires: true,
121+
config: springConfig,
122+
immediate: !animate,
123+
})
141124

142125
return (
143-
<TransitionMotion
144-
willEnter={ribbonWillEnter}
145-
willLeave={ribbonWillLeave(springConfig)}
146-
styles={ribbons.map(ribbon => {
147-
return {
148-
key: ribbon.id,
149-
data: ribbon,
150-
style: {
151-
...getRibbonAngles(ribbon, false, springConfig),
152-
opacity: spring(getOpacity(ribbon), springConfig),
153-
...interpolateColor(ribbon.source.color, springConfig),
154-
},
155-
}
156-
})}
157-
>
158-
{interpolatedStyles => (
159-
<>
160-
{interpolatedStyles.map(({ key, style, data: ribbon }) => {
161-
const color = getInterpolatedColor(style)
162-
163-
return (
164-
<ChordRibbon
165-
key={key}
166-
ribbon={ribbon}
167-
ribbonGenerator={ribbonGenerator}
168-
sourceStartAngle={style.sourceStartAngle}
169-
sourceEndAngle={Math.max(
170-
style.sourceEndAngle,
171-
style.sourceStartAngle
172-
)}
173-
targetStartAngle={style.targetStartAngle}
174-
targetEndAngle={Math.max(
175-
style.targetEndAngle,
176-
style.targetStartAngle
177-
)}
178-
color={color}
179-
blendMode={blendMode}
180-
opacity={style.opacity}
181-
borderWidth={borderWidth}
182-
getBorderColor={getBorderColor}
183-
isInteractive={isInteractive}
184-
setCurrent={setCurrent}
185-
onMouseEnter={onMouseEnter}
186-
onMouseMove={onMouseMove}
187-
onMouseLeave={onMouseLeave}
188-
onClick={onClick}
189-
tooltip={tooltip}
190-
/>
191-
)
192-
})}
193-
</>
194-
)}
195-
</TransitionMotion>
126+
<>
127+
{transition((animatedProps, ribbon) => (
128+
<ChordRibbon
129+
key={ribbon.id}
130+
ribbon={ribbon}
131+
ribbonGenerator={ribbonGenerator}
132+
animatedProps={animatedProps}
133+
borderWidth={borderWidth}
134+
blendMode={blendMode}
135+
setCurrent={setCurrent}
136+
isInteractive={isInteractive}
137+
tooltip={tooltip}
138+
onMouseEnter={onMouseEnter}
139+
onMouseMove={onMouseMove}
140+
onMouseLeave={onMouseLeave}
141+
onClick={onClick}
142+
/>
143+
))}
144+
</>
196145
)
197146
}
198147
)

‎packages/chord/src/compute.ts

+94-29
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import { to, SpringValues } from '@react-spring/web'
12
import { arc as d3Arc } from 'd3-shape'
23
import { chord as d3Chord, ChordLayout, ribbon as d3Ribbon } from 'd3-chord'
3-
import { ArcDatum, ChordCommonProps, ChordDataProps } from './types'
4+
import {
5+
ArcDatum,
6+
ChordCommonProps,
7+
ChordDataProps,
8+
RibbonAnimatedProps,
9+
RibbonDatum,
10+
RibbonGenerator,
11+
ArcGenerator,
12+
} from './types'
413
import { OrdinalColorScale } from '@nivo/colors'
514

615
export const computeChordLayout = ({ padAngle }: { padAngle: ChordCommonProps['padAngle'] }) =>
@@ -17,16 +26,20 @@ export const computeChordGenerators = ({
1726
innerRadiusRatio: ChordCommonProps['innerRadiusRatio']
1827
innerRadiusOffset: ChordCommonProps['innerRadiusOffset']
1928
}) => {
20-
const center = [width / 2, height / 2]
29+
const center: [number, number] = [width / 2, height / 2]
2130
const radius = Math.min(width, height) / 2
2231
const innerRadius = radius * innerRadiusRatio
2332
const ribbonRadius = radius * (innerRadiusRatio - innerRadiusOffset)
2433

25-
const arcGenerator = d3Arc().outerRadius(radius).innerRadius(innerRadius)
26-
27-
const ribbonGenerator = d3Ribbon().radius(ribbonRadius)
28-
29-
return { center, radius, innerRadius, arcGenerator, ribbonGenerator }
34+
return {
35+
center,
36+
radius,
37+
innerRadius,
38+
arcGenerator: d3Arc()
39+
.outerRadius(radius)
40+
.innerRadius(innerRadius) as unknown as ArcGenerator,
41+
ribbonGenerator: d3Ribbon().radius(ribbonRadius) as unknown as RibbonGenerator,
42+
}
3043
}
3144

3245
export const computeChordArcsAndRibbons = ({
@@ -40,35 +53,87 @@ export const computeChordArcsAndRibbons = ({
4053
chord: ChordLayout
4154
data: ChordDataProps['data']
4255
keys: ChordDataProps['keys']
43-
getLabel: (arc: ArcDatum) => string
44-
formatValue: (valuee: number) => string
45-
getColor: OrdinalColorScale<any>
46-
}) => {
47-
const ribbons = chord(data)
56+
getLabel: (arc: Omit<ArcDatum, 'label' | 'color'>) => string
57+
formatValue: (value: number) => string
58+
getColor: OrdinalColorScale<Omit<ArcDatum, 'label' | 'color'>>
59+
}): {
60+
arcs: ArcDatum[]
61+
ribbons: RibbonDatum[]
62+
} => {
63+
const _ribbons = chord(data)
4864

49-
const arcs = ribbons.groups.map(arc => {
50-
arc.id = keys[arc.index]
51-
arc.color = getColor(arc)
52-
arc.formattedValue = formatValue(arc.value)
53-
arc.label = getLabel(arc)
65+
const arcs: ArcDatum[] = _ribbons.groups.map(chordGroup => {
66+
const arc: Omit<ArcDatum, 'label' | 'color'> = {
67+
...chordGroup,
68+
id: keys[chordGroup.index],
69+
formattedValue: formatValue(chordGroup.value),
70+
}
5471

55-
return arc
72+
return {
73+
...arc,
74+
label: getLabel(arc),
75+
color: getColor(arc),
76+
}
5677
})
5778

58-
ribbons.forEach(ribbon => {
59-
ribbon.source.id = keys[ribbon.source.index]
60-
ribbon.source.color = getColor(ribbon.source)
61-
ribbon.source.formattedValue = formatValue(ribbon.source.value)
62-
ribbon.source.label = getLabel(ribbon.source)
79+
const ribbons: RibbonDatum[] = _ribbons.map(_ribbon => {
80+
const source = {
81+
..._ribbon.source,
82+
id: keys[_ribbon.source.index],
83+
formattedValue: formatValue(_ribbon.source.value),
84+
}
6385

64-
ribbon.target.id = keys[ribbon.target.index]
65-
ribbon.target.color = getColor(ribbon.target)
66-
ribbon.target.formattedValue = formatValue(ribbon.target.value)
67-
ribbon.target.label = getLabel(ribbon.target)
86+
const target = {
87+
..._ribbon.target,
88+
id: keys[_ribbon.target.index],
89+
formattedValue: formatValue(_ribbon.target.value),
90+
}
6891

69-
// ensure id remains the same even if source/target are reversed
70-
ribbon.id = [ribbon.source.id, ribbon.target.id].sort().join('.')
92+
return {
93+
..._ribbon,
94+
// ensure id remains the same even if source/target are reversed
95+
id: [source.id, target.id].sort().join('.'),
96+
source: {
97+
...source,
98+
label: getLabel(source),
99+
color: getColor(source),
100+
},
101+
target: {
102+
...target,
103+
label: getLabel(target),
104+
color: getColor(target),
105+
},
106+
}
71107
})
72108

73109
return { arcs, ribbons }
74110
}
111+
112+
export const computeRibbonPath = ({
113+
sourceStartAngle,
114+
sourceEndAngle,
115+
targetStartAngle,
116+
targetEndAngle,
117+
ribbonGenerator,
118+
}: SpringValues<
119+
Pick<
120+
RibbonAnimatedProps,
121+
'sourceStartAngle' | 'sourceEndAngle' | 'targetStartAngle' | 'targetEndAngle'
122+
>
123+
> & {
124+
ribbonGenerator: RibbonGenerator
125+
}) =>
126+
to(
127+
[sourceStartAngle, sourceEndAngle, targetStartAngle, targetEndAngle],
128+
(sourceStartAngle, sourceEndAngle, targetStartAngle, targetEndAngle) =>
129+
ribbonGenerator({
130+
source: {
131+
startAngle: Math.min(sourceStartAngle, sourceEndAngle),
132+
endAngle: Math.max(sourceEndAngle, sourceStartAngle),
133+
},
134+
target: {
135+
startAngle: Math.min(targetStartAngle, targetEndAngle),
136+
endAngle: Math.max(targetEndAngle, targetStartAngle),
137+
},
138+
})
139+
)

‎packages/chord/src/defaults.ts

-4
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,11 @@ export const commonDefaultProps: Omit<
7272

7373
export const svgDefaultProps = {
7474
...commonDefaultProps,
75-
// arcComponent:
76-
// ribbonComponent
7775
ribbonBlendMode: 'normal' as NonNullable<ChordSvgProps['ribbonBlendMode']>,
7876
ribbonTooltip: ChordRibbonTooltip,
7977
}
8078

8179
export const canvasDefaultProps = {
8280
...commonDefaultProps,
83-
// renderArc
84-
// renderRibbon
8581
pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
8682
}

‎packages/chord/src/hooks.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import { useMemo, useState } from 'react'
2-
import { useValueFormatter, getLabelGenerator } from '@nivo/core'
2+
import {
3+
useValueFormatter,
4+
// @ts-ignore
5+
getLabelGenerator,
6+
} from '@nivo/core'
37
import { OrdinalColorScale, useOrdinalColorScale } from '@nivo/colors'
48
import { computeChordLayout, computeChordGenerators, computeChordArcsAndRibbons } from './compute'
59
import { ArcDatum, ChordCommonProps, ChordDataProps, CustomLayerProps, RibbonDatum } from './types'
610
import { commonDefaultProps } from './defaults'
11+
import { ChordLayout } from 'd3-chord'
712

813
export const useChordLayout = ({ padAngle }: { padAngle: ChordCommonProps['padAngle'] }) =>
914
useMemo(() => computeChordLayout({ padAngle }), [padAngle])
@@ -38,12 +43,12 @@ export const useChordArcsAndRibbons = ({
3843
getLabel,
3944
formatValue,
4045
}: {
41-
chord: any
46+
chord: ChordLayout
4247
data: ChordDataProps['data']
4348
keys: ChordDataProps['keys']
44-
getLabel: (arc: ArcDatum) => string
49+
getLabel: (arc: Omit<ArcDatum, 'label' | 'color'>) => string
4550
formatValue: (value: number) => string
46-
getColor: OrdinalColorScale<ArcDatum>
51+
getColor: OrdinalColorScale<Omit<ArcDatum, 'label' | 'color'>>
4752
}) =>
4853
useMemo(
4954
() =>

‎packages/chord/src/types.ts

+44-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { AriaAttributes, MouseEvent, FunctionComponent } from 'react'
2+
import { RibbonGenerator as D3RibbonGenerator } from 'd3-chord'
3+
import { Arc as D3Arc } from 'd3-shape'
24
import {
35
Box,
46
Theme,
@@ -39,16 +41,47 @@ export interface ArcDatum {
3941
color: string
4042
}
4143

42-
export interface RibbonSubject extends ArcDatum {
43-
subindex: number
44-
}
45-
4644
export interface RibbonDatum {
4745
id: string
48-
source: RibbonSubject
49-
target: RibbonSubject
46+
source: ArcDatum
47+
target: ArcDatum
48+
}
49+
50+
export interface RibbonAnimatedProps {
51+
sourceStartAngle: number
52+
sourceEndAngle: number
53+
targetStartAngle: number
54+
targetEndAngle: number
55+
color: string
56+
opacity: number
57+
borderColor: string
5058
}
5159

60+
export type RibbonGenerator = D3RibbonGenerator<
61+
any,
62+
| RibbonDatum
63+
| {
64+
source: {
65+
startAngle: number
66+
endAngle: number
67+
}
68+
target: {
69+
startAngle: number
70+
endAngle: number
71+
}
72+
},
73+
RibbonDatum
74+
>
75+
76+
export type ArcGenerator = D3Arc<
77+
any,
78+
| ArcDatum
79+
| {
80+
startAngle: number
81+
endAngle: number
82+
}
83+
>
84+
5285
export interface ArcTooltipComponentProps {
5386
arc: ArcDatum
5487
}
@@ -66,21 +99,21 @@ export type ChordRibbonMouseHandler = (ribbon: any, event: MouseEvent) => void
6699
export type ChordCommonProps = {
67100
margin: Box
68101

69-
label: PropertyAccessor<ArcDatum, string>
102+
label: PropertyAccessor<Omit<ArcDatum, 'label' | 'color'>, string>
70103
valueFormat: ValueFormat<number>
71104

72105
padAngle: number
73106
innerRadiusRatio: number
74107
innerRadiusOffset: number
75108

76109
theme: Theme
77-
colors: OrdinalColorScaleConfig
110+
colors: OrdinalColorScaleConfig<Omit<ArcDatum, 'label' | 'color'>>
78111

79112
arcOpacity: number
80113
arcHoverOpacity: number
81114
arcHoverOthersOpacity: number
82115
arcBorderWidth: number
83-
arcBorderColor: InheritedColorConfig<any>
116+
arcBorderColor: InheritedColorConfig<ArcDatum>
84117
onArcMouseEnter: ChordArcMouseHandler
85118
onArcMouseMove: ChordArcMouseHandler
86119
onArcMouseLeave: ChordArcMouseHandler
@@ -92,12 +125,12 @@ export type ChordCommonProps = {
92125
ribbonHoverOpacity: number
93126
ribbonHoverOthersOpacity: number
94127
ribbonBorderWidth: number
95-
ribbonBorderColor: InheritedColorConfig<any>
128+
ribbonBorderColor: InheritedColorConfig<ArcDatum>
96129

97130
enableLabel: boolean
98131
labelOffset: number
99132
labelRotation: number
100-
labelTextColor: InheritedColorConfig<any>
133+
labelTextColor: InheritedColorConfig<ArcDatum>
101134

102135
isInteractive: boolean
103136
defaultActiveNodeIds: string[]

‎website/src/data/components/chord/props.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import {
55
groupProperties,
66
getLegendsProps,
77
} from '../../../lib/componentProperties'
8-
import { chartDimensions, ordinalColors, isInteractive } from '../../../lib/chart-properties'
8+
import {
9+
chartDimensions,
10+
ordinalColors,
11+
isInteractive,
12+
blendMode,
13+
} from '../../../lib/chart-properties'
914
import { ChartProperty, Flavor } from '../../../types'
1015

1116
const allFlavors: Flavor[] = ['svg', 'canvas', 'api']
@@ -143,6 +148,13 @@ const props: ChartProperty[] = [
143148
control: { type: 'inheritedColor' },
144149
group: 'Style',
145150
},
151+
blendMode({
152+
key: 'ribbonBlendMode',
153+
target: 'ribbons',
154+
group: 'Style',
155+
flavors: ['svg'],
156+
defaultValue: defaults.ribbonBlendMode,
157+
}),
146158
{
147159
key: 'ribbonOpacity',
148160
help: 'Ribbons opacity.',
@@ -436,7 +448,7 @@ const props: ChartProperty[] = [
436448
},
437449
},
438450
},
439-
...motionProperties(['svg'], defaults),
451+
...motionProperties(['svg'], defaults, 'react-spring'),
440452
]
441453

442454
export const groups = groupProperties(props)

‎website/src/pages/chord/index.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ const initialProperties = {
2727
arcBorderWidth: 1,
2828
arcBorderColor: {
2929
from: 'color',
30-
modifiers: [['darker', 0.4]],
30+
modifiers: [['darker', 0.6]],
3131
},
3232

33+
ribbonBlendMode: 'normal',
3334
ribbonOpacity: 0.5,
3435
ribbonBorderWidth: 1,
3536
ribbonBorderColor: {
3637
from: 'color',
37-
modifiers: [['darker', 0.4]],
38+
modifiers: [['darker', 0.6]],
3839
},
3940

4041
enableLabel: true,
@@ -55,8 +56,7 @@ const initialProperties = {
5556
ribbonHoverOthersOpacity: 0.25,
5657

5758
animate: true,
58-
motionStiffness: 90,
59-
motionDamping: 7,
59+
motionConfig: 'stiff',
6060

6161
legends: [
6262
{

0 commit comments

Comments
 (0)
Please sign in to comment.