Skip to content

Commit 8e898d5

Browse files
committedSep 7, 2021
feat(radar): init TypeScript migration
1 parent 6982b3d commit 8e898d5

33 files changed

+1100
-1060
lines changed
 

‎packages/radar/index.d.ts

-84
This file was deleted.

‎packages/radar/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@
2121
],
2222
"main": "./dist/nivo-radar.cjs.js",
2323
"module": "./dist/nivo-radar.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/colors": "0.73.0",
@@ -40,7 +41,6 @@
4041
},
4142
"peerDependencies": {
4243
"@nivo/core": "0.73.0",
43-
"prop-types": ">= 15.5.10 < 16.0.0",
4444
"react": ">= 16.14.0 < 18.0.0"
4545
},
4646
"publishConfig": {

‎packages/radar/src/Radar.js

-192
This file was deleted.

‎packages/radar/src/Radar.tsx

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { ReactNode, Fragment } from 'react'
2+
import { Container, useDimensions, SvgWrapper } from '@nivo/core'
3+
import { BoxLegendSvg } from '@nivo/legends'
4+
import { RadarShapes } from './RadarShapes'
5+
import { RadarGrid } from './RadarGrid'
6+
import { RadarTooltip } from './RadarTooltip'
7+
import { RadarDots } from './RadarDots'
8+
import { svgDefaultProps } from './props'
9+
import { RadarLayerId, RadarSvgProps } from './types'
10+
import { useRadar } from './hooks'
11+
12+
type InnerRadarProps<D extends Record<string, unknown>> = Omit<
13+
RadarSvgProps<D>,
14+
'animate' | 'motionConfig' | 'renderWrapper' | 'theme'
15+
>
16+
17+
const InnerRadar = <D extends Record<string, unknown>>({
18+
data,
19+
keys,
20+
indexBy,
21+
layers = svgDefaultProps.layers,
22+
maxValue = svgDefaultProps.maxValue,
23+
curve = svgDefaultProps.curve,
24+
margin: partialMargin,
25+
width,
26+
height,
27+
borderWidth = svgDefaultProps.borderWidth,
28+
borderColor = svgDefaultProps.borderColor,
29+
gridLevels = svgDefaultProps.gridLevels,
30+
gridShape = svgDefaultProps.gridShape,
31+
gridLabel = svgDefaultProps.gridLabel,
32+
gridLabelOffset = svgDefaultProps.gridLabelOffset,
33+
enableDots = svgDefaultProps.enableDots,
34+
dotSymbol,
35+
dotSize,
36+
dotColor,
37+
dotBorderWidth,
38+
dotBorderColor,
39+
enableDotLabel,
40+
dotLabel,
41+
dotLabelFormat,
42+
dotLabelYOffset,
43+
colors = svgDefaultProps.colors,
44+
fillOpacity = svgDefaultProps.fillOpacity,
45+
blendMode = svgDefaultProps.blendMode,
46+
isInteractive = svgDefaultProps.isInteractive,
47+
tooltipFormat,
48+
legends = svgDefaultProps.legends,
49+
role,
50+
ariaLabel,
51+
ariaLabelledBy,
52+
ariaDescribedBy,
53+
}: InnerRadarProps<D>) => {
54+
const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions(
55+
width,
56+
height,
57+
partialMargin
58+
)
59+
60+
const {
61+
getIndex,
62+
indices,
63+
colorByKey,
64+
radius,
65+
radiusScale,
66+
centerX,
67+
centerY,
68+
angleStep,
69+
curveInterpolator,
70+
legendData,
71+
} = useRadar<D>({
72+
data,
73+
keys,
74+
indexBy,
75+
maxValue,
76+
curve,
77+
width: innerWidth,
78+
height: innerHeight,
79+
colors,
80+
})
81+
82+
const layerById: Record<RadarLayerId, ReactNode> = {
83+
grid: null,
84+
shapes: null,
85+
dots: null,
86+
legends: null,
87+
}
88+
89+
if (layers.includes('grid')) {
90+
layerById.grid = (
91+
<g key="grid" transform={`translate(${centerX}, ${centerY})`}>
92+
<RadarGrid<D>
93+
levels={gridLevels}
94+
shape={gridShape}
95+
radius={radius}
96+
angleStep={angleStep}
97+
indices={indices}
98+
label={gridLabel}
99+
labelOffset={gridLabelOffset}
100+
/>
101+
</g>
102+
)
103+
}
104+
105+
if (layers.includes('shapes')) {
106+
layerById.shapes = (
107+
<g key="shapes" transform={`translate(${centerX}, ${centerY})`}>
108+
{keys.map(key => (
109+
<RadarShapes<D>
110+
key={key}
111+
data={data}
112+
item={key}
113+
colorByKey={colorByKey}
114+
radiusScale={radiusScale}
115+
angleStep={angleStep}
116+
curveInterpolator={curveInterpolator}
117+
borderWidth={borderWidth}
118+
borderColor={borderColor}
119+
fillOpacity={fillOpacity}
120+
blendMode={blendMode}
121+
/>
122+
))}
123+
</g>
124+
)
125+
}
126+
127+
if (layers.includes('dots') && enableDots) {
128+
layerById.dots = (
129+
<g key="dots" transform={`translate(${centerX}, ${centerY})`}>
130+
<RadarDots<D>
131+
data={data}
132+
keys={keys}
133+
getIndex={getIndex}
134+
radiusScale={radiusScale}
135+
angleStep={angleStep}
136+
symbol={dotSymbol}
137+
size={dotSize}
138+
colorByKey={colorByKey}
139+
color={dotColor}
140+
borderWidth={dotBorderWidth}
141+
borderColor={dotBorderColor}
142+
enableLabel={enableDotLabel}
143+
label={dotLabel}
144+
labelFormat={dotLabelFormat}
145+
labelYOffset={dotLabelYOffset}
146+
/>
147+
</g>
148+
)
149+
}
150+
151+
if (layers.includes('legends')) {
152+
layerById.legends = (
153+
<Fragment key="legends">
154+
{legends.map((legend, i) => (
155+
<BoxLegendSvg
156+
key={i}
157+
{...legend}
158+
containerWidth={width}
159+
containerHeight={height}
160+
data={legendData}
161+
/>
162+
))}
163+
</Fragment>
164+
)
165+
}
166+
167+
return (
168+
<SvgWrapper
169+
width={outerWidth}
170+
height={outerHeight}
171+
margin={margin}
172+
role={role}
173+
ariaLabel={ariaLabel}
174+
ariaLabelledBy={ariaLabelledBy}
175+
ariaDescribedBy={ariaDescribedBy}
176+
>
177+
{layerById.grid}
178+
{layerById.shapes}
179+
{isInteractive && (
180+
<g transform={`translate(${centerX}, ${centerY})`}>
181+
<RadarTooltip<D>
182+
data={data}
183+
keys={keys}
184+
getIndex={getIndex}
185+
colorByKey={colorByKey}
186+
radius={radius}
187+
angleStep={angleStep}
188+
tooltipFormat={tooltipFormat}
189+
/>
190+
</g>
191+
)}
192+
{layerById.dots}
193+
{layerById.legends}
194+
</SvgWrapper>
195+
)
196+
}
197+
198+
export const Radar = <D extends Record<string, unknown>>({
199+
isInteractive = svgDefaultProps.isInteractive,
200+
animate = svgDefaultProps.animate,
201+
motionConfig = svgDefaultProps.motionConfig,
202+
theme,
203+
renderWrapper,
204+
...otherProps
205+
}: RadarSvgProps<D>) => (
206+
<Container
207+
{...{
208+
animate,
209+
isInteractive,
210+
motionConfig,
211+
renderWrapper,
212+
theme,
213+
}}
214+
>
215+
<InnerRadar<D> isInteractive={isInteractive} {...otherProps} />
216+
</Container>
217+
)

‎packages/radar/src/RadarDots.js

-121
This file was deleted.

‎packages/radar/src/RadarDots.tsx

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { useMemo, ReactNode } from 'react'
2+
import {
3+
useTheme,
4+
positionFromAngle,
5+
// @ts-ignore: core package should be converted to TypeScript
6+
getLabelGenerator,
7+
// @ts-ignore: core package should be converted to TypeScript
8+
DotsItem,
9+
} from '@nivo/core'
10+
import { InheritedColorConfig, getInheritedColorGenerator } from '@nivo/colors'
11+
import { RadarDataProps } from './types'
12+
import { ScaleLinear } from 'd3-scale'
13+
14+
interface RadarDotsProps<D extends Record<string, unknown>> {
15+
data: RadarDataProps<D>['data']
16+
keys: RadarDataProps<D>['keys']
17+
radiusScale: ScaleLinear<number, number>
18+
getIndex: (d: D) => string | number
19+
colorByKey: Record<string | number, string>
20+
angleStep: number
21+
// symbol: PropTypes.func,
22+
size?: number
23+
color?: InheritedColorConfig<any>
24+
borderWidth?: number
25+
borderColor?: InheritedColorConfig<any>
26+
enableLabel?: boolean
27+
// label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
28+
// labelFormat: PropTypes.string,
29+
labelYOffset?: number
30+
}
31+
32+
interface Point {
33+
key: string
34+
label: ReactNode | null
35+
data: {
36+
index: string | number
37+
key: string | number
38+
value: number
39+
color: string
40+
}
41+
style: {
42+
fill: string
43+
stroke: string
44+
x: number
45+
y: number
46+
}
47+
}
48+
49+
export const RadarDots = <D extends Record<string, unknown>>({
50+
data,
51+
keys,
52+
getIndex,
53+
colorByKey,
54+
radiusScale,
55+
angleStep,
56+
symbol,
57+
size = 6,
58+
color = { from: 'color' },
59+
borderWidth = 0,
60+
borderColor = { from: 'color' },
61+
enableLabel = false,
62+
label = 'value',
63+
labelFormat,
64+
labelYOffset,
65+
}: RadarDotsProps<D>) => {
66+
const theme = useTheme()
67+
const fillColor = getInheritedColorGenerator(color, theme)
68+
const strokeColor = getInheritedColorGenerator(borderColor, theme)
69+
const getLabel = getLabelGenerator(label, labelFormat)
70+
71+
const points: Point[] = useMemo(
72+
() =>
73+
data.reduce((acc, datum, i) => {
74+
const index = getIndex(datum)
75+
keys.forEach(key => {
76+
const pointData: Point['data'] = {
77+
index,
78+
key,
79+
value: datum[key] as number,
80+
color: colorByKey[key],
81+
}
82+
acc.push({
83+
key: `${key}.${index}`,
84+
label: enableLabel ? getLabel(pointData) : null,
85+
style: {
86+
fill: fillColor(pointData),
87+
stroke: strokeColor(pointData),
88+
...positionFromAngle(
89+
angleStep * i - Math.PI / 2,
90+
radiusScale(datum[key] as number)
91+
),
92+
},
93+
data: pointData,
94+
})
95+
})
96+
97+
return acc
98+
}, [] as Point[]),
99+
[
100+
data,
101+
getIndex,
102+
colorByKey,
103+
enableLabel,
104+
getLabel,
105+
fillColor,
106+
strokeColor,
107+
angleStep,
108+
radiusScale,
109+
]
110+
)
111+
112+
return (
113+
<>
114+
{points.map(point => (
115+
<DotsItem
116+
key={point.key}
117+
x={point.style.x}
118+
y={point.style.y}
119+
symbol={symbol}
120+
size={size}
121+
color={point.style.fill}
122+
borderWidth={borderWidth}
123+
borderColor={point.style.stroke}
124+
label={point.label}
125+
labelYOffset={labelYOffset}
126+
theme={theme}
127+
datum={point.data}
128+
/>
129+
))}
130+
</>
131+
)
132+
}
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1-
/*
2-
* This file is part of the nivo project.
3-
*
4-
* Copyright 2016-present, Raphaël Benitte.
5-
*
6-
* For the full copyright and license information, please view the LICENSE
7-
* file that was distributed with this source code.
8-
*/
9-
import { memo, useMemo } from 'react'
10-
import PropTypes from 'prop-types'
1+
import { useMemo } from 'react'
112
import { positionFromAngle, useTheme } from '@nivo/core'
12-
import RadialGridLabels from './RadarGridLabels'
13-
import RadarGridLevels from './RadarGridLevels'
3+
import { RadarGridLabels } from './RadarGridLabels'
4+
import { RadarGridLevels } from './RadarGridLevels'
5+
import { GridLabelComponent, RadarCommonProps } from './types'
146

15-
const RadarGrid = memo(({ indices, levels, shape, radius, angleStep, label, labelOffset }) => {
7+
interface RadarGridProps<D extends Record<string, unknown>> {
8+
indices: string[] | number[]
9+
shape: RadarCommonProps<D>['gridShape']
10+
radius: number
11+
levels: number
12+
angleStep: number
13+
label: GridLabelComponent
14+
labelOffset: number
15+
}
16+
17+
export const RadarGrid = <D extends Record<string, unknown>>({
18+
indices,
19+
levels,
20+
shape,
21+
radius,
22+
angleStep,
23+
label,
24+
labelOffset,
25+
}: RadarGridProps<D>) => {
1626
const theme = useTheme()
1727
const { radii, angles } = useMemo(() => {
1828
return {
@@ -34,7 +44,7 @@ const RadarGrid = memo(({ indices, levels, shape, radius, angleStep, label, labe
3444
y1={0}
3545
x2={position.x}
3646
y2={position.y}
37-
{...theme.grid.line}
47+
{...(theme.grid.line as any)}
3848
/>
3949
)
4050
})}
@@ -47,7 +57,7 @@ const RadarGrid = memo(({ indices, levels, shape, radius, angleStep, label, labe
4757
dataLength={indices.length}
4858
/>
4959
))}
50-
<RadialGridLabels
60+
<RadarGridLabels
5161
radius={radius}
5262
angles={angles}
5363
indices={indices}
@@ -56,18 +66,4 @@ const RadarGrid = memo(({ indices, levels, shape, radius, angleStep, label, labe
5666
/>
5767
</g>
5868
)
59-
})
60-
61-
RadarGrid.displayName = 'RadarGrid'
62-
RadarGrid.propTypes = {
63-
indices: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))
64-
.isRequired,
65-
shape: PropTypes.oneOf(['circular', 'linear']).isRequired,
66-
radius: PropTypes.number.isRequired,
67-
levels: PropTypes.number.isRequired,
68-
angleStep: PropTypes.number.isRequired,
69-
label: PropTypes.func,
70-
labelOffset: PropTypes.number.isRequired,
7169
}
72-
73-
export default RadarGrid

‎packages/radar/src/RadarGridLabel.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { animated } from '@react-spring/web'
2+
import { useTheme } from '@nivo/core'
3+
import { GridLabelProps } from './types'
4+
5+
export const RadarGridLabel = ({ id, anchor, animated: animatedProps }: GridLabelProps) => {
6+
const theme = useTheme()
7+
8+
return (
9+
<animated.g transform={animatedProps.transform}>
10+
<text style={theme.axis.ticks.text} dominantBaseline="central" textAnchor={anchor}>
11+
{id}
12+
</text>
13+
</animated.g>
14+
)
15+
}

‎packages/radar/src/RadarGridLabels.js

-83
This file was deleted.
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { createElement } from 'react'
2+
import { useSprings } from '@react-spring/web'
3+
import { useMotionConfig, positionFromAngle, radiansToDegrees } from '@nivo/core'
4+
import { GridLabelComponent } from './types'
5+
6+
const textAnchorFromAngle = (_angle: number) => {
7+
const angle = radiansToDegrees(_angle) + 90
8+
9+
if (angle <= 10 || angle >= 350 || (angle >= 170 && angle <= 190)) return 'middle' as const
10+
if (angle > 180) return 'end' as const
11+
return 'start' as const
12+
}
13+
14+
interface RadarGridLabelsProps {
15+
radius: number
16+
angles: number[]
17+
indices: string[] | number[]
18+
label: GridLabelComponent
19+
labelOffset: number
20+
}
21+
22+
export const RadarGridLabels = ({
23+
radius,
24+
angles,
25+
indices,
26+
label: labelComponent,
27+
labelOffset,
28+
}: RadarGridLabelsProps) => {
29+
const { animate, config: springConfig } = useMotionConfig()
30+
31+
const labels = indices.map((index, i) => {
32+
const position = positionFromAngle(angles[i], radius + labelOffset)
33+
const textAnchor = textAnchorFromAngle(angles[i])
34+
35+
return {
36+
id: index,
37+
angle: radiansToDegrees(angles[i]),
38+
anchor: textAnchor,
39+
...position,
40+
}
41+
})
42+
43+
const springs = useSprings(
44+
labels.length,
45+
labels.map(label => ({
46+
transform: `translate(${label.x}, ${label.y})`,
47+
config: springConfig,
48+
immediate: !animate,
49+
}))
50+
)
51+
52+
return (
53+
<>
54+
{springs.map((animatedProps, index) => {
55+
const label = labels[index]
56+
57+
return createElement(labelComponent, {
58+
key: label.id,
59+
id: label.id,
60+
anchor: label.anchor,
61+
angle: label.angle,
62+
x: label.x,
63+
y: label.y,
64+
animated: animatedProps,
65+
})
66+
})}
67+
</>
68+
)
69+
}

‎packages/radar/src/RadarGridLevels.js

-80
This file was deleted.
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { memo, useMemo } from 'react'
2+
import { lineRadial, curveLinearClosed } from 'd3-shape'
3+
import { animated, useSpring, to } from '@react-spring/web'
4+
import { useTheme, useAnimatedPath, useMotionConfig } from '@nivo/core'
5+
import { RadarCommonProps } from './types'
6+
7+
interface RadarGridLevelCircularProps {
8+
radius: number
9+
}
10+
11+
const RadarGridLevelCircular = memo(({ radius }: RadarGridLevelCircularProps) => {
12+
const theme = useTheme()
13+
const { animate, config: springConfig } = useMotionConfig()
14+
15+
const animatedProps = useSpring({
16+
radius,
17+
config: springConfig,
18+
immediate: !animate,
19+
})
20+
21+
return (
22+
<animated.circle
23+
fill="none"
24+
r={to(animatedProps.radius, value => Math.max(value, 0))}
25+
{...(theme.grid.line as any)}
26+
/>
27+
)
28+
})
29+
30+
interface RadarGridLevelLinearProps {
31+
radius: number
32+
angleStep: number
33+
dataLength: number
34+
}
35+
36+
const RadarGridLevelLinear = ({ radius, angleStep, dataLength }: RadarGridLevelLinearProps) => {
37+
const theme = useTheme()
38+
39+
const radarLineGenerator = useMemo(
40+
() =>
41+
lineRadial<number>()
42+
.angle(i => i * angleStep)
43+
.radius(radius)
44+
.curve(curveLinearClosed),
45+
[angleStep, radius]
46+
)
47+
48+
const points = Array.from({ length: dataLength }, (_, i) => i)
49+
const animatedPath = useAnimatedPath(radarLineGenerator(points) as string)
50+
51+
return <animated.path fill="none" d={animatedPath} {...(theme.grid.line as any)} />
52+
}
53+
54+
interface RadarGridLevelsProps {
55+
shape: RadarCommonProps<any>['gridShape']
56+
radius: number
57+
angleStep: number
58+
dataLength: number
59+
}
60+
61+
export const RadarGridLevels = ({ shape, ...props }: RadarGridLevelsProps) => {
62+
return shape === 'circular' ? (
63+
<RadarGridLevelCircular radius={props.radius} />
64+
) : (
65+
<RadarGridLevelLinear {...props} />
66+
)
67+
}

‎packages/radar/src/RadarShapes.js

-80
This file was deleted.

‎packages/radar/src/RadarShapes.tsx

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useMemo } from 'react'
2+
import { useSpring, animated } from '@react-spring/web'
3+
import { lineRadial, CurveFactory } from 'd3-shape'
4+
import { ScaleLinear } from 'd3-scale'
5+
import { useMotionConfig, useTheme, useAnimatedPath } from '@nivo/core'
6+
import { useInheritedColor } from '@nivo/colors'
7+
import { RadarCommonProps } from './types'
8+
9+
interface RadarShapesProps<D extends Record<string, unknown>> {
10+
data: D[]
11+
item: string | number
12+
colorByKey: Record<string | number, string>
13+
radiusScale: ScaleLinear<number, number>
14+
angleStep: number
15+
curveInterpolator: CurveFactory
16+
borderWidth: RadarCommonProps<D>['borderWidth']
17+
borderColor: RadarCommonProps<D>['borderColor']
18+
fillOpacity: RadarCommonProps<D>['fillOpacity']
19+
blendMode: RadarCommonProps<D>['blendMode']
20+
}
21+
22+
export const RadarShapes = <D extends Record<string, unknown>>({
23+
data,
24+
item: key,
25+
colorByKey,
26+
radiusScale,
27+
angleStep,
28+
curveInterpolator,
29+
borderWidth,
30+
borderColor,
31+
fillOpacity,
32+
blendMode,
33+
}: RadarShapesProps<D>) => {
34+
const theme = useTheme()
35+
const getBorderColor = useInheritedColor(borderColor, theme)
36+
37+
const lineGenerator = useMemo(() => {
38+
return lineRadial<number>()
39+
.radius(d => radiusScale(d))
40+
.angle((_, i) => i * angleStep)
41+
.curve(curveInterpolator)
42+
}, [radiusScale, angleStep, curveInterpolator])
43+
44+
const { animate, config: springConfig } = useMotionConfig()
45+
const animatedPath = useAnimatedPath(lineGenerator(data.map(d => d[key])))
46+
const animatedProps = useSpring<{ fill: string; stroke: string }>({
47+
fill: colorByKey[key],
48+
stroke: getBorderColor({ key, color: colorByKey[key] }),
49+
config: springConfig,
50+
immediate: !animate,
51+
})
52+
53+
return (
54+
<animated.path
55+
key={key}
56+
d={animatedPath}
57+
fill={animatedProps.fill}
58+
fillOpacity={fillOpacity}
59+
stroke={animatedProps.stroke}
60+
strokeWidth={borderWidth}
61+
style={{ mixBlendMode: blendMode }}
62+
/>
63+
)
64+
}

‎packages/radar/src/RadarTooltip.js

-63
This file was deleted.

‎packages/radar/src/RadarTooltip.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { arc as d3Arc } from 'd3-shape'
2+
import { RadarTooltipItem } from './RadarTooltipItem'
3+
import { RadarDataProps } from './types'
4+
5+
interface RadarTooltipProps<D extends Record<string, unknown>> {
6+
data: RadarDataProps<D>['data']
7+
keys: RadarDataProps<D>['keys']
8+
getIndex: (d: D) => string | number
9+
colorByKey: Record<string | number, string>
10+
radius: number
11+
angleStep: number
12+
tooltipFormat: any
13+
// tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
14+
}
15+
16+
export const RadarTooltip = <D extends Record<string, unknown>>({
17+
data,
18+
keys,
19+
getIndex,
20+
colorByKey,
21+
radius,
22+
angleStep,
23+
tooltipFormat,
24+
}: RadarTooltipProps<D>) => {
25+
const arc = d3Arc<{ startAngle: number; endAngle: number }>().outerRadius(radius).innerRadius(0)
26+
27+
const halfAngleStep = angleStep * 0.5
28+
let rootStartAngle = -halfAngleStep
29+
30+
return (
31+
<g>
32+
{data.map(d => {
33+
const index = getIndex(d)
34+
const startAngle = rootStartAngle
35+
const endAngle = startAngle + angleStep
36+
37+
rootStartAngle += angleStep
38+
39+
return (
40+
<RadarTooltipItem
41+
key={index}
42+
datum={d}
43+
keys={keys}
44+
index={index}
45+
colorByKey={colorByKey}
46+
startAngle={startAngle}
47+
endAngle={endAngle}
48+
radius={radius}
49+
arcGenerator={arc}
50+
tooltipFormat={tooltipFormat}
51+
/>
52+
)
53+
})}
54+
</g>
55+
)
56+
}

‎packages/radar/src/RadarTooltipItem.js

-99
This file was deleted.
+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useMemo, useState, useCallback } from 'react'
2+
import { Arc } from 'd3-shape'
3+
import { positionFromAngle, useTheme, useValueFormatter } from '@nivo/core'
4+
import { TableTooltip, Chip, useTooltip } from '@nivo/tooltip'
5+
import { RadarDataProps } from './types'
6+
7+
interface RadarTooltipItemProps<D extends Record<string, unknown>> {
8+
datum: D
9+
keys: RadarDataProps<D>['keys']
10+
index: string | number
11+
colorByKey: Record<string | number, string>
12+
startAngle: number
13+
endAngle: number
14+
radius: number
15+
arcGenerator: Arc<any, { startAngle: number; endAngle: number }>
16+
// tooltipFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
17+
}
18+
19+
export const RadarTooltipItem = <D extends Record<string, unknown>>({
20+
datum,
21+
keys,
22+
index,
23+
colorByKey,
24+
radius,
25+
startAngle,
26+
endAngle,
27+
arcGenerator,
28+
tooltipFormat,
29+
}: RadarTooltipItemProps<D>) => {
30+
const [isHover, setIsHover] = useState(false)
31+
const theme = useTheme()
32+
const { showTooltipFromEvent, hideTooltip } = useTooltip()
33+
34+
const tooltipFormatter = useValueFormatter(tooltipFormat)
35+
const tooltip = useMemo(() => {
36+
const rows = keys.map(key => [
37+
<Chip key={key} color={colorByKey[key]} />,
38+
key,
39+
tooltipFormatter(datum[key], key),
40+
])
41+
rows.sort((a, b) => a[2] - b[2])
42+
rows.reverse()
43+
44+
return <TableTooltip title={<strong>{index}</strong>} rows={rows} theme={theme} />
45+
}, [datum, keys, index, colorByKey, theme, tooltipFormatter])
46+
const showItemTooltip = useCallback(
47+
event => {
48+
setIsHover(true)
49+
showTooltipFromEvent(tooltip, event)
50+
},
51+
[showTooltipFromEvent, tooltip]
52+
)
53+
const hideItemTooltip = useCallback(() => {
54+
setIsHover(false)
55+
hideTooltip()
56+
}, [hideTooltip, setIsHover])
57+
58+
const { path, tipX, tipY } = useMemo(() => {
59+
const position = positionFromAngle(
60+
startAngle + (endAngle - startAngle) * 0.5 - Math.PI / 2,
61+
radius
62+
)
63+
64+
return {
65+
path: arcGenerator({ startAngle, endAngle }) as string,
66+
tipX: position.x,
67+
tipY: position.y,
68+
}
69+
}, [startAngle, endAngle, radius, arcGenerator])
70+
71+
return (
72+
<>
73+
{isHover && <line x1={0} y1={0} x2={tipX} y2={tipY} style={theme.crosshair.line} />}
74+
<path
75+
d={path}
76+
fill="#F00"
77+
fillOpacity={0}
78+
onMouseEnter={showItemTooltip}
79+
onMouseMove={showItemTooltip}
80+
onMouseLeave={hideItemTooltip}
81+
/>
82+
</>
83+
)
84+
}

‎packages/radar/src/ResponsiveRadar.js

-18
This file was deleted.
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { ResponsiveWrapper } from '@nivo/core'
2+
import { RadarSvgProps } from './types'
3+
import { Radar } from './Radar'
4+
5+
export const ResponsiveRadar = <D extends Record<string, unknown>>(
6+
props: Omit<RadarSvgProps<D>, 'height' | 'width'>
7+
) => (
8+
<ResponsiveWrapper>
9+
{({ width, height }) => <Radar<D> width={width} height={height} {...props} />}
10+
</ResponsiveWrapper>
11+
)

‎packages/radar/src/hooks.ts

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useMemo } from 'react'
2+
import { scaleLinear } from 'd3-scale'
3+
import {
4+
// @ts-ignore
5+
useCurveInterpolation,
6+
usePropertyAccessor,
7+
} from '@nivo/core'
8+
import { useOrdinalColorScale } from '@nivo/colors'
9+
import { svgDefaultProps } from './props'
10+
import { RadarCommonProps, RadarDataProps } from './types'
11+
12+
export const useRadar = <D extends Record<string, unknown>>({
13+
data,
14+
keys,
15+
indexBy,
16+
maxValue,
17+
curve,
18+
width,
19+
height,
20+
colors = svgDefaultProps.colors,
21+
}: {
22+
data: RadarDataProps<D>['data']
23+
keys: RadarDataProps<D>['keys']
24+
indexBy: RadarDataProps<D>['indexBy']
25+
maxValue: RadarCommonProps<D>['maxValue']
26+
curve: RadarCommonProps<D>['curve']
27+
width: number
28+
height: number
29+
colors: RadarCommonProps<D>['colors']
30+
}) => {
31+
const getIndex = usePropertyAccessor<D, string | number>(indexBy)
32+
const indices = useMemo(() => data.map(getIndex), [data, getIndex]) as string[] | number[]
33+
34+
const getColor = useOrdinalColorScale<{ key: string | number; index: number }>(colors, 'key')
35+
const colorByKey: Record<string | number, string> = useMemo(
36+
() =>
37+
keys.reduce((mapping: Record<string | number, string>, key: string | number, index: number) => {
38+
mapping[key] = getColor({ key, index })
39+
return mapping
40+
}, {} as Record<string | number, string>),
41+
[keys, getColor]
42+
)
43+
44+
const { radius, radiusScale, centerX, centerY, angleStep } = useMemo(() => {
45+
const computedMaxValue =
46+
maxValue !== 'auto'
47+
? maxValue
48+
: Math.max(...data.reduce((acc, d) => [...acc, ...keys.map(key => d[key])], []))
49+
50+
const radius = Math.min(width, height) / 2
51+
const radiusScale = scaleLinear<number, number>()
52+
.range([0, radius])
53+
.domain([0, computedMaxValue])
54+
55+
return {
56+
radius,
57+
radiusScale,
58+
centerX: width / 2,
59+
centerY: height / 2,
60+
angleStep: (Math.PI * 2) / data.length,
61+
}
62+
}, [keys, indexBy, data, maxValue, width, height])
63+
64+
const curveInterpolator = useCurveInterpolation(curve)
65+
66+
const legendData = keys.map(key => ({
67+
id: key,
68+
label: key,
69+
color: colorByKey[key],
70+
}))
71+
72+
return {
73+
getIndex,
74+
indices,
75+
colorByKey,
76+
radius,
77+
radiusScale,
78+
centerX,
79+
centerY,
80+
angleStep,
81+
curveInterpolator,
82+
legendData,
83+
}
84+
}

‎packages/radar/src/index.js

-13
This file was deleted.

‎packages/radar/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from './Radar'
2+
export * from './ResponsiveRadar'
3+
export * from './RadarDots'
4+
export * from './props'
5+
export * from './types'

‎packages/radar/src/props.js

-80
This file was deleted.

‎packages/radar/src/props.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { RadarGridLabel } from './RadarGridLabel'
2+
import { RadarLayerId } from './types'
3+
4+
export const svgDefaultProps = {
5+
layers: ['grid', 'shapes', 'dots', 'legends'] as RadarLayerId[],
6+
7+
maxValue: 'auto' as const,
8+
9+
curve: 'linearClosed',
10+
11+
borderWidth: 2,
12+
borderColor: { from: 'color' },
13+
14+
gridLevels: 5,
15+
gridShape: 'circular' as const,
16+
gridLabelOffset: 16,
17+
gridLabel: RadarGridLabel,
18+
19+
enableDots: true,
20+
21+
colors: { scheme: 'nivo' as const },
22+
fillOpacity: 0.25,
23+
blendMode: 'normal' as const,
24+
25+
isInteractive: true,
26+
27+
legends: [],
28+
role: 'img',
29+
30+
animate: true,
31+
motionConfig: 'gentle' as const,
32+
}

‎packages/radar/src/types.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { FunctionComponent, AriaAttributes, ReactNode } from 'react'
2+
import { AnimatedProps } from '@react-spring/web'
3+
import {
4+
Box,
5+
Theme,
6+
CssMixBlendMode,
7+
Dimensions,
8+
ModernMotionProps,
9+
PropertyAccessor,
10+
ValueFormat,
11+
} from '@nivo/core'
12+
import { InheritedColorConfig, OrdinalColorScaleConfig } from '@nivo/colors'
13+
import { LegendProps } from '@nivo/legends'
14+
15+
export interface RadarDataProps<D extends Record<string, unknown>> {
16+
data: D[]
17+
keys: string[] | number[]
18+
indexBy: PropertyAccessor<D, string | number>
19+
}
20+
21+
export interface GridLabelProps {
22+
id: string | number
23+
anchor: 'start' | 'middle' | 'end'
24+
angle: number
25+
x: number
26+
y: number
27+
animated: AnimatedProps<{
28+
transform: string
29+
}>
30+
}
31+
export type GridLabelComponent = FunctionComponent<GridLabelProps>
32+
33+
export type PointData = {
34+
index: number
35+
key: string
36+
value: number
37+
color: string
38+
}
39+
40+
export interface DotSymbolProps {
41+
size: number
42+
color: InheritedColorConfig<PointData>
43+
borderWidth: number
44+
borderColor: InheritedColorConfig<PointData>
45+
}
46+
export type DotSymbolComponent = FunctionComponent<DotSymbolProps>
47+
48+
export type RadarLayerId = 'grid' | 'shapes' | 'dots' | 'legends'
49+
50+
export interface RadarCommonProps<D extends Record<string, unknown>> {
51+
maxValue: number | 'auto'
52+
53+
layers: RadarLayerId[]
54+
55+
margin: Box
56+
57+
curve: string // closedCurvePropType.isRequired,
58+
59+
gridLevels: number
60+
gridShape: 'circular' | 'linear'
61+
gridLabel: GridLabelComponent
62+
gridLabelOffset: number
63+
64+
enableDots: boolean
65+
dotSymbol: DotSymbolComponent
66+
dotSize: number
67+
dotColor: InheritedColorConfig<PointData>
68+
dotBorderWidth: number
69+
dotBorderColor: InheritedColorConfig<PointData>
70+
enableDotLabel: boolean
71+
dotLabel: PropertyAccessor<D, ReactNode>
72+
dotLabelFormat: ValueFormat<number>
73+
dotLabelYOffset: number
74+
75+
theme: Theme
76+
colors: OrdinalColorScaleConfig<{ key: string | number; index: number }>
77+
fillOpacity: number
78+
blendMode: CssMixBlendMode
79+
borderWidth: number
80+
borderColor: InheritedColorConfig<{ key: string | number; color: string }>
81+
82+
isInteractive: boolean
83+
tooltipFormat: ValueFormat<number>
84+
85+
renderWrapper: boolean
86+
87+
legends: LegendProps[]
88+
89+
role: string
90+
ariaLabel: AriaAttributes['aria-label']
91+
ariaLabelledBy: AriaAttributes['aria-labelledby']
92+
ariaDescribedBy: AriaAttributes['aria-describedby']
93+
}
94+
95+
export type RadarSvgProps<D extends Record<string, unknown>> = Partial<RadarCommonProps<D>> &
96+
RadarDataProps<D> &
97+
Dimensions &
98+
ModernMotionProps

‎packages/radar/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+
}

