/
links.ts
198 lines (185 loc) · 5.74 KB
/
links.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { useCallback, useMemo } from 'react'
import {
positionFromAngle,
radiansToDegrees,
PropertyAccessor,
usePropertyAccessor,
useTheme,
} from '@nivo/core'
import { InheritedColorConfig, useInheritedColor } from '@nivo/colors'
import { Arc, DatumWithArc, DatumWithArcAndColor, Point } from './types'
import { getNormalizedAngle } from './utils'
export interface ArcLink {
side: 'before' | 'after'
points: [Point, Point, Point]
}
export interface ArcLinkWithDatum<Datum extends DatumWithArc> extends ArcLink {
data: Datum
}
/**
* Compute the link of a single arc, returning its points,
* please not that points coordinates are relative to
* the center of the arc.
*/
export const computeArcLink = (
arc: Arc,
offset: number,
diagonalLength: number,
straightLength: number
): ArcLink => {
const centerAngle = getNormalizedAngle(
arc.startAngle + (arc.endAngle - arc.startAngle) / 2 - Math.PI / 2
)
const point0: Point = positionFromAngle(centerAngle, arc.outerRadius + offset)
const point1: Point = positionFromAngle(centerAngle, arc.outerRadius + offset + diagonalLength)
let side: ArcLink['side']
let point2: Point
if (centerAngle < Math.PI / 2 || centerAngle > Math.PI * 1.5) {
side = 'after'
point2 = {
x: point1.x + straightLength,
y: point1.y,
}
} else {
side = 'before'
point2 = {
x: point1.x - straightLength,
y: point1.y,
}
}
return {
side,
points: [point0, point1, point2],
}
}
/**
* Compute links for an array of data containing arcs.
*
* This is typically used to create labels for arcs.
*/
export const useArcLinks = <
Datum extends DatumWithArc,
ExtraProps extends Record<string, any> = Record<string, any>
>({
data,
skipAngle = 0,
offset = 0.5,
diagonalLength,
straightLength,
computeExtraProps = () => ({} as ExtraProps),
}: {
data: Datum[]
// arcs with a length below this (end angle - start angle in degrees)
// are gonna be excluded, this can be typically used to avoid having
// overlapping labels.
skipAngle?: number
// offset from arc outer radius in pixels
offset?: number
// length of the diagonal segment of the link
diagonalLength: number
// length of the straight segment of the link
straightLength: number
// this can be used to append extra properties to the links,
// can be used to compute a color/label for example.
computeExtraProps?: (datum: ArcLinkWithDatum<Datum>) => ExtraProps
}): (ArcLinkWithDatum<Datum> & ExtraProps)[] => {
const links = useMemo(
() =>
data
// filter out arcs with a length below `skipAngle`
.filter(
datum =>
Math.abs(radiansToDegrees(datum.arc.endAngle - datum.arc.startAngle)) >=
skipAngle
)
// compute the link for each eligible arc
.map(datum => ({
...computeArcLink(datum.arc, offset, diagonalLength, straightLength),
data: datum,
})),
[data, skipAngle, offset, diagonalLength, straightLength]
)
// splitting memoization of links and extra props can be more efficient,
// this way if only `computeExtraProps` changes, we skip links computation.
return useMemo(
() =>
links.map(link => ({
...computeExtraProps(link),
...link,
})),
[links, computeExtraProps]
)
}
export interface ArcLinkLabel<Datum extends DatumWithArcAndColor> extends ArcLinkWithDatum<Datum> {
x: number
y: number
label: string
linkColor: string
textAnchor: 'start' | 'end'
textColor: string
}
/**
* Compute arc link labels, please note that the datum should
* contain a color in order to be able to compute the link/label text color.
*
* Please see `useArcLinks` for a more detailed explanation
* about the parameters.
*/
export const useArcLinkLabels = <Datum extends DatumWithArcAndColor>({
data,
skipAngle,
offset,
diagonalLength,
straightLength,
textOffset = 0,
label,
linkColor,
textColor,
}: {
data: Datum[]
skipAngle?: number
offset?: number
diagonalLength: number
straightLength: number
textOffset: number
label: PropertyAccessor<Datum, string>
linkColor: InheritedColorConfig<Datum>
textColor: InheritedColorConfig<Datum>
}) => {
const getLabel = usePropertyAccessor<Datum, string>(label)
const theme = useTheme()
const getLinkColor = useInheritedColor<Datum>(linkColor, theme)
const getTextColor = useInheritedColor<Datum>(textColor, theme)
const computeExtraProps = useCallback(
(link: ArcLinkWithDatum<Datum>) => {
const position = {
x: link.points[2].x,
y: link.points[2].y,
}
let textAnchor: ArcLinkLabel<Datum>['textAnchor']
if (link.side === 'before') {
position.x -= textOffset
textAnchor = 'end'
} else {
position.x += textOffset
textAnchor = 'start'
}
return {
...position,
label: getLabel(link.data),
linkColor: getLinkColor(link.data),
textAnchor,
textColor: getTextColor(link.data),
}
},
[getLabel, getLinkColor, getTextColor]
)
return useArcLinks<Datum, Omit<ArcLinkLabel<Datum>, keyof ArcLinkWithDatum<Datum>>>({
data,
skipAngle,
offset,
diagonalLength,
straightLength,
computeExtraProps,
})
}