Skip to content

Commit 16c316b

Browse files
committedJan 1, 2022
feat(chord): init TypeScript migration
1 parent 8faac86 commit 16c316b

32 files changed

+1057
-844
lines changed
 

‎examples/codesandbox/src/charts/Chord.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export function Chord() {
1111
const [data, flavor] = useChart(() => generateChordData({ size: 7 }))
1212

1313
if (flavor === 'canvas') {
14-
return <ResponsiveChordCanvas {...data} {...props} />
14+
return <ResponsiveChordCanvas {...props} data{data.matrix} keys={data.keys} />
1515
}
1616

17-
return <ResponsiveChord {...data} {...props} />
17+
return <ResponsiveChord {...props} data{data.matrix} keys={data.keys} />
1818
}
File renamed without changes.

‎packages/chord/package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
],
2222
"main": "./dist/nivo-chord.cjs.js",
2323
"module": "./dist/nivo-chord.es.js",
24+
"typings": "./dist/types/index.d.ts",
2425
"files": [
2526
"README.md",
2627
"LICENSE.md",
27-
"index.d.ts",
28-
"dist/"
28+
"dist/",
29+
"!dist/tsconfig.tsbuildinfo"
2930
],
3031
"dependencies": {
3132
"@nivo/arcs": "0.77.0",
@@ -38,11 +39,11 @@
3839
"react-motion": "^0.5.2"
3940
},
4041
"devDependencies": {
41-
"@nivo/core": "0.77.0"
42+
"@nivo/core": "0.77.0",
43+
"@types/d3-chord": "^3.0.1"
4244
},
4345
"peerDependencies": {
4446
"@nivo/core": "0.77.0",
45-
"prop-types": ">= 15.5.10 < 16.0.0",
4647
"react": ">= 16.14.0 < 18.0.0"
4748
},
4849
"publishConfig": {
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,55 @@
1-
import { Fragment } from 'react'
2-
import { withContainer, SvgWrapper, useDimensions, useTheme } from '@nivo/core'
1+
import { createElement, Fragment, ReactNode } from 'react'
2+
import { Container, SvgWrapper, useDimensions, useTheme } from '@nivo/core'
33
import { useInheritedColor } from '@nivo/colors'
44
import { BoxLegendSvg } from '@nivo/legends'
5-
import { ChordPropTypes, ChordDefaultProps } from './props'
6-
import { useChord, useChordSelection, useChordLayerContext } from './hooks'
7-
import ChordRibbons from './ChordRibbons'
8-
import ChordArcs from './ChordArcs'
9-
import ChordLabels from './ChordLabels'
5+
import { svgDefaultProps } from './defaults'
6+
import { useChord, useChordSelection, useCustomLayerProps } from './hooks'
7+
import { ChordRibbons } from './ChordRibbons'
8+
import { ChordArcs } from './ChordArcs'
9+
import { ChordLabels } from './ChordLabels'
10+
import { ChordSvgProps, LayerId } from './types'
1011

11-
const Chord = ({
12-
margin: partialMargin,
13-
width,
14-
height,
12+
type InnerChordProps = Omit<ChordSvgProps, 'animate' | 'motionConfig' | 'renderWrapper' | 'theme'>
1513

14+
const InnerChord = ({
15+
data,
1616
keys,
17-
matrix,
1817
label,
1918
valueFormat,
20-
innerRadiusRatio,
21-
innerRadiusOffset,
22-
padAngle,
23-
24-
layers,
25-
26-
colors,
27-
28-
arcBorderWidth,
29-
arcBorderColor,
30-
arcOpacity,
31-
arcHoverOpacity,
32-
arcHoverOthersOpacity,
33-
arcTooltip,
34-
35-
ribbonBorderWidth,
36-
ribbonBorderColor,
37-
ribbonBlendMode,
38-
ribbonOpacity,
39-
ribbonHoverOpacity,
40-
ribbonHoverOthersOpacity,
41-
ribbonTooltip,
42-
43-
enableLabel,
44-
labelOffset,
45-
labelRotation,
46-
labelTextColor,
47-
48-
isInteractive,
19+
20+
margin: partialMargin,
21+
width,
22+
height,
23+
24+
innerRadiusRatio = svgDefaultProps.innerRadiusRatio,
25+
innerRadiusOffset = svgDefaultProps.innerRadiusOffset,
26+
padAngle = svgDefaultProps.padAngle,
27+
28+
layers = svgDefaultProps.layers,
29+
30+
colors = svgDefaultProps.colors,
31+
32+
arcBorderWidth = svgDefaultProps.arcBorderWidth,
33+
arcBorderColor = svgDefaultProps.arcBorderColor,
34+
arcOpacity = svgDefaultProps.arcOpacity,
35+
arcHoverOpacity = svgDefaultProps.arcHoverOpacity,
36+
arcHoverOthersOpacity = svgDefaultProps.arcHoverOthersOpacity,
37+
arcTooltip = svgDefaultProps.arcTooltip,
38+
39+
ribbonBorderWidth = svgDefaultProps.ribbonBorderWidth,
40+
ribbonBorderColor = svgDefaultProps.ribbonBorderColor,
41+
ribbonBlendMode = svgDefaultProps.ribbonBlendMode,
42+
ribbonOpacity = svgDefaultProps.ribbonOpacity,
43+
ribbonHoverOpacity = svgDefaultProps.ribbonHoverOpacity,
44+
ribbonHoverOthersOpacity = svgDefaultProps.ribbonHoverOthersOpacity,
45+
ribbonTooltip = svgDefaultProps.ribbonTooltip,
46+
47+
enableLabel = svgDefaultProps.enableLabel,
48+
labelOffset = svgDefaultProps.labelOffset,
49+
labelRotation = svgDefaultProps.labelRotation,
50+
labelTextColor = svgDefaultProps.labelTextColor,
51+
52+
isInteractive = svgDefaultProps.isInteractive,
4953
onArcMouseEnter,
5054
onArcMouseMove,
5155
onArcMouseLeave,
@@ -55,18 +59,22 @@ const Chord = ({
5559
onRibbonMouseLeave,
5660
onRibbonClick,
5761

58-
legends,
59-
role,
60-
}) => {
62+
legends = svgDefaultProps.legends,
63+
64+
role = svgDefaultProps.role,
65+
ariaLabel,
66+
ariaLabelledBy,
67+
ariaDescribedBy,
68+
}: InnerChordProps) => {
6169
const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(
6270
width,
6371
height,
6472
partialMargin
6573
)
6674

6775
const { center, radius, arcGenerator, ribbonGenerator, arcs, ribbons } = useChord({
76+
data,
6877
keys,
69-
matrix,
7078
label,
7179
valueFormat,
7280
width: innerWidth,
@@ -93,7 +101,7 @@ const Chord = ({
93101
const getArcBorderColor = useInheritedColor(arcBorderColor, theme)
94102
const getRibbonBorderColor = useInheritedColor(ribbonBorderColor, theme)
95103

96-
const layerContext = useChordLayerContext({
104+
const customLayerProps = useCustomLayerProps({
97105
center,
98106
radius,
99107
arcs,
@@ -110,8 +118,15 @@ const Chord = ({
110118
color: arc.color,
111119
}))
112120

113-
const layerById = {
114-
ribbons: (
121+
const layerById: Record<LayerId, ReactNode> = {
122+
ribbons: null,
123+
arcs: null,
124+
labels: null,
125+
legends: null,
126+
}
127+
128+
if (layers.includes('ribbons')) {
129+
layerById.ribbons = (
115130
<g key="ribbons" transform={`translate(${center[0]}, ${center[1]})`}>
116131
<ChordRibbons
117132
ribbons={ribbons}
@@ -129,8 +144,11 @@ const Chord = ({
129144
tooltip={ribbonTooltip}
130145
/>
131146
</g>
132-
),
133-
arcs: (
147+
)
148+
}
149+
150+
if (layers.includes('arcs')) {
151+
layerById.arcs = (
134152
<g key="arcs" transform={`translate(${center[0]}, ${center[1]})`}>
135153
<ChordArcs
136154
arcs={arcs}
@@ -147,25 +165,10 @@ const Chord = ({
147165
tooltip={arcTooltip}
148166
/>
149167
</g>
150-
),
151-
labels: null,
152-
legends: (
153-
<Fragment key="legends">
154-
{legends.map((legend, i) => (
155-
<BoxLegendSvg
156-
key={i}
157-
{...legend}
158-
containerWidth={innerWidth}
159-
containerHeight={innerHeight}
160-
data={legendData}
161-
theme={theme}
162-
/>
163-
))}
164-
</Fragment>
165-
),
168+
)
166169
}
167170

168-
if (enableLabel === true) {
171+
if (layers.includes('labels') && enableLabel) {
169172
layerById.labels = (
170173
<g key="labels" transform={`translate(${center[0]}, ${center[1]})`}>
171174
<ChordLabels
@@ -178,29 +181,60 @@ const Chord = ({
178181
)
179182
}
180183

184+
if (layers.includes('legends') && legends.length > 0) {
185+
layerById.legends = (
186+
<Fragment key="legends">
187+
{legends.map((legend, i) => (
188+
<BoxLegendSvg
189+
key={i}
190+
{...legend}
191+
containerWidth={innerWidth}
192+
containerHeight={innerHeight}
193+
data={legendData}
194+
/>
195+
))}
196+
</Fragment>
197+
)
198+
}
199+
181200
return (
182201
<SvgWrapper
183202
width={outerWidth}
184203
height={outerHeight}
185204
margin={margin}
186-
theme={theme}
187205
role={role}
206+
ariaLabel={ariaLabel}
207+
ariaLabelledBy={ariaLabelledBy}
208+
ariaDescribedBy={ariaDescribedBy}
188209
>
189210
{layers.map((layer, i) => {
190-
if (layerById[layer] !== undefined) {
191-
return layerById[layer]
192-
}
193211
if (typeof layer === 'function') {
194-
return <Fragment key={i}>{layer(layerContext)}</Fragment>
212+
return <Fragment key={i}>{createElement(layer, customLayerProps)}</Fragment>
195213
}
196214

197-
return null
215+
return layerById?.[layer] ?? null
198216
})}
199217
</SvgWrapper>
200218
)
201219
}
202220

203-
Chord.propTypes = ChordPropTypes
204-
Chord.defaultProps = ChordDefaultProps
205-
206-
export default withContainer(Chord)
221+
export const Chord = ({
222+
isInteractive = svgDefaultProps.isInteractive,
223+
animate = svgDefaultProps.animate,
224+
motionConfig = svgDefaultProps.motionConfig,
225+
theme,
226+
renderWrapper,
227+
...otherProps
228+
}: ChordSvgProps) => (
229+
<Container
230+
{...{
231+
animate,
232+
isInteractive,
233+
motionConfig,
234+
renderWrapper,
235+
theme,
236+
}}
237+
>
238+
<InnerChord isInteractive={isInteractive} {...otherProps} />
239+
</Container>
240+
)

‎packages/chord/src/ChordArc.js ‎packages/chord/src/ChordArc.tsx

+32-28
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
import { createElement, memo, useMemo } from 'react'
2-
import PropTypes from 'prop-types'
1+
import { createElement, memo, useMemo, MouseEvent } from 'react'
32
import { useTooltip } from '@nivo/tooltip'
3+
import { ArcDatum, ChordCommonProps } from './types'
44

5-
const ChordArc = memo(
5+
interface ChordArcProps {
6+
arc: ArcDatum
7+
startAngle: number
8+
endAngle: number
9+
arcGenerator: any
10+
borderWidth: number
11+
getBorderColor: (arc: ArcDatum) => string
12+
opacity: number
13+
setCurrent: (arc: ArcDatum | null) => void
14+
isInteractive: ChordCommonProps['isInteractive']
15+
onMouseEnter?: ChordCommonProps['onArcMouseEnter']
16+
onMouseMove?: ChordCommonProps['onArcMouseMove']
17+
onMouseLeave?: ChordCommonProps['onArcMouseLeave']
18+
onClick?: ChordCommonProps['onArcClick']
19+
tooltip: ChordCommonProps['arcTooltip']
20+
}
21+
22+
export const ChordArc = memo(
623
({
724
arc,
825
startAngle,
@@ -18,35 +35,42 @@ const ChordArc = memo(
1835
onMouseLeave,
1936
onClick,
2037
tooltip,
21-
}) => {
38+
}: ChordArcProps) => {
2239
const { showTooltipFromEvent, hideTooltip } = useTooltip()
2340

2441
const handleMouseEnter = useMemo(() => {
2542
if (!isInteractive) return undefined
26-
return event => {
43+
44+
return (event: MouseEvent) => {
2745
setCurrent(arc)
2846
showTooltipFromEvent(createElement(tooltip, { arc }), event)
2947
onMouseEnter && onMouseEnter(arc, event)
3048
}
3149
}, [isInteractive, showTooltipFromEvent, tooltip, arc, onMouseEnter])
50+
3251
const handleMouseMove = useMemo(() => {
3352
if (!isInteractive) return undefined
34-
return event => {
53+
54+
return (event: MouseEvent) => {
3555
showTooltipFromEvent(createElement(tooltip, { arc }), event)
3656
onMouseMove && onMouseMove(arc, event)
3757
}
3858
}, [isInteractive, showTooltipFromEvent, tooltip, arc, onMouseMove])
59+
3960
const handleMouseLeave = useMemo(() => {
4061
if (!isInteractive) return undefined
41-
return event => {
62+
63+
return (event: MouseEvent) => {
4264
setCurrent(null)
4365
hideTooltip()
4466
onMouseLeave && onMouseLeave(arc, event)
4567
}
4668
}, [isInteractive, hideTooltip, arc, onMouseLeave])
69+
4770
const handleClick = useMemo(() => {
4871
if (!isInteractive || !onClick) return undefined
49-
return event => onClick(arc, event)
72+
73+
return (event: MouseEvent) => onClick(arc, event)
5074
}, [isInteractive, arc, onClick])
5175

5276
return (
@@ -65,23 +89,3 @@ const ChordArc = memo(
6589
)
6690
}
6791
)
68-
69-
ChordArc.displayName = 'ChordArc'
70-
ChordArc.propTypes = {
71-
arc: PropTypes.object.isRequired,
72-
startAngle: PropTypes.number.isRequired,
73-
endAngle: PropTypes.number.isRequired,
74-
arcGenerator: PropTypes.func.isRequired,
75-
borderWidth: PropTypes.number.isRequired,
76-
getBorderColor: PropTypes.func.isRequired,
77-
opacity: PropTypes.number.isRequired,
78-
setCurrent: PropTypes.func.isRequired,
79-
isInteractive: PropTypes.bool.isRequired,
80-
onMouseEnter: PropTypes.func,
81-
onMouseMove: PropTypes.func,
82-
onMouseLeave: PropTypes.func,
83-
onClick: PropTypes.func,
84-
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
85-
}
86-
87-
export default ChordArc

‎packages/chord/src/ChordArcTooltip.js

-21
This file was deleted.
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { memo } from 'react'
2+
import { BasicTooltip } from '@nivo/tooltip'
3+
import { ArcTooltipComponentProps } from './types'
4+
5+
export const ChordArcTooltip = memo(({ arc }: ArcTooltipComponentProps) => (
6+
<BasicTooltip id={arc.label} value={arc.formattedValue} color={arc.color} enableChip={true} />
7+
))
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
import { memo } from 'react'
2-
import PropTypes from 'prop-types'
32
import { TransitionMotion, spring } from 'react-motion'
4-
import { interpolateColor, getInterpolatedColor } from '@nivo/colors'
3+
import { interpolateColor } from '@nivo/colors'
54
import { useMotionConfig } from '@nivo/core'
6-
import ChordArc from './ChordArc'
5+
import { ChordArc } from './ChordArc'
6+
import { ArcDatum, ChordCommonProps } from './types'
77

8-
const ChordArcs = memo(
8+
interface ChordArcsProps {
9+
arcs: ArcDatum[]
10+
arcGenerator: any
11+
borderWidth: ChordCommonProps['arcBorderWidth']
12+
getBorderColor: (arc: ArcDatum) => string
13+
getOpacity: (arc: ArcDatum) => number
14+
setCurrent: (arc: ArcDatum | null) => void
15+
isInteractive: ChordCommonProps['isInteractive']
16+
onMouseEnter?: ChordCommonProps['onArcMouseEnter']
17+
onMouseMove?: ChordCommonProps['onArcMouseMove']
18+
onMouseLeave?: ChordCommonProps['onArcMouseLeave']
19+
onClick?: ChordCommonProps['onArcClick']
20+
tooltip: ChordCommonProps['arcTooltip']
21+
}
22+
23+
export const ChordArcs = memo(
924
({
1025
arcs,
1126
borderWidth,
@@ -19,33 +34,35 @@ const ChordArcs = memo(
1934
onMouseLeave,
2035
onClick,
2136
tooltip,
22-
}) => {
37+
}: ChordArcsProps) => {
2338
const { animate, springConfig: _springConfig } = useMotionConfig()
2439

25-
if (animate !== true) {
26-
return arcs.map(arc => {
27-
return (
28-
<ChordArc
29-
key={arc.id}
30-
arc={arc}
31-
arcGenerator={arcGenerator}
32-
startAngle={arc.startAngle}
33-
endAngle={arc.endAngle}
34-
color={arc.color}
35-
opacity={getOpacity(arc)}
36-
borderWidth={borderWidth}
37-
getBorderColor={getBorderColor}
38-
getOpacity={getOpacity}
39-
isInteractive={isInteractive}
40-
setCurrent={setCurrent}
41-
onMouseEnter={onMouseEnter}
42-
onMouseMove={onMouseMove}
43-
onMouseLeave={onMouseLeave}
44-
onClick={onClick}
45-
tooltip={tooltip}
46-
/>
47-
)
48-
})
40+
if (!animate) {
41+
return (
42+
<>
43+
{arcs.map(arc => {
44+
return (
45+
<ChordArc
46+
key={arc.id}
47+
arc={arc}
48+
arcGenerator={arcGenerator}
49+
startAngle={arc.startAngle}
50+
endAngle={arc.endAngle}
51+
opacity={getOpacity(arc)}
52+
borderWidth={borderWidth}
53+
getBorderColor={getBorderColor}
54+
isInteractive={isInteractive}
55+
setCurrent={setCurrent}
56+
onMouseEnter={onMouseEnter}
57+
onMouseMove={onMouseMove}
58+
onMouseLeave={onMouseLeave}
59+
onClick={onClick}
60+
tooltip={tooltip}
61+
/>
62+
)
63+
})}
64+
</>
65+
)
4966
}
5067

5168
const springConfig = {
@@ -71,20 +88,16 @@ const ChordArcs = memo(
7188
{interpolatedStyles => (
7289
<>
7390
{interpolatedStyles.map(({ key, style, data: arc }) => {
74-
const color = getInterpolatedColor(style)
75-
7691
return (
7792
<ChordArc
7893
key={key}
7994
arc={arc}
8095
arcGenerator={arcGenerator}
8196
startAngle={style.startAngle}
8297
endAngle={style.endAngle}
83-
color={color}
8498
opacity={style.opacity}
8599
borderWidth={borderWidth}
86100
getBorderColor={getBorderColor}
87-
getOpacity={getOpacity}
88101
isInteractive={isInteractive}
89102
setCurrent={setCurrent}
90103
onMouseEnter={onMouseEnter}
@@ -101,21 +114,3 @@ const ChordArcs = memo(
101114
)
102115
}
103116
)
104-
105-
ChordArcs.displayName = 'ChordArcs'
106-
ChordArcs.propTypes = {
107-
arcs: PropTypes.array.isRequired,
108-
arcGenerator: PropTypes.func.isRequired,
109-
borderWidth: PropTypes.number.isRequired,
110-
getBorderColor: PropTypes.func.isRequired,
111-
getOpacity: PropTypes.func.isRequired,
112-
setCurrent: PropTypes.func.isRequired,
113-
isInteractive: PropTypes.bool.isRequired,
114-
onMouseEnter: PropTypes.func,
115-
onMouseMove: PropTypes.func,
116-
onMouseLeave: PropTypes.func,
117-
onClick: PropTypes.func,
118-
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
119-
}
120-
121-
export default ChordArcs

‎packages/chord/src/ChordCanvas.js

-351
This file was deleted.

‎packages/chord/src/ChordCanvas.tsx

+381
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
import { createElement, useRef, useEffect, useCallback, MouseEvent } from 'react'
2+
import {
3+
useDimensions,
4+
useTheme,
5+
midAngle,
6+
getPolarLabelProps,
7+
degreesToRadians,
8+
getRelativeCursor,
9+
Margin,
10+
Container,
11+
} from '@nivo/core'
12+
import { findArcUnderCursor } from '@nivo/arcs'
13+
import { useInheritedColor } from '@nivo/colors'
14+
import { renderLegendToCanvas } from '@nivo/legends'
15+
import { useTooltip } from '@nivo/tooltip'
16+
import { useChord, useChordSelection, useCustomLayerProps } from './hooks'
17+
import { ArcDatum, ChordCanvasProps } from './types'
18+
import { canvasDefaultProps } from './defaults'
19+
20+
const getArcFromMouseEvent = ({
21+
event,
22+
canvasEl,
23+
center,
24+
margin,
25+
radius,
26+
innerRadius,
27+
arcs,
28+
}: {
29+
event: MouseEvent
30+
canvasEl: HTMLCanvasElement
31+
center: [number, number]
32+
margin: Margin
33+
radius: number
34+
innerRadius: number
35+
arcs: ArcDatum[]
36+
}) => {
37+
const [x, y] = getRelativeCursor(canvasEl, event)
38+
const centerX = margin.left + center[0]
39+
const centerY = margin.top + center[1]
40+
41+
return findArcUnderCursor(centerX, centerY, radius, innerRadius, arcs, x, y)
42+
}
43+
44+
type InnerChordCanvasProps = Omit<ChordCanvasProps, 'renderWrapper' | 'theme'>
45+
46+
const InnerChordCanvas = ({
47+
pixelRatio = canvasDefaultProps.pixelRatio,
48+
margin: partialMargin,
49+
data,
50+
keys,
51+
width,
52+
height,
53+
label = canvasDefaultProps.label,
54+
valueFormat,
55+
innerRadiusRatio = canvasDefaultProps.innerRadiusRatio,
56+
innerRadiusOffset = canvasDefaultProps.innerRadiusOffset,
57+
padAngle = canvasDefaultProps.padAngle,
58+
layers = canvasDefaultProps.layers,
59+
colors = canvasDefaultProps.colors,
60+
arcBorderWidth = canvasDefaultProps.arcBorderWidth,
61+
arcBorderColor = canvasDefaultProps.arcBorderColor,
62+
arcOpacity = canvasDefaultProps.arcOpacity,
63+
arcHoverOpacity = canvasDefaultProps.arcHoverOpacity,
64+
arcHoverOthersOpacity = canvasDefaultProps.arcHoverOthersOpacity,
65+
arcTooltip = canvasDefaultProps.arcTooltip,
66+
ribbonBorderWidth = canvasDefaultProps.ribbonBorderWidth,
67+
ribbonBorderColor = canvasDefaultProps.ribbonBorderColor,
68+
ribbonOpacity = canvasDefaultProps.ribbonOpacity,
69+
ribbonHoverOpacity = canvasDefaultProps.ribbonHoverOpacity,
70+
ribbonHoverOthersOpacity = canvasDefaultProps.arcHoverOthersOpacity,
71+
enableLabel = canvasDefaultProps.enableLabel,
72+
labelOffset = canvasDefaultProps.labelOffset,
73+
labelRotation = canvasDefaultProps.labelRotation,
74+
labelTextColor = canvasDefaultProps.labelTextColor,
75+
isInteractive = canvasDefaultProps.isInteractive,
76+
onArcMouseEnter,
77+
onArcMouseMove,
78+
onArcMouseLeave,
79+
onArcClick,
80+
legends = canvasDefaultProps.legends,
81+
}: InnerChordCanvasProps) => {
82+
const canvasEl = useRef<HTMLCanvasElement | null>(null)
83+
84+
const { innerWidth, innerHeight, outerWidth, outerHeight, margin } = useDimensions(
85+
width,
86+
height,
87+
partialMargin
88+
)
89+
90+
const { center, radius, innerRadius, arcGenerator, ribbonGenerator, arcs, ribbons } = useChord({
91+
data,
92+
keys,
93+
label,
94+
valueFormat,
95+
width: innerWidth,
96+
height: innerHeight,
97+
innerRadiusRatio,
98+
innerRadiusOffset,
99+
padAngle,
100+
colors,
101+
})
102+
103+
const { currentArc, setCurrentArc, getArcOpacity, getRibbonOpacity } = useChordSelection({
104+
arcs,
105+
arcOpacity,
106+
arcHoverOpacity,
107+
arcHoverOthersOpacity,
108+
ribbons,
109+
ribbonOpacity,
110+
ribbonHoverOpacity,
111+
ribbonHoverOthersOpacity,
112+
})
113+
114+
const theme = useTheme()
115+
const getLabelTextColor = useInheritedColor(labelTextColor, theme)
116+
const getArcBorderColor = useInheritedColor(arcBorderColor, theme)
117+
const getRibbonBorderColor = useInheritedColor(ribbonBorderColor, theme)
118+
119+
const layerContext = useCustomLayerProps({
120+
center,
121+
radius,
122+
arcs,
123+
arcGenerator,
124+
ribbons,
125+
ribbonGenerator,
126+
})
127+
128+
useEffect(() => {
129+
if (canvasEl.current === null) return
130+
131+
canvasEl.current.width = outerWidth * pixelRatio
132+
canvasEl.current.height = outerHeight * pixelRatio
133+
134+
const ctx = canvasEl.current.getContext('2d')!
135+
136+
ctx.scale(pixelRatio, pixelRatio)
137+
138+
ctx.fillStyle = theme.background
139+
ctx.fillRect(0, 0, outerWidth, outerHeight)
140+
141+
if (radius <= 0) return
142+
143+
layers.forEach(layer => {
144+
if (layer === 'ribbons') {
145+
ctx.save()
146+
ctx.translate(margin.left + center[0], margin.top + center[1])
147+
148+
ribbonGenerator.context(ctx)
149+
ribbons.forEach(ribbon => {
150+
ctx.save()
151+
152+
ctx.globalAlpha = getRibbonOpacity(ribbon)
153+
ctx.fillStyle = ribbon.source.color
154+
ctx.beginPath()
155+
ribbonGenerator(ribbon)
156+
ctx.fill()
157+
158+
if (ribbonBorderWidth > 0) {
159+
ctx.strokeStyle = getRibbonBorderColor({
160+
...ribbon,
161+
color: ribbon.source.color,
162+
})
163+
ctx.lineWidth = ribbonBorderWidth
164+
ctx.stroke()
165+
}
166+
167+
ctx.restore()
168+
})
169+
170+
ctx.restore()
171+
}
172+
173+
if (layer === 'arcs') {
174+
ctx.save()
175+
ctx.translate(margin.left + center[0], margin.top + center[1])
176+
177+
arcGenerator.context(ctx)
178+
arcs.forEach(arc => {
179+
ctx.save()
180+
181+
ctx.globalAlpha = getArcOpacity(arc)
182+
ctx.fillStyle = arc.color
183+
ctx.beginPath()
184+
arcGenerator(arc)
185+
ctx.fill()
186+
187+
if (arcBorderWidth > 0) {
188+
ctx.strokeStyle = getArcBorderColor(arc)
189+
ctx.lineWidth = arcBorderWidth
190+
ctx.stroke()
191+
}
192+
193+
ctx.restore()
194+
})
195+
196+
ctx.restore()
197+
}
198+
199+
if (layer === 'labels' && enableLabel === true) {
200+
ctx.save()
201+
ctx.translate(margin.left + center[0], margin.top + center[1])
202+
203+
ctx.font = `${theme.labels.text.fontSize}px ${
204+
theme.labels.text.fontFamily || 'sans-serif'
205+
}`
206+
207+
arcs.forEach(arc => {
208+
const angle = midAngle(arc)
209+
const props = getPolarLabelProps(radius + labelOffset, angle, labelRotation)
210+
211+
ctx.save()
212+
ctx.translate(props.x, props.y)
213+
ctx.rotate(degreesToRadians(props.rotate))
214+
215+
ctx.textAlign = props.align
216+
ctx.textBaseline = props.baseline
217+
ctx.fillStyle = getLabelTextColor(arc, theme)
218+
ctx.fillText(arc.label, 0, 0)
219+
220+
ctx.restore()
221+
})
222+
223+
ctx.restore()
224+
}
225+
226+
if (layer === 'legends') {
227+
ctx.save()
228+
ctx.translate(margin.left, margin.top)
229+
230+
const legendData = arcs.map(arc => ({
231+
id: arc.id,
232+
label: arc.label,
233+
color: arc.color,
234+
}))
235+
236+
legends.forEach(legend => {
237+
renderLegendToCanvas(ctx, {
238+
...legend,
239+
data: legendData,
240+
containerWidth: innerWidth,
241+
containerHeight: innerHeight,
242+
theme,
243+
})
244+
})
245+
246+
ctx.restore()
247+
}
248+
249+
if (typeof layer === 'function') {
250+
layer(ctx, layerContext)
251+
}
252+
})
253+
}, [
254+
canvasEl,
255+
innerWidth,
256+
innerHeight,
257+
outerWidth,
258+
outerHeight,
259+
margin,
260+
pixelRatio,
261+
theme,
262+
layers,
263+
arcs,
264+
arcGenerator,
265+
getArcOpacity,
266+
arcBorderWidth,
267+
getArcBorderColor,
268+
ribbons,
269+
ribbonGenerator,
270+
getRibbonOpacity,
271+
ribbonBorderWidth,
272+
getRibbonBorderColor,
273+
enableLabel,
274+
labelOffset,
275+
labelRotation,
276+
getLabelTextColor,
277+
legends,
278+
layerContext,
279+
])
280+
281+
const { showTooltipFromEvent, hideTooltip } = useTooltip()
282+
283+
const handleMouseHover = useCallback(
284+
event => {
285+
if (canvasEl.current === null) return
286+
287+
const arc = getArcFromMouseEvent({
288+
event,
289+
canvasEl: canvasEl.current,
290+
center,
291+
margin,
292+
radius,
293+
innerRadius,
294+
arcs,
295+
})
296+
297+
if (arc) {
298+
setCurrentArc(arc)
299+
showTooltipFromEvent(createElement(arcTooltip, { arc }), event)
300+
!currentArc && onArcMouseEnter && onArcMouseEnter(arc, event)
301+
onArcMouseMove && onArcMouseMove(arc, event)
302+
currentArc &&
303+
currentArc.id !== arc.id &&
304+
onArcMouseLeave &&
305+
onArcMouseLeave(arc, event)
306+
} else {
307+
setCurrentArc(null)
308+
hideTooltip()
309+
currentArc && onArcMouseLeave && onArcMouseLeave(currentArc, event)
310+
}
311+
},
312+
[
313+
canvasEl,
314+
center,
315+
margin,
316+
radius,
317+
innerRadius,
318+
arcs,
319+
setCurrentArc,
320+
showTooltipFromEvent,
321+
hideTooltip,
322+
onArcMouseEnter,
323+
onArcMouseMove,
324+
onArcMouseLeave,
325+
]
326+
)
327+
328+
const handleMouseLeave = useCallback(() => {
329+
setCurrentArc(null)
330+
hideTooltip()
331+
}, [setCurrentArc, hideTooltip])
332+
333+
const handleClick = useCallback(
334+
event => {
335+
if (canvasEl.current === null || !onArcClick) return
336+
337+
const arc = getArcFromMouseEvent({
338+
event,
339+
canvasEl: canvasEl.current,
340+
center,
341+
margin,
342+
radius,
343+
innerRadius,
344+
arcs,
345+
})
346+
347+
arc && onArcClick(arc, event)
348+
},
349+
[canvasEl, center, margin, radius, innerRadius, arcs, onArcClick]
350+
)
351+
352+
return (
353+
<canvas
354+
ref={canvasEl}
355+
width={outerWidth * pixelRatio}
356+
height={outerHeight * pixelRatio}
357+
style={{
358+
width: outerWidth,
359+
height: outerHeight,
360+
cursor: isInteractive ? 'auto' : 'normal',
361+
}}
362+
onMouseEnter={isInteractive ? handleMouseHover : undefined}
363+
onMouseMove={isInteractive ? handleMouseHover : undefined}
364+
onMouseLeave={isInteractive ? handleMouseLeave : undefined}
365+
onClick={isInteractive ? handleClick : undefined}
366+
/>
367+
)
368+
}
369+
370+
export const ChordCanvas = ({
371+
theme,
372+
isInteractive = canvasDefaultProps.isInteractive,
373+
animate = canvasDefaultProps.animate,
374+
motionConfig = canvasDefaultProps.motionConfig,
375+
renderWrapper,
376+
...otherProps
377+
}: ChordCanvasProps) => (
378+
<Container {...{ isInteractive, animate, motionConfig, theme, renderWrapper }}>
379+
<InnerChordCanvas isInteractive={isInteractive} {...otherProps} />
380+
</Container>
381+
)

‎packages/chord/src/ChordLabels.js ‎packages/chord/src/ChordLabels.tsx

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
import PropTypes from 'prop-types'
1+
import { memo } from 'react'
22
import { TransitionMotion, spring } from 'react-motion'
33
import { midAngle, getPolarLabelProps, useTheme } from '@nivo/core'
44
import { useMotionConfig } from '@nivo/core'
5+
import { ArcDatum } from './types'
56

6-
const ChordLabels = ({ arcs, radius, rotation, getColor }) => {
7+
interface ChordLabelsProps {
8+
arcs: ArcDatum[]
9+
radius: number
10+
rotation: number
11+
getColor: (arc: ArcDatum) => string
12+
}
13+
14+
export const ChordLabels = memo(({ arcs, radius, rotation, getColor }: ChordLabelsProps) => {
715
const theme = useTheme()
816
const { animate, springConfig } = useMotionConfig()
917

10-
if (animate !== true) {
18+
if (animate) {
1119
return (
1220
<>
1321
{arcs.map(arc => {
@@ -75,13 +83,4 @@ const ChordLabels = ({ arcs, radius, rotation, getColor }) => {
7583
)}
7684
</TransitionMotion>
7785
)
78-
}
79-
80-
ChordLabels.propTypes = {
81-
arcs: PropTypes.array.isRequired,
82-
radius: PropTypes.number.isRequired,
83-
rotation: PropTypes.number.isRequired,
84-
getColor: PropTypes.func.isRequired,
85-
}
86-
87-
export default ChordLabels
86+
})

‎packages/chord/src/ChordRibbon.js ‎packages/chord/src/ChordRibbon.tsx

+36-33
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
1-
import { createElement, memo, useMemo } from 'react'
2-
import PropTypes from 'prop-types'
3-
import { blendModePropType } from '@nivo/core'
1+
import { createElement, memo, useMemo, MouseEvent } from 'react'
42
import { useTooltip } from '@nivo/tooltip'
3+
import { ChordCommonProps, ChordSvgProps, RibbonDatum } from './types'
54

6-
const ChordRibbon = memo(
5+
interface ChordRibbonProps {
6+
ribbon: RibbonDatum
7+
ribbonGenerator: any
8+
sourceStartAngle: number
9+
sourceEndAngle: number
10+
targetStartAngle: number
11+
targetEndAngle: number
12+
color: string
13+
blendMode: NonNullable<ChordSvgProps['ribbonBlendMode']>
14+
opacity: number
15+
borderWidth: number
16+
getBorderColor: (ribbon: RibbonDatum) => string
17+
setCurrent: (ribbon: RibbonDatum | null) => void
18+
isInteractive: ChordCommonProps['isInteractive']
19+
tooltip: NonNullable<ChordSvgProps['ribbonTooltip']>
20+
onMouseEnter: ChordSvgProps['onRibbonMouseEnter']
21+
onMouseMove: ChordSvgProps['onRibbonMouseMove']
22+
onMouseLeave: ChordSvgProps['onRibbonMouseLeave']
23+
onClick: ChordSvgProps['onRibbonClick']
24+
}
25+
26+
export const ChordRibbon = memo(
727
({
828
ribbon,
929
ribbonGenerator,
@@ -23,35 +43,42 @@ const ChordRibbon = memo(
2343
onMouseLeave,
2444
onClick,
2545
tooltip,
26-
}) => {
46+
}: ChordRibbonProps) => {
2747
const { showTooltipFromEvent, hideTooltip } = useTooltip()
2848

2949
const handleMouseEnter = useMemo(() => {
3050
if (!isInteractive) return undefined
31-
return event => {
51+
52+
return (event: MouseEvent) => {
3253
setCurrent(ribbon)
3354
showTooltipFromEvent(createElement(tooltip, { ribbon }), event)
3455
onMouseEnter && onMouseEnter(ribbon, event)
3556
}
3657
}, [isInteractive, showTooltipFromEvent, tooltip, ribbon, onMouseEnter])
58+
3759
const handleMouseMove = useMemo(() => {
3860
if (!isInteractive) return undefined
39-
return event => {
61+
62+
return (event: MouseEvent) => {
4063
showTooltipFromEvent(createElement(tooltip, { ribbon }), event)
4164
onMouseMove && onMouseMove(ribbon, event)
4265
}
4366
}, [isInteractive, showTooltipFromEvent, tooltip, ribbon, onMouseMove])
67+
4468
const handleMouseLeave = useMemo(() => {
4569
if (!isInteractive) return undefined
46-
return event => {
70+
71+
return (event: MouseEvent) => {
4772
setCurrent(null)
4873
hideTooltip()
4974
onMouseLeave && onMouseLeave(ribbon, event)
5075
}
5176
}, [isInteractive, hideTooltip, ribbon, onMouseLeave])
77+
5278
const handleClick = useMemo(() => {
5379
if (!isInteractive || !onClick) return undefined
54-
return event => onClick(ribbon, event)
80+
81+
return (event: MouseEvent) => onClick(ribbon, event)
5582
}, [isInteractive, ribbon, onClick])
5683

5784
return (
@@ -80,27 +107,3 @@ const ChordRibbon = memo(
80107
)
81108
}
82109
)
83-
84-
ChordRibbon.displayName = 'ChordRibbon'
85-
ChordRibbon.propTypes = {
86-
ribbon: PropTypes.object.isRequired,
87-
ribbonGenerator: PropTypes.func.isRequired,
88-
sourceStartAngle: PropTypes.number.isRequired,
89-
sourceEndAngle: PropTypes.number.isRequired,
90-
targetStartAngle: PropTypes.number.isRequired,
91-
targetEndAngle: PropTypes.number.isRequired,
92-
color: PropTypes.string.isRequired,
93-
blendMode: blendModePropType.isRequired,
94-
opacity: PropTypes.number.isRequired,
95-
borderWidth: PropTypes.number.isRequired,
96-
getBorderColor: PropTypes.func.isRequired,
97-
setCurrent: PropTypes.func.isRequired,
98-
isInteractive: PropTypes.bool.isRequired,
99-
onMouseEnter: PropTypes.func,
100-
onMouseMove: PropTypes.func,
101-
onMouseLeave: PropTypes.func,
102-
onClick: PropTypes.func,
103-
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
104-
}
105-
106-
export default ChordRibbon

‎packages/chord/src/ChordRibbonTooltip.js

-33
This file was deleted.
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { memo } from 'react'
2+
import { TableTooltip, Chip } from '@nivo/tooltip'
3+
import { RibbonTooltipComponentProps } from './types'
4+
5+
export const ChordRibbonTooltip = memo(({ ribbon }: RibbonTooltipComponentProps) => (
6+
<TableTooltip
7+
rows={[
8+
[
9+
<Chip key="chip" color={ribbon.source.color} />,
10+
<strong key="id">{ribbon.source.label}</strong>,
11+
ribbon.source.formattedValue,
12+
],
13+
[
14+
<Chip key="chip" color={ribbon.target.color} />,
15+
<strong key="id">{ribbon.target.label}</strong>,
16+
ribbon.target.formattedValue,
17+
],
18+
]}
19+
/>
20+
))

‎packages/chord/src/ChordRibbons.js ‎packages/chord/src/ChordRibbons.tsx

+21-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { memo } from 'react'
2-
import PropTypes from 'prop-types'
32
import mapValues from 'lodash/mapValues'
43
import { TransitionMotion, spring } from 'react-motion'
5-
import { blendModePropType, midAngle, useMotionConfig } from '@nivo/core'
4+
import { midAngle, useMotionConfig } from '@nivo/core'
65
import { interpolateColor, getInterpolatedColor } from '@nivo/colors'
7-
import ChordRibbon from './ChordRibbon'
6+
import { ChordRibbon } from './ChordRibbon'
7+
import { ChordCommonProps, ChordSvgProps, RibbonDatum } from './types'
88

99
/**
1010
* Used to get ribbon angles, instead of using source and target arcs,
@@ -68,7 +68,23 @@ const ribbonWillLeave =
6868
...interpolateColor(ribbon.source.color, springConfig),
6969
})
7070

71-
const ChordRibbons = memo(
71+
interface ChordRibbonsProps {
72+
ribbons: RibbonDatum[]
73+
ribbonGenerator: any
74+
borderWidth: ChordCommonProps['ribbonBorderWidth']
75+
getBorderColor: (ribbon: RibbonDatum) => string
76+
getOpacity: (ribbon: RibbonDatum) => number
77+
blendMode: NonNullable<ChordSvgProps['ribbonBlendMode']>
78+
isInteractive: ChordCommonProps['isInteractive']
79+
setCurrent: (ribbon: RibbonDatum | null) => void
80+
tooltip: NonNullable<ChordSvgProps['ribbonTooltip']>
81+
onMouseEnter: ChordSvgProps['onRibbonMouseEnter']
82+
onMouseMove: ChordSvgProps['onRibbonMouseMove']
83+
onMouseLeave: ChordSvgProps['onRibbonMouseLeave']
84+
onClick: ChordSvgProps['onRibbonClick']
85+
}
86+
87+
export const ChordRibbons = memo(
7288
({
7389
ribbons,
7490
ribbonGenerator,
@@ -83,7 +99,7 @@ const ChordRibbons = memo(
8399
onMouseLeave,
84100
onClick,
85101
tooltip,
86-
}) => {
102+
}: ChordRibbonsProps) => {
87103
const { animate, springConfig: _springConfig } = useMotionConfig()
88104

89105
if (animate !== true) {
@@ -180,22 +196,3 @@ const ChordRibbons = memo(
180196
)
181197
}
182198
)
183-
184-
ChordRibbons.displayName = 'ChordRibbons'
185-
ChordRibbons.propTypes = {
186-
ribbons: PropTypes.array.isRequired,
187-
ribbonGenerator: PropTypes.func.isRequired,
188-
borderWidth: PropTypes.number.isRequired,
189-
getBorderColor: PropTypes.func.isRequired,
190-
getOpacity: PropTypes.func.isRequired,
191-
blendMode: blendModePropType.isRequired,
192-
isInteractive: PropTypes.bool.isRequired,
193-
setCurrent: PropTypes.func.isRequired,
194-
tooltip: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
195-
onMouseEnter: PropTypes.func,
196-
onMouseMove: PropTypes.func,
197-
onMouseLeave: PropTypes.func,
198-
onClick: PropTypes.func,
199-
}
200-
201-
export default ChordRibbons

‎packages/chord/src/ResponsiveChord.js

-10
This file was deleted.
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResponsiveWrapper } from '@nivo/core'
2+
import { Chord } from './Chord'
3+
import { ChordSvgProps } from './types'
4+
5+
export const ResponsiveChord = (props: Omit<ChordSvgProps, 'width' | 'height'>) => (
6+
<ResponsiveWrapper>
7+
{({ width, height }) => <Chord {...props} width={width} height={height} />}
8+
</ResponsiveWrapper>
9+
)

‎packages/chord/src/ResponsiveChordCanvas.js

-10
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ResponsiveWrapper } from '@nivo/core'
2+
import { ChordCanvas } from './ChordCanvas'
3+
import { ChordCanvasProps } from './types'
4+
5+
export const ResponsiveChordCanvas = (props: Omit<ChordCanvasProps, 'width' | 'height'>) => (
6+
<ResponsiveWrapper>
7+
{({ width, height }) => <ChordCanvas {...props} width={width} height={height} />}
8+
</ResponsiveWrapper>
9+
)

‎packages/chord/src/compute.js ‎packages/chord/src/compute.ts

+26-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
11
import { arc as d3Arc } from 'd3-shape'
2-
import { chord as d3Chord, ribbon as d3Ribbon } from 'd3-chord'
2+
import { chord as d3Chord, ChordLayout, ribbon as d3Ribbon } from 'd3-chord'
3+
import { ArcDatum, ChordCommonProps, ChordDataProps } from './types'
4+
import { OrdinalColorScale } from '@nivo/colors'
35

4-
export const computeChordLayout = ({ padAngle }) => d3Chord().padAngle(padAngle)
6+
export const computeChordLayout = ({ padAngle }: { padAngle: ChordCommonProps['padAngle'] }) =>
7+
d3Chord().padAngle(padAngle)
58

6-
export const computeChordGenerators = ({ width, height, innerRadiusRatio, innerRadiusOffset }) => {
9+
export const computeChordGenerators = ({
10+
width,
11+
height,
12+
innerRadiusRatio,
13+
innerRadiusOffset,
14+
}: {
15+
width: number
16+
height: number
17+
innerRadiusRatio: ChordCommonProps['innerRadiusRatio']
18+
innerRadiusOffset: ChordCommonProps['innerRadiusOffset']
19+
}) => {
720
const center = [width / 2, height / 2]
821
const radius = Math.min(width, height) / 2
922
const innerRadius = radius * innerRadiusRatio
@@ -18,13 +31,20 @@ export const computeChordGenerators = ({ width, height, innerRadiusRatio, innerR
1831

1932
export const computeChordArcsAndRibbons = ({
2033
chord,
21-
getColor,
34+
data,
2235
keys,
23-
matrix,
2436
getLabel,
2537
formatValue,
38+
getColor,
39+
}: {
40+
chord: ChordLayout
41+
data: ChordDataProps['data']
42+
keys: ChordDataProps['keys']
43+
getLabel: (arc: ArcDatum) => string
44+
formatValue: (valuee: number) => string
45+
getColor: OrdinalColorScale<any>
2646
}) => {
27-
const ribbons = chord(matrix)
47+
const ribbons = chord(data)
2848

2949
const arcs = ribbons.groups.map(arc => {
3050
arc.id = keys[arc.index]

‎packages/chord/src/defaults.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { LayerId, ChordSvgProps, ChordCommonProps } from './types'
2+
import { ChordArcTooltip } from './ChordArcTooltip'
3+
import { ChordRibbonTooltip } from './ChordRibbonTooltip'
4+
5+
export const commonDefaultProps: Omit<
6+
ChordCommonProps,
7+
| 'valueFormat'
8+
| 'margin'
9+
| 'theme'
10+
| 'onArcMouseEnter'
11+
| 'onArcMouseMove'
12+
| 'onArcMouseLeave'
13+
| 'onArcClick'
14+
| 'onRibbonMouseEnter'
15+
| 'onRibbonMouseMove'
16+
| 'onRibbonMouseLeave'
17+
| 'onRibbonClick'
18+
| 'renderWrapper'
19+
| 'ariaLabel'
20+
| 'ariaLabelledBy'
21+
| 'ariaDescribedBy'
22+
> & {
23+
layers: LayerId[]
24+
} = {
25+
layers: ['ribbons', 'arcs', 'labels', 'legends'],
26+
27+
padAngle: 0,
28+
innerRadiusRatio: 0.9,
29+
innerRadiusOffset: 0,
30+
31+
colors: { scheme: 'nivo' },
32+
33+
arcOpacity: 1,
34+
arcHoverOpacity: 1,
35+
arcHoverOthersOpacity: 0.15,
36+
arcBorderWidth: 1,
37+
arcBorderColor: {
38+
from: 'color',
39+
modifiers: [['darker', 0.4]],
40+
},
41+
arcTooltip: ChordArcTooltip,
42+
43+
ribbonOpacity: 0.5,
44+
ribbonHoverOpacity: 0.85,
45+
ribbonHoverOthersOpacity: 0.15,
46+
ribbonBorderWidth: 1,
47+
ribbonBorderColor: {
48+
from: 'color',
49+
modifiers: [['darker', 0.4]],
50+
},
51+
ribbonBlendMode: 'normal',
52+
53+
enableLabel: true,
54+
label: 'id',
55+
labelOffset: 12,
56+
labelRotation: 0,
57+
labelTextColor: {
58+
from: 'color',
59+
modifiers: [['darker', 1]],
60+
},
61+
62+
isInteractive: true,
63+
defaultActiveNodeIds: [],
64+
65+
legends: [],
66+
67+
animate: true,
68+
motionConfig: 'gentle',
69+
70+
role: 'img',
71+
}
72+
73+
export const svgDefaultProps = {
74+
...commonDefaultProps,
75+
// arcComponent:
76+
// ribbonComponent
77+
ribbonBlendMode: 'normal' as NonNullable<ChordSvgProps['ribbonBlendMode']>,
78+
ribbonTooltip: ChordRibbonTooltip,
79+
}
80+
81+
export const canvasDefaultProps = {
82+
...commonDefaultProps,
83+
// renderArc
84+
// renderRibbon
85+
pixelRatio: typeof window !== 'undefined' ? window.devicePixelRatio || 1 : 1,
86+
}

‎packages/chord/src/hooks.js ‎packages/chord/src/hooks.ts

+80-27
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
import { useMemo, useState } from 'react'
22
import { useValueFormatter, getLabelGenerator } from '@nivo/core'
3-
import { useOrdinalColorScale } from '@nivo/colors'
3+
import { OrdinalColorScale, useOrdinalColorScale } from '@nivo/colors'
44
import { computeChordLayout, computeChordGenerators, computeChordArcsAndRibbons } from './compute'
5+
import { ArcDatum, ChordCommonProps, ChordDataProps, CustomLayerProps, RibbonDatum } from './types'
6+
import { commonDefaultProps } from './defaults'
57

6-
export const useChordLayout = ({ padAngle }) =>
8+
export const useChordLayout = ({ padAngle }: { padAngle: ChordCommonProps['padAngle'] }) =>
79
useMemo(() => computeChordLayout({ padAngle }), [padAngle])
810

9-
export const useChordGenerators = ({ width, height, innerRadiusRatio, innerRadiusOffset }) =>
11+
export const useChordGenerators = ({
12+
width,
13+
height,
14+
innerRadiusRatio,
15+
innerRadiusOffset,
16+
}: {
17+
width: number
18+
height: number
19+
innerRadiusRatio: ChordCommonProps['innerRadiusRatio']
20+
innerRadiusOffset: ChordCommonProps['innerRadiusOffset']
21+
}) =>
1022
useMemo(
1123
() =>
1224
computeChordGenerators({
@@ -18,31 +30,56 @@ export const useChordGenerators = ({ width, height, innerRadiusRatio, innerRadiu
1830
[width, height, innerRadiusRatio, innerRadiusOffset]
1931
)
2032

21-
export const useChordArcsAndRibbons = ({ chord, getColor, keys, matrix, getLabel, formatValue }) =>
33+
export const useChordArcsAndRibbons = ({
34+
chord,
35+
getColor,
36+
keys,
37+
data,
38+
getLabel,
39+
formatValue,
40+
}: {
41+
chord: any
42+
data: ChordDataProps['data']
43+
keys: ChordDataProps['keys']
44+
getLabel: (arc: ArcDatum) => string
45+
formatValue: (value: number) => string
46+
getColor: OrdinalColorScale<ArcDatum>
47+
}) =>
2248
useMemo(
2349
() =>
2450
computeChordArcsAndRibbons({
2551
chord,
26-
getColor,
52+
data,
2753
keys,
28-
matrix,
2954
getLabel,
3055
formatValue,
56+
getColor,
3157
}),
32-
[chord, getColor, keys, matrix, getLabel, formatValue]
58+
[chord, getColor, keys, data, getLabel, formatValue]
3359
)
3460

3561
export const useChord = ({
62+
data,
3663
keys,
37-
matrix,
38-
label,
64+
label = commonDefaultProps.label,
3965
valueFormat,
4066
width,
4167
height,
42-
innerRadiusRatio,
43-
innerRadiusOffset,
44-
padAngle,
45-
colors,
68+
innerRadiusRatio = commonDefaultProps.innerRadiusRatio,
69+
innerRadiusOffset = commonDefaultProps.innerRadiusOffset,
70+
padAngle = commonDefaultProps.padAngle,
71+
colors = commonDefaultProps.colors,
72+
}: {
73+
data: ChordDataProps['data']
74+
keys: ChordDataProps['keys']
75+
label?: ChordCommonProps['label']
76+
valueFormat?: ChordCommonProps['valueFormat']
77+
width: number
78+
height: number
79+
innerRadiusRatio?: ChordCommonProps['innerRadiusRatio']
80+
innerRadiusOffset?: ChordCommonProps['innerRadiusOffset']
81+
padAngle?: ChordCommonProps['padAngle']
82+
colors?: ChordCommonProps['colors']
4683
}) => {
4784
const chord = useChordLayout({ padAngle })
4885
const { center, radius, innerRadius, arcGenerator, ribbonGenerator } = useChordGenerators({
@@ -52,14 +89,14 @@ export const useChord = ({
5289
innerRadiusOffset,
5390
})
5491
const getLabel = useMemo(() => getLabelGenerator(label), [label])
55-
const formatValue = useValueFormatter(valueFormat)
92+
const formatValue = useValueFormatter<number>(valueFormat)
5693

5794
const getColor = useOrdinalColorScale(colors, 'id')
5895
const { arcs, ribbons } = useChordArcsAndRibbons({
5996
chord,
6097
getColor,
6198
keys,
62-
matrix,
99+
data,
63100
getLabel,
64101
formatValue,
65102
})
@@ -79,16 +116,25 @@ export const useChord = ({
79116

80117
export const useChordSelection = ({
81118
arcs,
82-
arcOpacity,
83-
arcHoverOpacity,
84-
arcHoverOthersOpacity,
119+
arcOpacity = commonDefaultProps.arcOpacity,
120+
arcHoverOpacity = commonDefaultProps.arcHoverOpacity,
121+
arcHoverOthersOpacity = commonDefaultProps.arcHoverOthersOpacity,
85122
ribbons,
86-
ribbonOpacity,
87-
ribbonHoverOpacity,
88-
ribbonHoverOthersOpacity,
123+
ribbonOpacity = commonDefaultProps.ribbonOpacity,
124+
ribbonHoverOpacity = commonDefaultProps.ribbonHoverOpacity,
125+
ribbonHoverOthersOpacity = commonDefaultProps.ribbonHoverOthersOpacity,
126+
}: {
127+
arcs: ArcDatum[]
128+
arcOpacity?: ChordCommonProps['arcOpacity']
129+
arcHoverOpacity?: ChordCommonProps['arcHoverOpacity']
130+
arcHoverOthersOpacity?: ChordCommonProps['arcHoverOthersOpacity']
131+
ribbons: RibbonDatum[]
132+
ribbonOpacity?: ChordCommonProps['ribbonOpacity']
133+
ribbonHoverOpacity?: ChordCommonProps['ribbonHoverOpacity']
134+
ribbonHoverOthersOpacity?: ChordCommonProps['ribbonHoverOthersOpacity']
89135
}) => {
90-
const [currentArc, setCurrentArc] = useState(null)
91-
const [currentRibbon, setCurrentRibbon] = useState(null)
136+
const [currentArc, setCurrentArc] = useState<ArcDatum | null>(null)
137+
const [currentRibbon, setCurrentRibbon] = useState<RibbonDatum | null>(null)
92138

93139
const selection = useMemo(() => {
94140
const selectedArcIds = []
@@ -119,7 +165,7 @@ export const useChordSelection = ({
119165
selection.selectedArcIds.length > 1 || selection.selectedRibbonIds.length > 0
120166

121167
const getArcOpacity = useMemo(
122-
() => arc => {
168+
() => (arc: ArcDatum) => {
123169
if (!hasSelection) return arcOpacity
124170
return selection.selectedArcIds.includes(arc.id)
125171
? arcHoverOpacity
@@ -128,7 +174,7 @@ export const useChordSelection = ({
128174
[selection.selectedArcIds, arcOpacity, arcHoverOpacity, arcHoverOthersOpacity]
129175
)
130176
const getRibbonOpacity = useMemo(
131-
() => ribbon => {
177+
() => (ribbon: RibbonDatum) => {
132178
if (!hasSelection) return ribbonOpacity
133179
return selection.selectedRibbonIds.includes(ribbon.id)
134180
? ribbonHoverOpacity
@@ -149,14 +195,21 @@ export const useChordSelection = ({
149195
}
150196
}
151197

152-
export const useChordLayerContext = ({
198+
export const useCustomLayerProps = ({
153199
center,
154200
radius,
155201
arcs,
156202
arcGenerator,
157203
ribbons,
158204
ribbonGenerator,
159-
}) =>
205+
}: {
206+
center: [number, number]
207+
radius: number
208+
arcs: ArcDatum[]
209+
arcGenerator: any
210+
ribbons: RibbonDatum[]
211+
ribbonGenerator: any
212+
}): CustomLayerProps =>
160213
useMemo(
161214
() => ({
162215
center,

‎packages/chord/src/index.js

-7
This file was deleted.

‎packages/chord/src/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export * from './Chord'
2+
export * from './ChordCanvas'
3+
export * from './ResponsiveChord'
4+
export * from './ResponsiveChordCanvas'
5+
export * from './compute'
6+
export * from './hooks'
7+
export * from './types'
8+
export * from './defaults'

‎packages/chord/src/props.js

-126
This file was deleted.

‎packages/chord/src/types.ts

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { AriaAttributes, MouseEvent, FunctionComponent } from 'react'
2+
import {
3+
Box,
4+
Theme,
5+
Dimensions,
6+
ModernMotionProps,
7+
CssMixBlendMode,
8+
PropertyAccessor,
9+
ValueFormat,
10+
} from '@nivo/core'
11+
import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors'
12+
import { LegendProps } from '@nivo/legends'
13+
14+
export type LayerId = 'ribbons' | 'arcs' | 'labels' | 'legends'
15+
export interface CustomLayerProps {
16+
center: [number, number]
17+
radius: number
18+
arcs: ArcDatum[]
19+
arcGenerator: any
20+
ribbons: RibbonDatum[]
21+
ribbonGenerator: any
22+
}
23+
export type CustomLayer = FunctionComponent<CustomLayerProps>
24+
export type CustomCanvasLayer = (ctx: CanvasRenderingContext2D, props: CustomLayerProps) => void
25+
26+
export interface ChordDataProps {
27+
data: number[][]
28+
keys: string[]
29+
}
30+
31+
export interface ArcDatum {
32+
id: string
33+
index: number
34+
label: string
35+
value: number
36+
formattedValue: number | string
37+
startAngle: number
38+
endAngle: number
39+
color: string
40+
}
41+
42+
export interface RibbonSubject extends ArcDatum {
43+
subindex: number
44+
}
45+
46+
export interface RibbonDatum {
47+
id: string
48+
source: RibbonSubject
49+
target: RibbonSubject
50+
}
51+
52+
export interface ArcTooltipComponentProps {
53+
arc: ArcDatum
54+
}
55+
export type ArcTooltipComponent = FunctionComponent<ArcTooltipComponentProps>
56+
57+
export interface RibbonTooltipComponentProps {
58+
ribbon: RibbonDatum
59+
}
60+
export type RibbonTooltipComponent = FunctionComponent<RibbonTooltipComponentProps>
61+
62+
export type ChordArcMouseHandler = (arc: any, event: MouseEvent) => void
63+
64+
export type ChordRibbonMouseHandler = (ribbon: any, event: MouseEvent) => void
65+
66+
export type ChordCommonProps = {
67+
margin: Box
68+
69+
label: PropertyAccessor<ArcDatum, string>
70+
valueFormat: ValueFormat<number>
71+
72+
padAngle: number
73+
innerRadiusRatio: number
74+
innerRadiusOffset: number
75+
76+
theme: Theme
77+
colors: OrdinalColorScaleConfig
78+
79+
arcOpacity: number
80+
arcHoverOpacity: number
81+
arcHoverOthersOpacity: number
82+
arcBorderWidth: number
83+
arcBorderColor: InheritedColorConfig<any>
84+
onArcMouseEnter: ChordArcMouseHandler
85+
onArcMouseMove: ChordArcMouseHandler
86+
onArcMouseLeave: ChordArcMouseHandler
87+
onArcClick: ChordArcMouseHandler
88+
arcTooltip: ArcTooltipComponent
89+
90+
ribbonBlendMode: CssMixBlendMode
91+
ribbonOpacity: number
92+
ribbonHoverOpacity: number
93+
ribbonHoverOthersOpacity: number
94+
ribbonBorderWidth: number
95+
ribbonBorderColor: InheritedColorConfig<any>
96+
97+
enableLabel: boolean
98+
labelOffset: number
99+
labelRotation: number
100+
labelTextColor: InheritedColorConfig<any>
101+
102+
isInteractive: boolean
103+
defaultActiveNodeIds: string[]
104+
105+
legends: LegendProps[]
106+
107+
renderWrapper: boolean
108+
109+
role: string
110+
ariaLabel: AriaAttributes['aria-label']
111+
ariaLabelledBy: AriaAttributes['aria-labelledby']
112+
ariaDescribedBy: AriaAttributes['aria-describedby']
113+
} & Required<ModernMotionProps>
114+
115+
export type ChordSvgProps = Partial<ChordCommonProps> &
116+
ChordDataProps &
117+
Dimensions & {
118+
onRibbonMouseEnter?: ChordRibbonMouseHandler
119+
onRibbonMouseMove?: ChordRibbonMouseHandler
120+
onRibbonMouseLeave?: ChordRibbonMouseHandler
121+
onRibbonClick?: ChordRibbonMouseHandler
122+
ribbonTooltip?: RibbonTooltipComponent
123+
layers?: (LayerId | CustomLayer)[]
124+
}
125+
126+
export type ChordCanvasProps = Partial<ChordCommonProps> &
127+
ChordDataProps &
128+
Dimensions & {
129+
layers?: (LayerId | CustomCanvasLayer)[]
130+
pixelRatio?: number
131+
}

‎packages/chord/tsconfig.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../tsconfig.types.json",
3+
"compilerOptions": {
4+
"outDir": "./dist/types",
5+
"rootDir": "./src"
6+
},
7+
"include": ["src/**/*"]
8+
}

‎tsconfig.monorepo.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
{ "path": "./packages/bullet" },
2525
{ "path": "./packages/bump" },
2626
{ "path": "./packages/calendar" },
27+
{ "path": "./packages/chord" },
2728
{ "path": "./packages/circle-packing" },
2829
{ "path": "./packages/funnel" },
2930
{ "path": "./packages/marimekko" },

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

+15-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// @ts-ignore
2-
import { ChordDefaultProps as defaults } from '@nivo/chord'
1+
import { commonDefaultProps as defaults } from '@nivo/chord'
32
import {
43
themeProperty,
54
motionProperties,
@@ -12,6 +11,19 @@ import { ChartProperty, Flavor } from '../../../types'
1211
const allFlavors: Flavor[] = ['svg', 'canvas', 'api']
1312

1413
const props: ChartProperty[] = [
14+
{
15+
key: 'data',
16+
group: 'Base',
17+
help: 'The matrix used to compute the chord diagram.',
18+
description: `
19+
The matrix used to compute the chord diagram,
20+
it must be a square matrix, meaning each row length
21+
must equal the row count.
22+
`,
23+
required: true,
24+
type: 'number[][]',
25+
flavors: allFlavors,
26+
},
1527
{
1628
key: 'keys',
1729
group: 'Base',
@@ -40,26 +52,14 @@ const props: ChartProperty[] = [
4052
flavors: allFlavors,
4153
type: 'string[]',
4254
},
43-
{
44-
key: 'matrix',
45-
group: 'Base',
46-
help: 'The matrix used to compute the chord diagram.',
47-
description: `
48-
The matrix used to compute the chord diagram,
49-
it must be a square matrix, meaning each row length
50-
must equal the row count.
51-
`,
52-
required: true,
53-
type: 'Array<number[]>',
54-
flavors: allFlavors,
55-
},
5655
{
5756
key: 'valueFormat',
5857
group: 'Base',
5958
type: 'string | Function',
6059
required: false,
6160
help: `Optional value formatter.`,
6261
flavors: allFlavors,
62+
// control: { type: 'valueFormat'}
6363
},
6464
...chartDimensions(allFlavors),
6565
{

‎website/src/pages/chord/canvas.js ‎website/src/pages/chord/canvas.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22
import { generateChordData } from '@nivo/generators'
3-
import { ResponsiveChordCanvas } from '@nivo/chord'
3+
import { ResponsiveChordCanvas, canvasDefaultProps } from '@nivo/chord'
44
import { ComponentTemplate } from '../../components/components/ComponentTemplate'
55
import meta from '../../data/components/chord/meta.yml'
66
import mapper from '../../data/components/chord/mapper'
@@ -112,20 +112,20 @@ const ChordCanvas = () => {
112112
properties={groups}
113113
initialProperties={initialProperties}
114114
propertiesMapper={mapper}
115+
defaultProperties={canvasDefaultProps}
115116
codePropertiesMapper={(properties, data) => ({
116117
keys: data.keys,
117118
...properties,
118119
})}
119120
generateData={generateData}
120-
dataKey="matrix"
121121
getDataSize={() => MATRIX_SIZE * MATRIX_SIZE + MATRIX_SIZE}
122122
getTabData={data => data.matrix}
123123
image={image}
124124
>
125125
{(properties, data, theme, logAction) => {
126126
return (
127127
<ResponsiveChordCanvas
128-
matrix={data.matrix}
128+
data={data.matrix}
129129
keys={data.keys}
130130
{...properties}
131131
theme={theme}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22
import { generateChordData } from '@nivo/generators'
3-
import { ResponsiveChord } from '@nivo/chord'
3+
import { ResponsiveChord, svgDefaultProps } from '@nivo/chord'
44
import { ComponentTemplate } from '../../components/components/ComponentTemplate'
55
import meta from '../../data/components/chord/meta.yml'
66
import mapper from '../../data/components/chord/mapper'
@@ -113,20 +113,20 @@ const Chord = () => {
113113
currentFlavor="svg"
114114
properties={groups}
115115
initialProperties={initialProperties}
116+
defaultProperties={svgDefaultProps}
116117
propertiesMapper={mapper}
117118
codePropertiesMapper={(properties, data) => ({
118119
keys: data.keys,
119120
...properties,
120121
})}
121122
generateData={generateData}
122-
dataKey="matrix"
123123
getTabData={data => data.matrix}
124124
image={image}
125125
>
126126
{(properties, data, theme, logAction) => {
127127
return (
128128
<ResponsiveChord
129-
matrix={data.matrix}
129+
data={data.matrix}
130130
keys={data.keys}
131131
{...properties}
132132
theme={theme}

‎yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -5176,6 +5176,11 @@
51765176
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
51775177
integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
51785178

5179+
"@types/d3-chord@^3.0.1":
5180+
version "3.0.1"
5181+
resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.1.tgz#54c8856c19c8e4ab36a53f73ba737de4768ad248"
5182+
integrity sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==
5183+
51795184
"@types/d3-color@^2.0.0":
51805185
version "2.0.3"
51815186
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-2.0.3.tgz#8bc4589073c80e33d126345542f588056511fe82"

0 commit comments

Comments
 (0)
Please sign in to comment.