‎website/src/@types/file_types.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ declare module '*funnel/meta.yml' {
1414
export default meta
1515
}
1616

17+
declare module '*radar/meta.yml' {
18+
const meta: {
19+
flavors: ChartMetaFlavors
20+
Radar: ChartMeta
21+
}
22+
23+
export default meta
24+
}
25+
1726
declare module '*sankey/meta.yml' {
1827
const meta: {
1928
flavors: ChartMetaFlavors

‎website/src/data/components/radar/mapper.js

-13
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { settingsMapper } from '../../../lib/settings'
2+
3+
export default settingsMapper({})

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

+94-71
Large diffs are not rendered by default.

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

+24-31
Original file line numberDiff line numberDiff line change
@@ -70,36 +70,29 @@ const initialProperties = {
7070
],
7171
}
7272

73-
const Radar = () => {
74-
return (
75-
<ComponentTemplate
76-
name="Radar"
77-
meta={meta.Radar}
78-
icon="radar"
79-
flavors={meta.flavors}
80-
currentFlavor="svg"
81-
properties={groups}
82-
initialProperties={initialProperties}
83-
propertiesMapper={mapper}
84-
codePropertiesMapper={(properties, data) => ({
85-
keys: data.keys,
86-
...properties,
87-
})}
88-
generateData={generateWinesTastes}
89-
getTabData={data => data.data}
90-
>
91-
{(properties, data, theme) => {
92-
return (
93-
<ResponsiveRadar
94-
data={data.data}
95-
keys={data.keys}
96-
{...properties}
97-
theme={theme}
98-
/>
99-
)
100-
}}
101-
</ComponentTemplate>
102-
)
103-
}
73+
const Radar = () => (
74+
<ComponentTemplate
75+
name="Radar"
76+
meta={meta.Radar}
77+
icon="radar"
78+
flavors={meta.flavors}
79+
currentFlavor="svg"
80+
properties={groups}
81+
initialProperties={initialProperties}
82+
propertiesMapper={mapper}
83+
codePropertiesMapper={(properties, data) => ({
84+
keys: data.keys,
85+
...properties,
86+
})}
87+
generateData={generateWinesTastes}
88+
getTabData={data => data.data}
89+
>
90+
{(properties, data, theme) => {
91+
return (
92+
<ResponsiveRadar data={data.data} keys={data.keys} {...properties} theme={theme} />
93+
)
94+
}}
95+
</ComponentTemplate>
96+
)
10497

10598
export default Radar

0 commit comments

Comments
 (0)
Please sign in to comment.