Skip to content

Commit

Permalink
feat(pie): use mostly the arc package for PieCanvas
Browse files Browse the repository at this point in the history
  • Loading branch information
plouc committed Dec 18, 2020
1 parent ce6bb87 commit 46af372
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 102 deletions.
1 change: 1 addition & 0 deletions packages/arcs/package.json
Expand Up @@ -28,6 +28,7 @@
"!dist/tsconfig.tsbuildinfo"
],
"dependencies": {
"@nivo/colors": "0.67.0",
"d3-shape": "^1.3.5",
"react-spring": "9.0.0-rc.3"
},
Expand Down
48 changes: 48 additions & 0 deletions packages/arcs/src/canvas.ts
@@ -0,0 +1,48 @@
import {
// @ts-ignore
textPropsByEngine,
CompleteTheme,
} from '@nivo/core'
import { DatumWithArcAndColor } from './types'
import { ArcLabel } from './useArcLabels'
import { ArcLinkLabel } from './links'

export const drawCanvasArcLabels = <Datum extends DatumWithArcAndColor>(
ctx: CanvasRenderingContext2D,
labels: ArcLabel<Datum>[],
theme: CompleteTheme
) => {
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.font = `${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`

labels.forEach(label => {
ctx.fillStyle = label.textColor
ctx.fillText(`${label.label}`, label.x, label.y)
})
}

export const drawCanvasArcLinkLabels = <Datum extends DatumWithArcAndColor>(
ctx: CanvasRenderingContext2D,
labels: ArcLinkLabel<Datum>[],
theme: CompleteTheme,
strokeWidth: number
) => {
ctx.textBaseline = 'middle'
ctx.font = `${theme.labels.text.fontSize}px ${theme.labels.text.fontFamily}`

labels.forEach(label => {
ctx.fillStyle = label.textColor
ctx.textAlign = textPropsByEngine.canvas.align[label.textAnchor]
ctx.fillText(`${label.label}`, label.x, label.y)

ctx.beginPath()
ctx.strokeStyle = label.linkColor
ctx.lineWidth = strokeWidth
label.points.forEach((point, index) => {
if (index === 0) ctx.moveTo(point.x, point.y)
else ctx.lineTo(point.x, point.y)
})
ctx.stroke()
})
}
20 changes: 11 additions & 9 deletions packages/arcs/src/centers.ts
@@ -1,3 +1,4 @@
import { useMemo } from 'react'
import { useTransition, to, SpringValue } from 'react-spring'
import {
// @ts-ignore
Expand All @@ -10,7 +11,6 @@ import {
} from '@nivo/core'
import { Arc, DatumWithArc } from './types'
import { ArcTransitionMode, TransitionExtra, useArcTransitionMode } from './arcTransitionMode'
import { useMemo } from 'react'

export const computeArcCenter = (
arc: Arc,
Expand Down Expand Up @@ -83,6 +83,12 @@ export const useArcCentersTransition = <Datum extends DatumWithArc, ExtraProps =
}
}

export interface ArcCenter<Datum extends DatumWithArc> {
x: number
y: number
data: Datum
}

/**
* Compute an array of arc centers from an array of data containing arcs.
*
Expand All @@ -105,19 +111,15 @@ export const useArcCenters = <
// 0.0: inner radius
// 0.5: center
// 1.0: outer radius
offset: number
offset?: number
// 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
skipAngle?: number
// this can be used to append extra properties to the centers,
// can be used to compute a color/label for example.
computeExtraProps: (datum: Datum) => ExtraProps
}): ({
x: number
y: number
data: Datum
} & ExtraProps)[] =>
computeExtraProps?: (datum: Datum) => ExtraProps
}): (ArcCenter<Datum> & ExtraProps)[] =>
useMemo(
() =>
data
Expand Down
3 changes: 3 additions & 0 deletions packages/arcs/src/index.ts
@@ -1,8 +1,11 @@
export * from './arcTransitionMode'
export * from './canvas'
export * from './centers'
export * from './interactivity'
export * from './interpolateArc'
export * from './links'
export * from './types'
export * from './useAnimatedArc'
export * from './useArcGenerator'
export * from './useArcLabels'
export * from './useArcsTransition'
204 changes: 204 additions & 0 deletions packages/arcs/src/links.ts
@@ -0,0 +1,204 @@
import { useCallback, useMemo } from 'react'
import {
// @ts-ignore
positionFromAngle,
// @ts-ignore
radiansToDegrees,
// @ts-ignore
getLabelGenerator,
useTheme,
} from '@nivo/core'
import { InheritedColorConfig, useInheritedColor } from '@nivo/colors'
import { DatumWithArc, DatumWithArcAndColor } from './types'
import { getNormalizedAngle } from './utils'

interface Point {
x: number
y: number
}

export interface ArcLink<Datum extends DatumWithArc> {
side: 'before' | 'after'
points: [Point, Point, Point]
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 = <Datum extends DatumWithArc>(
datum: Datum,
offset: number,
diagonalLength: number,
straightLength: number
): ArcLink<Datum> => {
const centerAngle = getNormalizedAngle(
datum.arc.startAngle + (datum.arc.endAngle - datum.arc.startAngle) / 2 - Math.PI / 2
)
const point0: Point = positionFromAngle(centerAngle, datum.arc.outerRadius + offset)
const point1: Point = positionFromAngle(
centerAngle,
datum.arc.outerRadius + offset + diagonalLength
)

let side: ArcLink<Datum>['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],
data: datum,
}
}

/**
* 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: ArcLink<Datum>) => ExtraProps
}): (ArcLink<Datum> & ExtraProps)[] => {
const links: ArcLink<Datum>[] = 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>(datum, offset, diagonalLength, straightLength)),
[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 ArcLink<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
// @todo come up with proper typing for label accessors, probably in `core`
label: any
linkColor: InheritedColorConfig<Datum>
textColor: InheritedColorConfig<Datum>
}) => {
const getLabel = useMemo(() => getLabelGenerator(label), [label])

const theme = useTheme()
const getLinkColor = useInheritedColor<Datum>(linkColor, theme)
const getTextColor = useInheritedColor<Datum>(textColor, theme)

const computeExtraProps = useCallback(
(link: ArcLink<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 ArcLink<Datum>>>({
data,
skipAngle,
offset,
diagonalLength,
straightLength,
computeExtraProps,
})
}
4 changes: 4 additions & 0 deletions packages/arcs/src/types.ts
Expand Up @@ -16,4 +16,8 @@ export interface DatumWithArc {
arc: Arc
}

export interface DatumWithArcAndColor extends DatumWithArc {
color: string
}

export type ArcGenerator = D3Arc<any, Arc>
9 changes: 9 additions & 0 deletions packages/arcs/src/useArcGenerator.ts
Expand Up @@ -2,6 +2,15 @@ import { useMemo } from 'react'
import { arc as d3Arc } from 'd3-shape'
import { ArcGenerator, Arc } from './types'

/**
* Memoize a d3 arc generator.
*
* Please note that both inner/outer radius should come
* aren't static and should come from the arc itself,
* while it requires more props on the arcs, it provides
* more flexibility because it's not limited to pie then
* but can also works with charts such as sunbursts.
*/
export const useArcGenerator = ({
cornerRadius = 0,
padAngle = 0,
Expand Down

0 comments on commit 46af372

Please sign in to comment.