Skip to content

Commit

Permalink
feat(radar): add support for global rotation to the radar chart (#1985)
Browse files Browse the repository at this point in the history
* implementing global rotatation in radar

* rename prop from 'angle' to 'rotation' + test for global rotation

* reformat rotation test

* format of a file in package 'line' (required for all-package lint CI)
  • Loading branch information
tkonopka authored May 10, 2022
1 parent c40006f commit d57345a
Show file tree
Hide file tree
Showing 12 changed files with 79 additions and 22 deletions.
27 changes: 15 additions & 12 deletions packages/line/src/Points.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,22 @@ const Points = ({ points, symbol, size, borderWidth, enableLabel, label, labelYO
* We reverse the `points` array so that points from the lower lines in stacked lines
* graph are drawn on top. See https://github.com/plouc/nivo/issues/1051.
*/
const mappedPoints = points.slice(0).reverse().map(point => {
const mappedPoint = {
id: point.id,
x: point.x,
y: point.y,
datum: point.data,
fill: point.color,
stroke: point.borderColor,
label: enableLabel ? getLabel(point.data) : null,
}
const mappedPoints = points
.slice(0)
.reverse()
.map(point => {
const mappedPoint = {
id: point.id,
x: point.x,
y: point.y,
datum: point.data,
fill: point.color,
stroke: point.borderColor,
label: enableLabel ? getLabel(point.data) : null,
}

return mappedPoint
})
return mappedPoint
})

return (
<g>
Expand Down
7 changes: 7 additions & 0 deletions packages/radar/src/Radar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
keys,
indexBy,
layers = svgDefaultProps.layers,
rotation: rotationDegrees = svgDefaultProps.rotation,
maxValue = svgDefaultProps.maxValue,
valueFormat,
curve = svgDefaultProps.curve,
Expand Down Expand Up @@ -66,6 +67,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
colorByKey,
fillByKey,
boundDefs,
rotation,
radius,
radiusScale,
centerX,
Expand All @@ -78,6 +80,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
data,
keys,
indexBy,
rotationDegrees,
maxValue,
valueFormat,
curve,
Expand All @@ -104,6 +107,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
levels={gridLevels}
shape={gridShape}
radius={radius}
rotation={rotation}
angleStep={angleStep}
indices={indices}
label={gridLabel}
Expand All @@ -124,6 +128,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
colorByKey={colorByKey}
fillByKey={fillByKey}
radiusScale={radiusScale}
rotation={rotation}
angleStep={angleStep}
curveFactory={curveFactory}
borderWidth={borderWidth}
Expand All @@ -146,6 +151,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
formatValue={formatValue}
colorByKey={colorByKey}
radius={radius}
rotation={rotation}
angleStep={angleStep}
tooltip={sliceTooltip}
/>
Expand All @@ -161,6 +167,7 @@ const InnerRadar = <D extends Record<string, unknown>>({
keys={keys}
getIndex={getIndex}
radiusScale={radiusScale}
rotation={rotation}
angleStep={angleStep}
symbol={dotSymbol}
size={dotSize}
Expand Down
5 changes: 4 additions & 1 deletion packages/radar/src/RadarDots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface RadarDotsProps<D extends Record<string, unknown>> {
radiusScale: ScaleLinear<number, number>
getIndex: (d: D) => string
colorByKey: RadarColorMapping
rotation: number
angleStep: number
symbol?: RadarCommonProps<D>['dotSymbol']
size: number
Expand All @@ -28,6 +29,7 @@ export const RadarDots = <D extends Record<string, unknown>>({
getIndex,
colorByKey,
radiusScale,
rotation,
angleStep,
symbol,
size = 6,
Expand Down Expand Up @@ -66,7 +68,7 @@ export const RadarDots = <D extends Record<string, unknown>>({
fill: fillColor(pointData),
stroke: strokeColor(pointData),
...positionFromAngle(
angleStep * i - Math.PI / 2,
rotation + angleStep * i - Math.PI / 2,
radiusScale(datum[key] as number)
),
},
Expand All @@ -86,6 +88,7 @@ export const RadarDots = <D extends Record<string, unknown>>({
formatValue,
fillColor,
strokeColor,
rotation,
angleStep,
radiusScale,
]
Expand Down
9 changes: 7 additions & 2 deletions packages/radar/src/RadarGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface RadarGridProps<D extends Record<string, unknown>> {
shape: RadarCommonProps<D>['gridShape']
radius: number
levels: number
rotation: number
angleStep: number
label: GridLabelComponent
labelOffset: number
Expand All @@ -19,6 +20,7 @@ export const RadarGrid = <D extends Record<string, unknown>>({
levels,
shape,
radius,
rotation,
angleStep,
label,
labelOffset,
Expand All @@ -29,9 +31,11 @@ export const RadarGrid = <D extends Record<string, unknown>>({
radii: Array.from({ length: levels })
.map((_, i) => (radius / levels) * (i + 1))
.reverse(),
angles: Array.from({ length: indices.length }, (_, i) => i * angleStep - Math.PI / 2),
angles: Array.from({ length: indices.length }).map(
(_, i) => rotation + i * angleStep - Math.PI / 2
),
}
}, [indices, levels, radius, angleStep])
}, [indices, levels, radius, rotation, angleStep])

return (
<>
Expand All @@ -53,6 +57,7 @@ export const RadarGrid = <D extends Record<string, unknown>>({
key={`level.${i}`}
shape={shape}
radius={radius}
rotation={rotation}
angleStep={angleStep}
dataLength={indices.length}
/>
Expand Down
13 changes: 10 additions & 3 deletions packages/radar/src/RadarGridLevels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,26 @@ const RadarGridLevelCircular = memo(({ radius }: RadarGridLevelCircularProps) =>

interface RadarGridLevelLinearProps {
radius: number
rotation: number
angleStep: number
dataLength: number
}

const RadarGridLevelLinear = ({ radius, angleStep, dataLength }: RadarGridLevelLinearProps) => {
const RadarGridLevelLinear = ({
radius,
rotation,
angleStep,
dataLength,
}: RadarGridLevelLinearProps) => {
const theme = useTheme()

const radarLineGenerator = useMemo(
() =>
lineRadial<number>()
.angle(i => i * angleStep)
.angle(i => rotation + i * angleStep)
.radius(radius)
.curve(curveLinearClosed),
[angleStep, radius]
[rotation, angleStep, radius]
)

const points = Array.from({ length: dataLength }, (_, i) => i)
Expand All @@ -60,6 +66,7 @@ const RadarGridLevelLinear = ({ radius, angleStep, dataLength }: RadarGridLevelL
interface RadarGridLevelsProps<D extends Record<string, unknown>> {
shape: RadarCommonProps<D>['gridShape']
radius: number
rotation: number
angleStep: number
dataLength: number
}
Expand Down
6 changes: 4 additions & 2 deletions packages/radar/src/RadarLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface RadarLayerProps<D extends Record<string, unknown>> {
colorByKey: Record<string | number, string>
fillByKey: Record<string, string | null>
radiusScale: ScaleLinear<number, number>
rotation: number
angleStep: number
curveFactory: CurveFactory
borderWidth: RadarCommonProps<D>['borderWidth']
Expand All @@ -26,6 +27,7 @@ export const RadarLayer = <D extends Record<string, unknown>>({
colorByKey,
fillByKey,
radiusScale,
rotation,
angleStep,
curveFactory,
borderWidth,
Expand All @@ -39,9 +41,9 @@ export const RadarLayer = <D extends Record<string, unknown>>({
const lineGenerator = useMemo(() => {
return lineRadial<number>()
.radius(d => radiusScale(d))
.angle((_, i) => i * angleStep)
.angle((_, i) => rotation + i * angleStep)
.curve(curveFactory)
}, [radiusScale, angleStep, curveFactory])
}, [radiusScale, rotation, angleStep, curveFactory])

const { animate, config: springConfig } = useMotionConfig()
const animatedPath = useAnimatedPath(lineGenerator(data.map(d => d[key] as number)) as string)
Expand Down
4 changes: 3 additions & 1 deletion packages/radar/src/RadarSlices.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface RadarSlicesProps<D extends Record<string, unknown>> {
formatValue: (value: number, context: string) => string
colorByKey: RadarColorMapping
radius: number
rotation: number
angleStep: number
tooltip: RadarCommonProps<D>['sliceTooltip']
}
Expand All @@ -20,13 +21,14 @@ export const RadarSlices = <D extends Record<string, unknown>>({
formatValue,
colorByKey,
radius,
rotation,
angleStep,
tooltip,
}: RadarSlicesProps<D>) => {
const arc = d3Arc<{ startAngle: number; endAngle: number }>().outerRadius(radius).innerRadius(0)

const halfAngleStep = angleStep * 0.5
let rootStartAngle = -halfAngleStep
let rootStartAngle = rotation - halfAngleStep

return (
<>
Expand Down
5 changes: 5 additions & 0 deletions packages/radar/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
usePropertyAccessor,
useValueFormatter,
} from '@nivo/core'
import { degreesToRadians } from '@nivo/core'
import { useOrdinalColorScale } from '@nivo/colors'
import { svgDefaultProps } from './props'
import {
Expand All @@ -22,6 +23,7 @@ export const useRadar = <D extends Record<string, unknown>>({
data,
keys,
indexBy,
rotationDegrees,
maxValue,
valueFormat,
curve,
Expand All @@ -35,6 +37,7 @@ export const useRadar = <D extends Record<string, unknown>>({
data: RadarDataProps<D>['data']
keys: RadarDataProps<D>['keys']
indexBy: RadarDataProps<D>['indexBy']
rotationDegrees: RadarCommonProps<D>['rotation']
maxValue: RadarCommonProps<D>['maxValue']
valueFormat?: RadarCommonProps<D>['valueFormat']
curve: RadarCommonProps<D>['curve']
Expand All @@ -48,6 +51,7 @@ export const useRadar = <D extends Record<string, unknown>>({
const getIndex = usePropertyAccessor<D, string>(indexBy)
const indices = useMemo(() => data.map(getIndex), [data, getIndex])
const formatValue = useValueFormatter<number, string>(valueFormat)
const rotation = degreesToRadians(rotationDegrees)

const getColor = useOrdinalColorScale<{ key: string; index: number }>(colors, 'key')
const colorByKey: RadarColorMapping = useMemo(
Expand Down Expand Up @@ -133,6 +137,7 @@ export const useRadar = <D extends Record<string, unknown>>({
colorByKey,
fillByKey,
boundDefs,
rotation,
radius,
radiusScale,
centerX,
Expand Down
2 changes: 2 additions & 0 deletions packages/radar/src/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const svgDefaultProps = {

maxValue: 'auto' as const,

rotation: 0,

curve: 'linearClosed' as const,

borderWidth: 2,
Expand Down
2 changes: 2 additions & 0 deletions packages/radar/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ export interface RadarCommonProps<D extends Record<string, unknown>> {
// second argument passed to the formatter is the key
valueFormat: ValueFormat<number, string>

rotation: number

layers: (RadarLayerId | RadarCustomLayer<D>)[]

margin: Box
Expand Down
4 changes: 3 additions & 1 deletion packages/radar/stories/radar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
const commonProperties = {
width: 900,
height: 500,
margin: { top: 60, right: 80, bottom: 20, left: 80 },
margin: { top: 60, right: 80, bottom: 30, left: 80 },
...generateWinesTastes(),
indexBy: 'taste',
animate: true,
Expand Down Expand Up @@ -195,3 +195,5 @@ export const WithPatterns = () => (
]}
/>
)

export const WithRotation = () => <Radar {...commonProperties} rotation={36} />
17 changes: 17 additions & 0 deletions packages/radar/tests/Radar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,23 @@ it('should render a basic radar chart', () => {
expect(layer1path.prop('fill')).toBe('rgba(244, 117, 96, 1)')
})

describe('layout', () => {
it('should support global rotation', () => {
const wrapperA = mount(<Radar<TestDatum> {...baseProps} rotation={90} />)
const wrapperB = mount(<Radar<TestDatum> {...baseProps} rotation={-90} />)
// the two first labels in the two components should have the same text content
const labelA0 = wrapperA.find('RadarGridLabels').at(0)
const labelB0 = wrapperB.find('RadarGridLabels').at(0)
// but positions should be opposite each other on the x axis, equal position on y axis
const getPos = (transformString: string) =>
transformString.replace('translate(', '').replace(')', '').split(', ')
const posA0 = getPos(labelA0.find('g').first().prop('transform') as string)
const posB0 = getPos(labelB0.find('g').first().prop('transform') as string)
expect(Number(posB0[0])).toBeCloseTo(-Number(posA0[0]), 4)
expect(Number(posB0[1])).toBeCloseTo(Number(posA0[1]), 4)
})
})

describe('data', () => {
it('should support value formatting', () => {
const wrapper = mount(
Expand Down

0 comments on commit d57345a

Please sign in to comment.