From afc6b8cc6ac0d9a6b9957fe0038481cd3c383a21 Mon Sep 17 00:00:00 2001 From: plouc Date: Sun, 6 Dec 2020 07:09:41 +0900 Subject: [PATCH] feat(arcs): introduce @nivo/arcs package --- packages/arcs/LICENSE.md | 19 ++++++ packages/arcs/README.md | 3 + packages/arcs/package.json | 45 +++++++++++++ packages/arcs/src/index.ts | 3 + packages/arcs/src/types.ts | 18 ++++++ packages/arcs/src/useAnimatedArc.ts | 35 +++++++++++ packages/arcs/src/useArcGenerator.ts | 13 ++++ packages/arcs/tsconfig.json | 8 +++ packages/pie/package.json | 4 +- packages/pie/src/Pie.tsx | 15 +++-- packages/pie/src/PieSlice.tsx | 18 ++++-- packages/pie/src/hooks.ts | 80 +++++++++++++----------- packages/pie/src/props.ts | 9 ++- packages/pie/src/types.ts | 13 ++-- tsconfig.monorepo.json | 1 + website/src/data/components/pie/props.js | 14 ++--- website/src/pages/pie/index.js | 11 +--- 17 files changed, 232 insertions(+), 77 deletions(-) create mode 100644 packages/arcs/LICENSE.md create mode 100644 packages/arcs/README.md create mode 100644 packages/arcs/package.json create mode 100644 packages/arcs/src/index.ts create mode 100644 packages/arcs/src/types.ts create mode 100644 packages/arcs/src/useAnimatedArc.ts create mode 100644 packages/arcs/src/useArcGenerator.ts create mode 100644 packages/arcs/tsconfig.json diff --git a/packages/arcs/LICENSE.md b/packages/arcs/LICENSE.md new file mode 100644 index 0000000000..faa45389ec --- /dev/null +++ b/packages/arcs/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) Raphaël Benitte + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/arcs/README.md b/packages/arcs/README.md new file mode 100644 index 0000000000..ab74e75bca --- /dev/null +++ b/packages/arcs/README.md @@ -0,0 +1,3 @@ +# `@nivo/arcs` + +[![version](https://img.shields.io/npm/v/@nivo/arcs.svg?style=flat-square)](https://www.npmjs.com/package/@nivo/arcs) diff --git a/packages/arcs/package.json b/packages/arcs/package.json new file mode 100644 index 0000000000..7dd88988ff --- /dev/null +++ b/packages/arcs/package.json @@ -0,0 +1,45 @@ +{ + "name": "@nivo/arcs", + "version": "0.66.0", + "license": "MIT", + "author": { + "name": "Raphaël Benitte", + "url": "https://github.com/plouc" + }, + "repository": { + "type": "git", + "url": "https://github.com/plouc/nivo.git", + "directory": "packages/arcs" + }, + "keywords": [ + "nivo", + "dataviz", + "react", + "d3", + "arcs" + ], + "main": "./dist/nivo-arcs.cjs.js", + "module": "./dist/nivo-arcs.es.js", + "typings": "./dist/types/index.d.ts", + "files": [ + "README.md", + "LICENSE.md", + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "d3-shape": "^1.3.5", + "react-spring": "9.0.0-rc.3" + }, + "devDependencies": { + "@nivo/core": "0.66.0", + "@types/d3-shape": "^2.0.0" + }, + "peerDependencies": { + "@nivo/core": "0.66.0", + "react": ">= 16.8.4 < 18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/arcs/src/index.ts b/packages/arcs/src/index.ts new file mode 100644 index 0000000000..583d25fca4 --- /dev/null +++ b/packages/arcs/src/index.ts @@ -0,0 +1,3 @@ +export * from './types' +export * from './useAnimatedArc' +export * from './useArcGenerator' diff --git a/packages/arcs/src/types.ts b/packages/arcs/src/types.ts new file mode 100644 index 0000000000..beac77fc2c --- /dev/null +++ b/packages/arcs/src/types.ts @@ -0,0 +1,18 @@ +import { Arc as D3Arc } from 'd3-shape' + +export interface Arc { + // start angle in radians + startAngle: number + // end angle in radians + endAngle: number + // inner radius in pixels + innerRadius: number + // outer radius in pixels + outerRadius: number +} + +export interface DatumWithArc { + arc: Arc +} + +export type ArcGenerator = D3Arc diff --git a/packages/arcs/src/useAnimatedArc.ts b/packages/arcs/src/useAnimatedArc.ts new file mode 100644 index 0000000000..58a095752d --- /dev/null +++ b/packages/arcs/src/useAnimatedArc.ts @@ -0,0 +1,35 @@ +import { to, useSpring } from 'react-spring' +import { useMotionConfig } from '@nivo/core' +import { Arc, ArcGenerator } from './types' + +export const useAnimatedArc = (datumWithArc: { arc: Arc }, arcGenerator: ArcGenerator) => { + const { animate, config: springConfig } = useMotionConfig() + + const animatedValues = useSpring({ + startAngle: datumWithArc.arc.startAngle, + endAngle: datumWithArc.arc.endAngle, + innerRadius: datumWithArc.arc.innerRadius, + outerRadius: datumWithArc.arc.outerRadius, + config: springConfig, + immediate: !animate, + }) + + return { + ...animatedValues, + path: to( + [ + animatedValues.startAngle, + animatedValues.endAngle, + animatedValues.innerRadius, + animatedValues.outerRadius, + ], + (startAngle, endAngle, innerRadius, outerRadius) => + arcGenerator({ + startAngle, + endAngle, + innerRadius, + outerRadius, + }) + ), + } +} diff --git a/packages/arcs/src/useArcGenerator.ts b/packages/arcs/src/useArcGenerator.ts new file mode 100644 index 0000000000..256d42be20 --- /dev/null +++ b/packages/arcs/src/useArcGenerator.ts @@ -0,0 +1,13 @@ +import { useMemo } from 'react' +import { arc as d3Arc } from 'd3-shape' +import { ArcGenerator, Arc } from './types' + +export const useArcGenerator = ({ cornerRadius = 0 }: { cornerRadius: number }): ArcGenerator => + useMemo( + () => + d3Arc() + .innerRadius(arc => arc.innerRadius) + .outerRadius(arc => arc.outerRadius) + .cornerRadius(cornerRadius), + [cornerRadius] + ) diff --git a/packages/arcs/tsconfig.json b/packages/arcs/tsconfig.json new file mode 100644 index 0000000000..39c997d5e7 --- /dev/null +++ b/packages/arcs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.types.json", + "compilerOptions": { + "outDir": "./dist/types", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/packages/pie/package.json b/packages/pie/package.json index d131908cfb..6049c9beca 100644 --- a/packages/pie/package.json +++ b/packages/pie/package.json @@ -29,11 +29,13 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { + "@nivo/arcs": "0.66.0", "@nivo/colors": "0.67.0", "@nivo/legends": "0.67.0", "@nivo/tooltip": "0.67.0", "d3-shape": "^1.3.5", - "lodash": "^4.17.11" + "lodash": "^4.17.11", + "react-spring": "9.0.0-rc.3" }, "devDependencies": { "@nivo/core": "0.67.0", diff --git a/packages/pie/src/Pie.tsx b/packages/pie/src/Pie.tsx index 89d2240aa1..d7e03bc4d2 100644 --- a/packages/pie/src/Pie.tsx +++ b/packages/pie/src/Pie.tsx @@ -95,9 +95,15 @@ const Pie = ({ colors, }) - const { dataWithArc, arcGenerator, centerX, centerY, radius, innerRadius } = usePieFromBox< - RawDatum - >({ + const { + dataWithArc, + arcGenerator, + centerX, + centerY, + radius, + innerRadius, + setActiveId, + } = usePieFromBox({ data: normalizedData, width: innerWidth, height: innerHeight, @@ -128,7 +134,7 @@ const Pie = ({ key={datumWithArc.id} datum={datumWithArc} - path={arcGenerator(datumWithArc.arc) ?? undefined} + arcGenerator={arcGenerator} borderWidth={borderWidth} borderColor={borderColor(datumWithArc)} tooltip={tooltip} @@ -137,6 +143,7 @@ const Pie = ({ onMouseEnter={onMouseEnter} onMouseMove={onMouseMove} onMouseLeave={onMouseLeave} + setActiveId={setActiveId} /> ))} diff --git a/packages/pie/src/PieSlice.tsx b/packages/pie/src/PieSlice.tsx index 6ecf5585c1..88a9442922 100644 --- a/packages/pie/src/PieSlice.tsx +++ b/packages/pie/src/PieSlice.tsx @@ -1,10 +1,12 @@ import React, { createElement, useCallback } from 'react' -// @ts-ignore +import { animated } from 'react-spring' import { useTooltip } from '@nivo/tooltip' +import { useAnimatedArc, ArcGenerator } from '@nivo/arcs' import { ComputedDatum, CompletePieSvgProps } from './types' interface PieSliceProps { datum: ComputedDatum + arcGenerator: ArcGenerator path?: string borderWidth: CompletePieSvgProps['borderWidth'] borderColor: string @@ -14,11 +16,12 @@ interface PieSliceProps { onMouseEnter: CompletePieSvgProps['onMouseEnter'] onMouseMove: CompletePieSvgProps['onMouseMove'] onMouseLeave: CompletePieSvgProps['onMouseLeave'] + setActiveId: (id: null | string | number) => void } export const PieSlice = ({ datum, - path, + arcGenerator, borderWidth, borderColor, isInteractive, @@ -27,6 +30,7 @@ export const PieSlice = ({ onMouseMove, onMouseLeave, tooltip, + setActiveId, }: PieSliceProps) => { const { showTooltipFromEvent, hideTooltip } = useTooltip() @@ -38,9 +42,10 @@ export const PieSlice = ({ const handleMouseEnter = useCallback( event => { onMouseEnter?.(datum, event) + setActiveId(datum.id) handleTooltip(event) }, - [onMouseEnter, handleTooltip, datum] + [onMouseEnter, setActiveId, handleTooltip, datum] ) const handleMouseMove = useCallback( @@ -54,6 +59,7 @@ export const PieSlice = ({ const handleMouseLeave = useCallback( event => { onMouseLeave?.(datum, event) + setActiveId(null) hideTooltip() }, [onMouseLeave, hideTooltip, datum] @@ -66,9 +72,11 @@ export const PieSlice = ({ [onClick, datum] ) + const animatedArc = useAnimatedArc(datum, arcGenerator) + return ( - ({ data, startAngle = defaultProps.startAngle, endAngle = defaultProps.endAngle, + innerRadius, + outerRadius, padAngle = defaultProps.padAngle, sortByValue = defaultProps.sortByValue, + activeId = null, }: { data: Omit, 'arc' | 'fill'>[] + // in degrees startAngle: number + // in degrees endAngle: number + // in pixels + innerRadius: number + // in pixels + outerRadius: number padAngle: number sortByValue: boolean + activeId?: null | string | number }): Omit, 'fill'>[] => { const pie = useMemo(() => { const innerPie = d3Pie, 'arc' | 'fill'>>() .value(d => d.value) - .padAngle(degreesToRadians(padAngle)) .startAngle(degreesToRadians(startAngle)) .endAngle(degreesToRadians(endAngle)) + .padAngle(degreesToRadians(padAngle)) - if (sortByValue !== true) innerPie.sortValues(null) + if (!sortByValue) { + innerPie.sortValues(null) + } return innerPie }, [startAngle, endAngle, padAngle, sortByValue]) @@ -126,7 +138,10 @@ export const usePieArcs = ({ () => pie(data).map( ( - arc: Omit & { + arc: Omit< + PieArc, + 'angle' | 'angleDeg' | 'innerRadius' | 'outerRadius' | 'thickness' + > & { data: Omit, 'arc' | 'fill'> } ) => { @@ -138,6 +153,9 @@ export const usePieArcs = ({ index: arc.index, startAngle: arc.startAngle, endAngle: arc.endAngle, + innerRadius: innerRadius, + outerRadius: activeId === arc.data.id ? outerRadius + 20 : outerRadius, + thickness: outerRadius - innerRadius, padAngle: arc.padAngle, angle, angleDeg: radiansToDegrees(angle), @@ -146,25 +164,10 @@ export const usePieArcs = ({ } ), - [pie, data] + [pie, data, innerRadius, outerRadius, activeId] ) } -export const usePieArcGenerator = ({ - radius, - innerRadius, - cornerRadius = defaultProps.cornerRadius, -}: { - radius: number - innerRadius: number - cornerRadius: number -}): PieArcGenerator => - useMemo( - () => - d3Arc().outerRadius(radius).innerRadius(innerRadius).cornerRadius(cornerRadius), - [radius, innerRadius, cornerRadius] - ) - /** * Compute pie layout using explicit radius/innerRadius, * expressed in pixels. @@ -190,15 +193,13 @@ export const usePie = ({ data, startAngle, endAngle, + innerRadius, + outerRadius: radius, padAngle, sortByValue, }) - const arcGenerator = usePieArcGenerator({ - radius, - innerRadius, - cornerRadius, - }) + const arcGenerator = useArcGenerator({ cornerRadius }) return { dataWithArc, arcGenerator } } @@ -236,14 +237,7 @@ export const usePieFromBox = ({ > & { data: Omit, 'arc'>[] }) => { - const dataWithArc = usePieArcs({ - data, - startAngle, - endAngle, - padAngle, - sortByValue, - }) - + const [activeId, setActiveId] = useState(null) const computedProps = useMemo(() => { let radius = Math.min(width, height) / 2 let innerRadius = radius * Math.min(innerRadiusRatio, 1) @@ -292,15 +286,25 @@ export const usePieFromBox = ({ } }, [width, height, innerRadiusRatio, startAngle, endAngle, fit, cornerRadius]) - const arcGenerator = usePieArcGenerator({ - radius: computedProps.radius, + const dataWithArc = usePieArcs({ + data, + startAngle, + endAngle, innerRadius: computedProps.innerRadius, + outerRadius: computedProps.radius, + padAngle, + sortByValue, + activeId, + }) + + const arcGenerator = useArcGenerator({ cornerRadius, }) return { dataWithArc, arcGenerator, + setActiveId, ...computedProps, } } @@ -449,7 +453,7 @@ export const usePieLayerContext = ({ innerRadius, }: { dataWithArc: ComputedDatum[] - arcGenerator: PieArcGenerator + arcGenerator: ArcGenerator centerX: number centerY: number radius: number diff --git a/packages/pie/src/props.ts b/packages/pie/src/props.ts index d649c6954a..d3bd3d0206 100644 --- a/packages/pie/src/props.ts +++ b/packages/pie/src/props.ts @@ -9,7 +9,7 @@ export const defaultProps = { padAngle: 0, cornerRadius: 0, - layers: ['slices', 'radialLabels', 'sliceLabels', 'legends'], + layers: ['radialLabels', 'slices', 'sliceLabels', 'legends'], // layout startAngle: 0, @@ -42,18 +42,17 @@ export const defaultProps = { sliceLabelsRadiusOffset: 0.5, sliceLabelsTextColor: { theme: 'labels.text.fill' }, - // styling colors: ({ scheme: 'nivo' } as unknown) as OrdinalColorScaleConfig, defs: [], fill: [], - // interactivity isInteractive: true, - // tooltip + animate: true, + motionConfig: 'gentle', + tooltip: PieTooltip, - // legends legends: [], role: 'img', diff --git a/packages/pie/src/types.ts b/packages/pie/src/types.ts index 272d37575a..76c778d2ba 100644 --- a/packages/pie/src/types.ts +++ b/packages/pie/src/types.ts @@ -3,6 +3,7 @@ import { Arc as ArcGenerator } from 'd3-shape' import { Box, Dimensions, Theme, SvgDefsAndFill } from '@nivo/core' import { OrdinalColorScaleConfig, InheritedColorConfig } from '@nivo/colors' import { LegendProps } from '@nivo/legends' +import { Arc } from '@nivo/arcs' export type DatumId = string | number export type DatumValue = number @@ -17,13 +18,13 @@ export interface DefaultRawDatum { value: DatumValue } -export interface PieArc { +export interface PieArc extends Arc { index: number - startAngle: number - endAngle: number - // center angle + // middle angle in radians angle: number angleDeg: number + // outer radius - inner radius in pixels + thickness: number padAngle: number } @@ -58,8 +59,6 @@ export type MouseEventHandler = ( event: React.MouseEvent ) => void -export type PieArcGenerator = ArcGenerator - export type PieLayerId = 'slices' | 'radialLabels' | 'sliceLabels' | 'legends' export interface PieCustomLayerProps { @@ -68,7 +67,7 @@ export interface PieCustomLayerProps { centerY: number radius: number innerRadius: number - arcGenerator: PieArcGenerator + arcGenerator: ArcGenerator } export type PieCustomLayer = React.FC> diff --git a/tsconfig.monorepo.json b/tsconfig.monorepo.json index faaea1a5da..0a4ae981c4 100644 --- a/tsconfig.monorepo.json +++ b/tsconfig.monorepo.json @@ -10,6 +10,7 @@ // { "path": "./packages/legends" }, // { "path": "./packages/scales" }, { "path": "./packages/tooltip" }, + { "path": "./packages/arcs" }, // Utility package // { "path": "./packages/generators" }, diff --git a/website/src/data/components/pie/props.js b/website/src/data/components/pie/props.js index d42b59cbad..b9f782ed17 100644 --- a/website/src/data/components/pie/props.js +++ b/website/src/data/components/pie/props.js @@ -1,18 +1,12 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ import { defaultProps as defaults } from '@nivo/pie' import { themeProperty, defsProperties, groupProperties, getLegendsProps, + motionProperties, } from '../../../lib/componentProperties' +import { defaultProps } from '@nivo/sunburst' const props = [ { @@ -25,6 +19,7 @@ const props = [ \`\`\` Array<{ + // must be unique for the whole dataset id: string | number, value: number }> @@ -44,7 +39,7 @@ const props = [ { key: 'id', group: 'Base', - help: 'ID accessor.', + help: 'ID accessor which should return a unique value for the whole dataset.', description: ` Define how to access the ID of each datum, by default, nivo will look for the \`id\` property. @@ -584,6 +579,7 @@ const props = [ controlType: 'switch', group: 'Interactivity', }, + ...motionProperties(['svg'], defaultProps, 'react-spring'), { key: 'legends', flavors: ['svg', 'canvas'], diff --git a/website/src/pages/pie/index.js b/website/src/pages/pie/index.js index 2ea6a515db..bd6059ad37 100644 --- a/website/src/pages/pie/index.js +++ b/website/src/pages/pie/index.js @@ -1,11 +1,3 @@ -/* - * This file is part of the nivo project. - * - * Copyright 2016-present, Raphaël Benitte. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ import React from 'react' import { ResponsivePie, defaultProps } from '@nivo/pie' import { generateProgrammingLanguageStats } from '@nivo/generators' @@ -72,6 +64,9 @@ const initialProperties = { defs: [], fill: [], + animate: true, + motionConfig: 'wobbly', + legends: [ { anchor: 'bottom',