From 96e60be0d2ed4bcfcd168b9df878d33b13cb5d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Benitte?= Date: Tue, 16 Apr 2019 14:34:40 +0900 Subject: [PATCH] feat(radar): improve @nivo/radar package --- Makefile | 5 +- packages/bar/index.d.ts | 8 + packages/core/src/hooks/index.js | 1 + .../core/src/hooks/useCurveInterpolation.js | 17 + packages/radar/index.d.ts | 57 +++ packages/radar/package.json | 4 +- packages/radar/src/Radar.js | 432 +++++++----------- packages/radar/src/RadarDots.js | 267 ++++++----- packages/radar/src/RadarGrid.js | 136 +++--- packages/radar/src/RadarGridLabels.js | 103 +++-- packages/radar/src/RadarGridLevels.js | 138 +++--- packages/radar/src/RadarShapes.js | 152 +++--- packages/radar/src/RadarTooltip.js | 88 ++-- packages/radar/src/RadarTooltipItem.js | 170 ++++--- packages/radar/src/index.js | 1 + packages/radar/src/props.js | 76 +++ website/src/pages/radar/index.js | 8 +- 17 files changed, 834 insertions(+), 829 deletions(-) create mode 100644 packages/core/src/hooks/useCurveInterpolation.js create mode 100644 packages/radar/index.d.ts create mode 100644 packages/radar/src/props.js diff --git a/Makefile b/Makefile index c70c63fc47..544d44ac9b 100644 --- a/Makefile +++ b/Makefile @@ -74,10 +74,9 @@ fmt-check: ##@0 global check if files were all formatted using prettier "api/**/*.{js,ts,tsx}" \ "README.md" -test-all: ##@0 global run all checks/tests (packages, website & examples) +test: ##@0 global run all checks/tests (packages, website & examples) @$(MAKE) fmt-check - @$(MAKE) packages-lint - @$(MAKE) packages-tslint + @$(MAKE) lint @$(MAKE) packages-test deploy-all: ##@0 global deploy website & storybook diff --git a/packages/bar/index.d.ts b/packages/bar/index.d.ts index 11bea0660a..1a22d5e687 100644 --- a/packages/bar/index.d.ts +++ b/packages/bar/index.d.ts @@ -1,3 +1,11 @@ +/* + * 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 * as React from 'react' import { Dimensions, diff --git a/packages/core/src/hooks/index.js b/packages/core/src/hooks/index.js index 00d727900b..d89d21eeee 100644 --- a/packages/core/src/hooks/index.js +++ b/packages/core/src/hooks/index.js @@ -6,6 +6,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +export * from './useCurveInterpolation' export * from './useDimensions' export * from './usePartialTheme' export * from './useValueFormatter' diff --git a/packages/core/src/hooks/useCurveInterpolation.js b/packages/core/src/hooks/useCurveInterpolation.js new file mode 100644 index 0000000000..61041ed7ee --- /dev/null +++ b/packages/core/src/hooks/useCurveInterpolation.js @@ -0,0 +1,17 @@ +/* + * 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 { useMemo } from 'react' +import { curveFromProp } from '../props' + +/** + * Transform d3 curve interpolation identifier + * to its corresponding interpolator. + */ +export const useCurveInterpolation = interpolation => + useMemo(() => curveFromProp(interpolation), [interpolation]) diff --git a/packages/radar/index.d.ts b/packages/radar/index.d.ts new file mode 100644 index 0000000000..7ff41bf916 --- /dev/null +++ b/packages/radar/index.d.ts @@ -0,0 +1,57 @@ +/* + * 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 { Component } from 'react' +import { Box, MotionProps, Dimensions, Theme } from '@nivo/core' +import { OrdinalColorsInstruction, InheritedColorProp } from '@nivo/colors' +import { LegendProps } from '@nivo/legends' + +declare module '@nivo/radar' { + interface CommonRadarProps { + data: object[] + keys: Array + indexBy: number | string | Function + maxValue?: 'auto' | Function + + margin?: Box + + curve?: string + + borderWidth?: number + borderColor?: InheritedColorProp + + gridLevels?: number + gridShape?: 'circular' | 'linear' + gridLabel?: Function + gridLabelOffset?: number + + enableDots?: boolean + dotSymbol?: Function + dotSize?: number + dotColor?: InheritedColorProp + dotBorderWidth?: number + dotBorderColor?: InheritedColorProp + enableDotLabel?: boolean + dotLabel?: string | Function + dotLabelFormat?: string | Function + dotLabelYOffset?: number + + colors?: OrdinalColorsInstruction + fillOpacity?: number + + isInteractive?: boolean + tooltipFormat?: string | Function + + legends: LegendProps[] + } + + export type RadarProps = CommonRadarProps & MotionProps + + export class Radar extends Component {} + export class ResponsiveRadar extends Component {} +} diff --git a/packages/radar/package.json b/packages/radar/package.json index 23febee42e..c680383f20 100644 --- a/packages/radar/package.json +++ b/packages/radar/package.json @@ -19,6 +19,7 @@ "files": [ "README.md", "LICENSE.md", + "index.d.ts", "dist/" ], "dependencies": { @@ -29,8 +30,7 @@ "d3-scale": "^3.0.0", "d3-shape": "^1.3.5", "lodash": "^4.17.11", - "react-motion": "^0.5.2", - "recompose": "^0.30.0" + "react-motion": "^0.5.2" }, "peerDependencies": { "prop-types": ">= 15.5.10 < 16.0.0", diff --git a/packages/radar/src/Radar.js b/packages/radar/src/Radar.js index 24bfff25bb..ba7252a6a7 100644 --- a/packages/radar/src/Radar.js +++ b/packages/radar/src/Radar.js @@ -6,301 +6,187 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import max from 'lodash/max' -import React from 'react' -import PropTypes from 'prop-types' -import compose from 'recompose/compose' -import pure from 'recompose/pure' -import withPropsOnChange from 'recompose/withPropsOnChange' -import defaultProps from 'recompose/defaultProps' +import React, { memo, useMemo } from 'react' import { scaleLinear } from 'd3-scale' import { - closedCurvePropType, - withTheme, - withCurve, - withDimensions, - withMotion, + withContainer, + useTheme, + useCurveInterpolation, + useDimensions, getAccessorFor, - Container, SvgWrapper, } from '@nivo/core' -import { getOrdinalColorScale, ordinalColorsPropType, inheritedColorPropType } from '@nivo/colors' -import { LegendPropShape, BoxLegendSvg } from '@nivo/legends' +import { useOrdinalColorScale } from '@nivo/colors' +import { BoxLegendSvg } from '@nivo/legends' import RadarShapes from './RadarShapes' import RadarGrid from './RadarGrid' import RadarTooltip from './RadarTooltip' import RadarDots from './RadarDots' - -const Radar = ({ - data, - keys, - getIndex, - indices, - - curveInterpolator, - - radius, - radiusScale, - angleStep, - - centerX, - centerY, - margin, // eslint-disable-line react/prop-types - width, // eslint-disable-line react/prop-types - height, // eslint-disable-line react/prop-types - outerWidth, // eslint-disable-line react/prop-types - outerHeight, // eslint-disable-line react/prop-types - - borderWidth, - borderColor, - - gridLevels, - gridShape, - gridLabel, - gridLabelOffset, - - enableDots, - dotSymbol, - dotSize, - dotColor, - dotBorderWidth, - dotBorderColor, - enableDotLabel, - dotLabel, - dotLabelFormat, - dotLabelYOffset, - - theme, // eslint-disable-line react/prop-types - fillOpacity, - colorByKey, - - animate, // eslint-disable-line react/prop-types - motionStiffness, // eslint-disable-line react/prop-types - motionDamping, // eslint-disable-line react/prop-types - - isInteractive, - tooltipFormat, - - legends, -}) => { - const motionProps = { +import { RadarDefaultProps, RadarPropTypes } from './props' + +const Radar = memo( + ({ + data, + keys, + indexBy, + maxValue, + curve, + margin: partialMargin, + width, + height, + borderWidth, + borderColor, + gridLevels, + gridShape, + gridLabel, + gridLabelOffset, + enableDots, + dotSymbol, + dotSize, + dotColor, + dotBorderWidth, + dotBorderColor, + enableDotLabel, + dotLabel, + dotLabelFormat, + dotLabelYOffset, + colors, + fillOpacity, animate, - motionDamping, motionStiffness, - } + motionDamping, + isInteractive, + tooltipFormat, + legends, + }) => { + const getIndex = useMemo(() => getAccessorFor(indexBy), [indexBy]) + const indices = useMemo(() => data.map(getIndex), [data, getIndex]) + + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + const theme = useTheme() + + const getColor = useOrdinalColorScale(colors, 'key') + const colorByKey = useMemo( + () => + keys.reduce((mapping, key, index) => { + mapping[key] = getColor({ key, index }) + return mapping + }, {}), + [keys, getColor] + ) + + const { radius, radiusScale, centerX, centerY, angleStep } = useMemo(() => { + const computedMaxValue = + maxValue !== 'auto' + ? maxValue + : Math.max(...data.reduce((acc, d) => [...acc, ...keys.map(key => d[key])], [])) + + const radius = Math.min(innerWidth, innerHeight) / 2 + const radiusScale = scaleLinear() + .range([0, radius]) + .domain([0, computedMaxValue]) + + return { + radius, + radiusScale, + centerX: innerWidth / 2, + centerY: innerHeight / 2, + angleStep: (Math.PI * 2) / data.length, + } + }, [keys, indexBy, data, maxValue, innerWidth, innerHeight]) - const legendData = keys.map(key => ({ - id: key, - label: key, - color: colorByKey[key], - })) + const motionProps = { + animate, + motionDamping, + motionStiffness, + } - return ( - - {({ showTooltip, hideTooltip }) => ( - - - ({ + id: key, + label: key, + color: colorByKey[key], + })) + + const curveInterpolator = useCurveInterpolation(curve) + + return ( + + + + + {isInteractive && ( + - - {isInteractive && ( - - )} - {enableDots && ( - - )} - - {legends.map((legend, i) => ( - - ))} - - )} - - ) -} - -Radar.propTypes = { - // data - data: PropTypes.arrayOf(PropTypes.object).isRequired, - keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, - indexBy: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.func]).isRequired, - maxValue: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]).isRequired, - getIndex: PropTypes.func.isRequired, // computed - indices: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])) - .isRequired, // computed - - centerX: PropTypes.number.isRequired, // computed - centerY: PropTypes.number.isRequired, // computed - - radius: PropTypes.number.isRequired, // computed - radiusScale: PropTypes.func.isRequired, // computed - angleStep: PropTypes.number.isRequired, // computed - - curve: closedCurvePropType.isRequired, - curveInterpolator: PropTypes.func.isRequired, // computed - - // border - borderWidth: PropTypes.number.isRequired, - borderColor: inheritedColorPropType.isRequired, - - // grid - gridLevels: PropTypes.number, - gridShape: PropTypes.oneOf(['circular', 'linear']), - gridLabel: PropTypes.func, - gridLabelOffset: PropTypes.number, - - // dots - enableDots: PropTypes.bool.isRequired, - dotSymbol: PropTypes.func, - dotSize: PropTypes.number, - dotColor: inheritedColorPropType, - dotBorderWidth: PropTypes.number, - dotBorderColor: inheritedColorPropType, - enableDotLabel: PropTypes.bool, - dotLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - dotLabelFormat: PropTypes.string, - dotLabelYOffset: PropTypes.number, - - // theming - colors: ordinalColorsPropType.isRequired, - colorByKey: PropTypes.object.isRequired, - getColor: PropTypes.func.isRequired, // computed - fillOpacity: PropTypes.number.isRequired, - - // interactivity - isInteractive: PropTypes.bool.isRequired, - tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), - - legends: PropTypes.arrayOf(PropTypes.shape(LegendPropShape)).isRequired, -} - -export const RadarDefaultProps = { - maxValue: 'auto', - - curve: 'linearClosed', - - borderWidth: 2, - borderColor: { from: 'color' }, - - gridLevels: 5, - gridShape: 'circular', - gridLabelOffset: 16, - - enableDots: true, - - colors: { scheme: 'nivo' }, - fillOpacity: 0.15, - - isInteractive: true, - - legends: [], -} - -const enhance = compose( - defaultProps(RadarDefaultProps), - withTheme(), - withCurve(), - withDimensions(), - withMotion(), - withPropsOnChange(['colors'], ({ colors }) => ({ - getColor: getOrdinalColorScale(colors, 'key'), - })), - withPropsOnChange(['indexBy'], ({ indexBy }) => ({ - getIndex: getAccessorFor(indexBy), - })), - withPropsOnChange(['data', 'getIndex'], ({ data, getIndex }) => ({ - indices: data.map(getIndex), - })), - withPropsOnChange(['keys', 'getColor'], ({ keys, getColor }) => ({ - colorByKey: keys.reduce((mapping, key, index) => { - mapping[key] = getColor({ key, index }) - return mapping - }, {}), - })), - withPropsOnChange( - ['keys', 'indexBy', 'data', 'maxValue', 'width', 'height'], - ({ data, keys, maxValue: _maxValue, width, height }) => { - const maxValue = - _maxValue !== 'auto' - ? _maxValue - : max(data.reduce((acc, d) => [...acc, ...keys.map(key => d[key])], [])) - - const radius = Math.min(width, height) / 2 - const radiusScale = scaleLinear() - .range([0, radius]) - .domain([0, maxValue]) - - return { - data, - radius, - radiusScale, - centerX: width / 2, - centerY: height / 2, - angleStep: (Math.PI * 2) / data.length, - } - } - ), - pure + )} + + {legends.map((legend, i) => ( + + ))} + + ) + } ) -const enhancedRadar = enhance(Radar) -enhancedRadar.displayName = 'Radar' +Radar.displayName = 'Radar' +Radar.propTypes = RadarPropTypes +Radar.defaultProps = RadarDefaultProps -export default enhancedRadar +export default withContainer(Radar) diff --git a/packages/radar/src/RadarDots.js b/packages/radar/src/RadarDots.js index 3e14e58ca6..844a17d673 100644 --- a/packages/radar/src/RadarDots.js +++ b/packages/radar/src/RadarDots.js @@ -6,171 +6,164 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import { TransitionMotion, spring } from 'react-motion' import { motionPropTypes, - dotsThemePropType, + useTheme, positionFromAngle, getLabelGenerator, DotsItem, } from '@nivo/core' import { getInheritedColorGenerator, inheritedColorPropType } from '@nivo/colors' -export default class RadarDots extends Component { - static propTypes = { - data: PropTypes.arrayOf(PropTypes.object).isRequired, - keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])) - .isRequired, - getIndex: PropTypes.func.isRequired, - - colorByKey: PropTypes.object.isRequired, - - radiusScale: PropTypes.func.isRequired, - angleStep: PropTypes.number.isRequired, - - symbol: PropTypes.func, - size: PropTypes.number.isRequired, - color: inheritedColorPropType.isRequired, - borderWidth: PropTypes.number.isRequired, - borderColor: inheritedColorPropType.isRequired, - - enableLabel: PropTypes.bool.isRequired, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, - labelFormat: PropTypes.string, - labelYOffset: PropTypes.number, +const RadarDots = ({ + data, + keys, + getIndex, + + colorByKey, + + radiusScale, + angleStep, + + symbol, + size, + color, + borderWidth, + borderColor, + + enableLabel, + label, + labelFormat, + labelYOffset, + + animate, + motionStiffness, + motionDamping, +}) => { + const theme = useTheme() + const fillColor = getInheritedColorGenerator(color, theme) + const strokeColor = getInheritedColorGenerator(borderColor, theme) + const getLabel = getLabelGenerator(label, labelFormat) + + const points = data.reduce((acc, datum, i) => { + const index = getIndex(datum) + keys.forEach(key => { + const pointData = { + index, + key, + value: datum[key], + color: colorByKey[key], + } + acc.push({ + key: `${key}.${index}`, + label: enableLabel ? getLabel(pointData) : null, + style: { + fill: fillColor(pointData), + stroke: strokeColor(pointData), + ...positionFromAngle(angleStep * i - Math.PI / 2, radiusScale(datum[key])), + }, + data: pointData, + }) + }) - theme: PropTypes.shape({ - dots: dotsThemePropType.isRequired, - }).isRequired, + return acc + }, []) - ...motionPropTypes, + if (animate !== true) { + return ( + + {points.map(point => ( + + ))} + + ) } - static defaultProps = { - size: 6, - color: { from: 'color' }, - borderWidth: 0, - borderColor: { from: 'color' }, - enableLabel: false, - label: 'value', + const springConfig = { + damping: motionDamping, + striffness: motionStiffness, } - render() { - const { - data, - keys, - getIndex, - - colorByKey, - - radiusScale, - angleStep, - - symbol, - size, - color, - borderWidth, - borderColor, - - enableLabel, - label, - labelFormat, - labelYOffset, - - theme, - - animate, - motionStiffness, - motionDamping, - } = this.props - - const fillColor = getInheritedColorGenerator(color, theme) - const strokeColor = getInheritedColorGenerator(borderColor, theme) - const getLabel = getLabelGenerator(label, labelFormat) - - const points = data.reduce((acc, datum, i) => { - const index = getIndex(datum) - keys.forEach(key => { - const pointData = { - index, - key, - value: datum[key], - color: colorByKey[key], - } - acc.push({ - key: `${key}.${index}`, - label: enableLabel ? getLabel(pointData) : null, - style: { - fill: fillColor(pointData), - stroke: strokeColor(pointData), - ...positionFromAngle(angleStep * i - Math.PI / 2, radiusScale(datum[key])), - }, - data: pointData, - }) - }) - - return acc - }, []) - - if (animate !== true) { - return ( + return ( + ({ + key: point.key, + data: point, + style: { + x: spring(point.style.x, springConfig), + y: spring(point.style.y, springConfig), + size: spring(size, springConfig), + }, + }))} + > + {interpolatedStyles => ( - {points.map(point => ( + {interpolatedStyles.map(({ key, style, data: point }) => ( ))} - ) - } + )} + + ) +} - const springConfig = { - motionDamping, - motionStiffness, - } +RadarDots.propTypes = { + data: PropTypes.arrayOf(PropTypes.object).isRequired, + keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, + getIndex: PropTypes.func.isRequired, - return ( - ({ - key: point.key, - data: point, - style: { - x: spring(point.style.x, springConfig), - y: spring(point.style.y, springConfig), - size: spring(size, springConfig), - }, - }))} - > - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, style, data: point }) => ( - - ))} - - )} - - ) - } + colorByKey: PropTypes.object.isRequired, + + radiusScale: PropTypes.func.isRequired, + angleStep: PropTypes.number.isRequired, + + symbol: PropTypes.func, + size: PropTypes.number.isRequired, + color: inheritedColorPropType.isRequired, + borderWidth: PropTypes.number.isRequired, + borderColor: inheritedColorPropType.isRequired, + + enableLabel: PropTypes.bool.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + labelFormat: PropTypes.string, + labelYOffset: PropTypes.number, + + ...motionPropTypes, } +RadarDots.defaultProps = { + size: 6, + color: { from: 'color' }, + borderWidth: 0, + borderColor: { from: 'color' }, + enableLabel: false, + label: 'value', +} + +export default RadarDots diff --git a/packages/radar/src/RadarGrid.js b/packages/radar/src/RadarGrid.js index 007f8a40ac..46e9d24bea 100644 --- a/packages/radar/src/RadarGrid.js +++ b/packages/radar/src/RadarGrid.js @@ -6,73 +6,80 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' +import React, { memo, useMemo } from 'react' import PropTypes from 'prop-types' import range from 'lodash/range' -import compose from 'recompose/compose' -import pure from 'recompose/pure' -import withPropsOnChange from 'recompose/withPropsOnChange' -import { motionPropTypes } from '@nivo/core' -import { positionFromAngle } from '@nivo/core' +import { motionPropTypes, positionFromAngle, useTheme } from '@nivo/core' import RadialGridLabels from './RadarGridLabels' import RadarGridLevels from './RadarGridLevels' -const RadarGrid = ({ - indices, - shape, - radius, - radii, - angles, - angleStep, - label, - labelOffset, - theme, - animate, - motionStiffness, - motionDamping, -}) => { - const motionProps = { +const RadarGrid = memo( + ({ + indices, + levels, + shape, + radius, + angleStep, + label, + labelOffset, animate, - motionDamping, motionStiffness, - } + motionDamping, + }) => { + const theme = useTheme() + const { radii, angles } = useMemo(() => { + return { + radii: range(levels) + .map(i => (radius / levels) * (i + 1)) + .reverse(), + angles: range(indices.length).map(i => i * angleStep - Math.PI / 2), + } + }, [indices, levels, radius, angleStep]) - return ( - - {angles.map((angle, i) => { - const position = positionFromAngle(angle, radius) - return ( - - ) - })} - - - - ) -} + const motionProps = { + animate, + motionDamping, + motionStiffness, + } + return ( + + {angles.map((angle, i) => { + const position = positionFromAngle(angle, radius) + return ( + + ) + })} + + + + ) + } +) + +RadarGrid.displayName = 'RadarGrid' RadarGrid.propTypes = { indices: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])) .isRequired, @@ -81,18 +88,7 @@ RadarGrid.propTypes = { angleStep: PropTypes.number.isRequired, label: PropTypes.func, labelOffset: PropTypes.number.isRequired, - theme: PropTypes.object.isRequired, ...motionPropTypes, } -const enhance = compose( - withPropsOnChange(['indices', 'levels', 'radius', 'angleStep'], props => ({ - radii: range(props.levels) - .map(i => (props.radius / props.levels) * (i + 1)) - .reverse(), - angles: range(props.indices.length).map(i => i * props.angleStep - Math.PI / 2), - })), - pure -) - -export default enhance(RadarGrid) +export default RadarGrid diff --git a/packages/radar/src/RadarGridLabels.js b/packages/radar/src/RadarGridLabels.js index d04ad20db2..1da9783724 100644 --- a/packages/radar/src/RadarGridLabels.js +++ b/packages/radar/src/RadarGridLabels.js @@ -6,12 +6,10 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' import { TransitionMotion, spring } from 'react-motion' -import pure from 'recompose/pure' -import { motionPropTypes } from '@nivo/core' -import { positionFromAngle, radiansToDegrees } from '@nivo/core' +import { motionPropTypes, positionFromAngle, radiansToDegrees } from '@nivo/core' const textAnchorFromAngle = _angle => { const angle = radiansToDegrees(_angle) + 90 @@ -39,58 +37,63 @@ const renderLabel = (label, theme, labelComponent) => { ) } -const RadarGridLabels = ({ - radius, - angles, - indices, - label: labelComponent, - labelOffset, - theme, - animate, - motionStiffness, - motionDamping, -}) => { - const springConfig = { - motionDamping, +const RadarGridLabels = memo( + ({ + radius, + angles, + indices, + label: labelComponent, + labelOffset, + theme, + animate, motionStiffness, - } + motionDamping, + }) => { + const springConfig = { + motionDamping, + motionStiffness, + } + + const labels = indices.map((index, i) => { + const position = positionFromAngle(angles[i], radius + labelOffset) + const textAnchor = textAnchorFromAngle(angles[i]) - const labels = indices.map((index, i) => { - const position = positionFromAngle(angles[i], radius + labelOffset) - const textAnchor = textAnchorFromAngle(angles[i]) + return { + id: index, + angle: radiansToDegrees(angles[i]), + anchor: textAnchor, + ...position, + } + }) - return { - id: index, - angle: radiansToDegrees(angles[i]), - anchor: textAnchor, - ...position, + if (animate !== true) { + return {labels.map(label => renderLabel(label, theme, labelComponent))} } - }) - if (animate !== true) { - return {labels.map(label => renderLabel(label, theme, labelComponent))} + return ( + ({ + key: label.id, + data: label, + style: { + x: spring(label.x, springConfig), + y: spring(label.y, springConfig), + }, + }))} + > + {interpolatedStyles => ( + + {interpolatedStyles.map(({ data }) => + renderLabel(data, theme, labelComponent) + )} + + )} + + ) } +) - return ( - ({ - key: label.id, - data: label, - style: { - x: spring(label.x, springConfig), - y: spring(label.y, springConfig), - }, - }))} - > - {interpolatedStyles => ( - - {interpolatedStyles.map(({ data }) => renderLabel(data, theme, labelComponent))} - - )} - - ) -} - +RadarGridLabels.displayName = 'RadarGridLabels' RadarGridLabels.propTypes = { radius: PropTypes.number.isRequired, angles: PropTypes.arrayOf(PropTypes.number).isRequired, @@ -102,4 +105,4 @@ RadarGridLabels.propTypes = { ...motionPropTypes, } -export default pure(RadarGridLabels) +export default RadarGridLabels diff --git a/packages/radar/src/RadarGridLevels.js b/packages/radar/src/RadarGridLevels.js index 7ba50e4f31..8308642637 100644 --- a/packages/radar/src/RadarGridLevels.js +++ b/packages/radar/src/RadarGridLevels.js @@ -6,48 +6,78 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +import React, { memo } from 'react' import range from 'lodash/range' -import React from 'react' import PropTypes from 'prop-types' -import pure from 'recompose/pure' import { TransitionMotion, spring } from 'react-motion' import { motionPropTypes } from '@nivo/core' import { lineRadial, curveLinearClosed } from 'd3-shape' const levelWillEnter = () => ({ r: 0 }) -const RadarGridLevels = ({ - shape, - radii, - angleStep, - dataLength, - theme, - animate, - motionStiffness, - motionDamping, -}) => { - const springConfig = { - motionDamping, - motionStiffness, - } +const RadarGridLevels = memo( + ({ shape, radii, angleStep, dataLength, theme, animate, motionStiffness, motionDamping }) => { + const springConfig = { + damping: motionDamping, + stiffness: motionStiffness, + } - const levelsTransitionProps = { - willEnter: levelWillEnter, - willLeave: () => ({ r: spring(0, springConfig) }), - styles: radii.map((r, i) => ({ - key: `level.${i}`, - style: { - r: spring(r, springConfig), - }, - })), - } + const levelsTransitionProps = { + willEnter: levelWillEnter, + willLeave: () => ({ r: spring(0, springConfig) }), + styles: radii.map((r, i) => ({ + key: `level.${i}`, + style: { + r: spring(r, springConfig), + }, + })), + } + + if (shape === 'circular') { + if (animate !== true) { + return ( + + {radii.map((r, i) => ( + + ))} + + ) + } + + return ( + + {interpolatedStyles => ( + + {interpolatedStyles.map(({ key, style }) => ( + + ))} + + )} + + ) + } + + const radarLineGenerator = lineRadial() + .angle(i => i * angleStep) + .curve(curveLinearClosed) + + const points = range(dataLength) - if (shape === 'circular') { if (animate !== true) { return ( - {radii.map((r, i) => ( - + {radii.map((radius, i) => ( + ))} ) @@ -58,53 +88,21 @@ const RadarGridLevels = ({ {interpolatedStyles => ( {interpolatedStyles.map(({ key, style }) => ( - + ))} )} ) } +) - const radarLineGenerator = lineRadial() - .angle(i => i * angleStep) - .curve(curveLinearClosed) - - const points = range(dataLength) - - if (animate !== true) { - return ( - - {radii.map((radius, i) => ( - - ))} - - ) - } - - return ( - - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, style }) => ( - - ))} - - )} - - ) -} - +RadarGridLevels.displayName = 'RadarGridLevels' RadarGridLevels.propTypes = { shape: PropTypes.oneOf(['circular', 'linear']).isRequired, radii: PropTypes.arrayOf(PropTypes.number).isRequired, @@ -114,4 +112,4 @@ RadarGridLevels.propTypes = { ...motionPropTypes, } -export default pure(RadarGridLevels) +export default RadarGridLevels diff --git a/packages/radar/src/RadarShapes.js b/packages/radar/src/RadarShapes.js index 7b5aaa704b..242a3bb965 100644 --- a/packages/radar/src/RadarShapes.js +++ b/packages/radar/src/RadarShapes.js @@ -6,84 +6,92 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' -import compose from 'recompose/compose' -import withPropsOnChange from 'recompose/withPropsOnChange' -import pure from 'recompose/pure' +import React, { memo, useMemo } from 'react' import PropTypes from 'prop-types' -import { motionPropTypes, SmartMotion } from '@nivo/core' -import { getInheritedColorGenerator } from '@nivo/colors' +import { motionPropTypes, SmartMotion, useTheme } from '@nivo/core' +import { useInheritedColor, inheritedColorPropType } from '@nivo/colors' import { lineRadial } from 'd3-shape' -const RadarShapes = ({ - data, - keys, - colorByKey, - lineGenerator, +const RadarShapes = memo( + ({ + data, + keys, + colorByKey, + radiusScale, + angleStep, + curveInterpolator, + borderWidth, + borderColor, + fillOpacity, + animate, + motionStiffness, + motionDamping, + }) => { + const theme = useTheme() + const getBorderColor = useInheritedColor(borderColor, theme) + const lineGenerator = useMemo(() => { + return lineRadial() + .radius(d => radiusScale(d)) + .angle((d, i) => i * angleStep) + .curve(curveInterpolator) + }, [radiusScale, angleStep, curveInterpolator]) - // border - borderWidth, - borderColor, + if (animate !== true) { + return ( + + {keys.map(key => { + return ( + d[key]))} + fill={colorByKey[key]} + fillOpacity={fillOpacity} + stroke={getBorderColor({ key, color: colorByKey[key] })} + strokeWidth={borderWidth} + /> + ) + })} + + ) + } - // theming - fillOpacity, + const springConfig = { + stiffness: motionStiffness, + damping: motionDamping, + } - // motion - animate, - motionStiffness, - motionDamping, -}) => { - if (animate !== true) { return ( {keys.map(key => { return ( - d[key]))} - fill={colorByKey[key]} - fillOpacity={fillOpacity} - stroke={borderColor({ key, color: colorByKey[key] })} - strokeWidth={borderWidth} - /> + style={spring => ({ + d: spring(lineGenerator(data.map(d => d[key])), springConfig), + fill: spring(colorByKey[key], springConfig), + stroke: spring( + getBorderColor({ key, color: colorByKey[key] }), + springConfig + ), + })} + > + {style => ( + + )} + ) })} ) } +) - const springConfig = { - stiffness: motionStiffness, - damping: motionDamping, - } - - return ( - - {keys.map(key => { - return ( - ({ - d: spring(lineGenerator(data.map(d => d[key])), springConfig), - fill: spring(colorByKey[key], springConfig), - stroke: spring( - borderColor({ key, color: colorByKey[key] }), - springConfig - ), - })} - > - {style => ( - - )} - - ) - })} - - ) -} - +RadarShapes.displayName = 'RadarShapes' RadarShapes.propTypes = { - // data data: PropTypes.arrayOf(PropTypes.object).isRequired, keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, colorByKey: PropTypes.object.isRequired, @@ -92,33 +100,13 @@ RadarShapes.propTypes = { angleStep: PropTypes.number.isRequired, curveInterpolator: PropTypes.func.isRequired, - lineGenerator: PropTypes.func.isRequired, - // border borderWidth: PropTypes.number.isRequired, - borderColor: PropTypes.func.isRequired, + borderColor: inheritedColorPropType.isRequired, - // theming fillOpacity: PropTypes.number.isRequired, - // motion ...motionPropTypes, } -const enhance = compose( - withPropsOnChange(['borderColor', 'theme'], ({ borderColor, theme }) => ({ - borderColor: getInheritedColorGenerator(borderColor, theme), - })), - withPropsOnChange( - ['radiusScale', 'angleStep', 'curveInterpolator'], - ({ radiusScale, angleStep, curveInterpolator }) => ({ - lineGenerator: lineRadial() - .radius(d => radiusScale(d)) - .angle((d, i) => i * angleStep) - .curve(curveInterpolator), - }) - ), - pure -) - -export default enhance(RadarShapes) +export default RadarShapes diff --git a/packages/radar/src/RadarTooltip.js b/packages/radar/src/RadarTooltip.js index 08d26cbee0..559bfebe26 100644 --- a/packages/radar/src/RadarTooltip.js +++ b/packages/radar/src/RadarTooltip.js @@ -6,62 +6,50 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' -import pure from 'recompose/pure' import { arc as d3Arc } from 'd3-shape' import RadarTooltipItem from './RadarTooltipItem' -const RadarTooltip = ({ - data, - keys, - getIndex, - colorByKey, - radius, - angleStep, - theme, - tooltipFormat, - showTooltip, - hideTooltip, -}) => { - const arc = d3Arc() - .outerRadius(radius) - .innerRadius(0) +const RadarTooltip = memo( + ({ data, keys, getIndex, colorByKey, radius, angleStep, tooltipFormat }) => { + const arc = d3Arc() + .outerRadius(radius) + .innerRadius(0) - const halfAngleStep = angleStep * 0.5 - let rootStartAngle = -halfAngleStep + const halfAngleStep = angleStep * 0.5 + let rootStartAngle = -halfAngleStep - return ( - - {data.map(d => { - const index = getIndex(d) - const startAngle = rootStartAngle - const endAngle = startAngle + angleStep + return ( + + {data.map(d => { + const index = getIndex(d) + const startAngle = rootStartAngle + const endAngle = startAngle + angleStep - rootStartAngle += angleStep + rootStartAngle += angleStep - return ( - - ) - })} - - ) -} + return ( + + ) + })} + + ) + } +) +RadarTooltip.displayName = 'RadarTooltip' RadarTooltip.propTypes = { data: PropTypes.array.isRequired, keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, @@ -71,11 +59,7 @@ RadarTooltip.propTypes = { radius: PropTypes.number.isRequired, angleStep: PropTypes.number.isRequired, - theme: PropTypes.object.isRequired, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), } -export default pure(RadarTooltip) +export default RadarTooltip diff --git a/packages/radar/src/RadarTooltipItem.js b/packages/radar/src/RadarTooltipItem.js index 722c919527..711d3271de 100644 --- a/packages/radar/src/RadarTooltipItem.js +++ b/packages/radar/src/RadarTooltipItem.js @@ -6,87 +6,59 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' +import React, { memo, useMemo, useState, useCallback } from 'react' import PropTypes from 'prop-types' -import isFunction from 'lodash/isFunction' import sortBy from 'lodash/sortBy' import { format as d3Format } from 'd3-format' -import compose from 'recompose/compose' -import withState from 'recompose/withState' -import withPropsOnChange from 'recompose/withPropsOnChange' -import withHandlers from 'recompose/withHandlers' -import pure from 'recompose/pure' -import { positionFromAngle } from '@nivo/core' -import { TableTooltip, Chip } from '@nivo/core' +import { positionFromAngle, TableTooltip, Chip, useTooltip, useTheme } from '@nivo/core' -const RadarTooltipItem = ({ path, tipX, tipY, showTooltip, hideTooltip, isHover }) => ( - - - - -) - -RadarTooltipItem.propTypes = { - datum: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, - keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, - index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - colorByKey: PropTypes.object.isRequired, - - startAngle: PropTypes.number.isRequired, - endAngle: PropTypes.number.isRequired, - radius: PropTypes.number.isRequired, - tipX: PropTypes.number.isRequired, // computed - tipY: PropTypes.number.isRequired, // computed - - arcGenerator: PropTypes.func.isRequired, // computed - path: PropTypes.string.isRequired, // computed - - theme: PropTypes.object.isRequired, - - showTooltip: PropTypes.func.isRequired, // re-computed - hideTooltip: PropTypes.func.isRequired, // re-computed - - isHover: PropTypes.bool.isRequired, // computed -} +const RadarTooltipItem = memo( + ({ + datum, + keys, + index, + colorByKey, + radius, + startAngle, + endAngle, + arcGenerator, + tooltipFormat, + }) => { + const [isHover, setIsHover] = useState(false) + const theme = useTheme() + const [showTooltip, hideTooltip] = useTooltip() -const enhance = compose( - withState('isHover', 'setIsHover', false), - withPropsOnChange( - ['datum', 'keys', 'index', 'colorByKey', 'theme', 'tooltipFormat'], - ({ datum, keys, index, colorByKey, theme, tooltipFormat }) => { + const tooltip = useMemo(() => { const format = - !tooltipFormat || isFunction(tooltipFormat) + !tooltipFormat || typeof tooltipFormat === 'function' ? tooltipFormat : d3Format(tooltipFormat) - return { - tooltip: ( - {index}} - rows={sortBy( - keys.map(key => [ - , - key, - format ? format(datum[key]) : datum[key], - ]), - '2' - ).reverse()} - theme={theme} - /> - ), - } - } - ), - withPropsOnChange( - ['startAngle', 'endAngle', 'radius', 'arcGenerator'], - ({ startAngle, endAngle, radius, arcGenerator }) => { + return ( + {index}} + rows={sortBy( + keys.map(key => [ + , + key, + format ? format(datum[key]) : datum[key], + ]), + '2' + ).reverse()} + theme={theme} + /> + ) + }, [datum, keys, index, colorByKey, theme, tooltipFormat]) + const showItemTooltip = useCallback(event => { + setIsHover(true) + showTooltip(tooltip, event) + }) + const hideItemTooltip = useCallback(() => { + setIsHover(false) + hideTooltip() + }, [hideTooltip, setIsHover]) + + const { path, tipX, tipY } = useMemo(() => { const position = positionFromAngle( startAngle + (endAngle - startAngle) * 0.5 - Math.PI / 2, radius @@ -97,19 +69,45 @@ const enhance = compose( tipX: position.x, tipY: position.y, } - } - ), - withHandlers({ - showTooltip: ({ showTooltip, setIsHover, tooltip }) => e => { - setIsHover(true) - showTooltip(tooltip, e) - }, - hideTooltip: ({ hideTooltip, setIsHover }) => () => { - setIsHover(false) - hideTooltip() - }, - }), - pure + }, [startAngle, endAngle, radius, arcGenerator]) + + return ( + + + + + ) + } ) -export default enhance(RadarTooltipItem) +RadarTooltipItem.displayName = 'RadarTooltipItem' +RadarTooltipItem.propTypes = { + datum: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, + keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, + index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + colorByKey: PropTypes.object.isRequired, + + startAngle: PropTypes.number.isRequired, + endAngle: PropTypes.number.isRequired, + radius: PropTypes.number.isRequired, + + arcGenerator: PropTypes.func.isRequired, + + tooltipFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), +} + +export default RadarTooltipItem diff --git a/packages/radar/src/index.js b/packages/radar/src/index.js index 9b58ffc29e..e2423faa3c 100644 --- a/packages/radar/src/index.js +++ b/packages/radar/src/index.js @@ -10,3 +10,4 @@ export { default as Radar } from './Radar' export * from './Radar' export { default as ResponsiveRadar } from './ResponsiveRadar' export { default as RadarDots } from './RadarDots' +export * from './props' diff --git a/packages/radar/src/props.js b/packages/radar/src/props.js new file mode 100644 index 0000000000..240f141bef --- /dev/null +++ b/packages/radar/src/props.js @@ -0,0 +1,76 @@ +/* + * 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 PropTypes from 'prop-types' +import { ordinalColorsPropType, inheritedColorPropType } from '@nivo/colors' +import { LegendPropShape } from '@nivo/legends' +import { closedCurvePropType, motionPropTypes } from '@nivo/core' + +export const RadarPropTypes = { + data: PropTypes.arrayOf(PropTypes.object).isRequired, + keys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])).isRequired, + indexBy: PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.func]).isRequired, + maxValue: PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])]).isRequired, + + curve: closedCurvePropType.isRequired, + + borderWidth: PropTypes.number.isRequired, + borderColor: inheritedColorPropType.isRequired, + + gridLevels: PropTypes.number, + gridShape: PropTypes.oneOf(['circular', 'linear']), + gridLabel: PropTypes.func, + gridLabelOffset: PropTypes.number, + + enableDots: PropTypes.bool.isRequired, + dotSymbol: PropTypes.func, + dotSize: PropTypes.number, + dotColor: inheritedColorPropType, + dotBorderWidth: PropTypes.number, + dotBorderColor: inheritedColorPropType, + enableDotLabel: PropTypes.bool, + dotLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + dotLabelFormat: PropTypes.string, + dotLabelYOffset: PropTypes.number, + + colors: ordinalColorsPropType.isRequired, + fillOpacity: PropTypes.number.isRequired, + + isInteractive: PropTypes.bool.isRequired, + tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + + legends: PropTypes.arrayOf(PropTypes.shape(LegendPropShape)).isRequired, + + ...motionPropTypes, +} + +export const RadarDefaultProps = { + maxValue: 'auto', + + curve: 'linearClosed', + + borderWidth: 2, + borderColor: { from: 'color' }, + + gridLevels: 5, + gridShape: 'circular', + gridLabelOffset: 16, + + enableDots: true, + + colors: { scheme: 'nivo' }, + fillOpacity: 0.15, + + isInteractive: true, + + legends: [], + + animate: true, + motionDamping: 13, + motionStiffness: 90, +} diff --git a/website/src/pages/radar/index.js b/website/src/pages/radar/index.js index a792b94e12..04ecfcbd9a 100644 --- a/website/src/pages/radar/index.js +++ b/website/src/pages/radar/index.js @@ -35,10 +35,10 @@ const initialProperties = { gridLabelOffset: 36, enableDots: true, - dotSize: 8, - dotColor: { from: 'color' }, - dotBorderWidth: 0, - dotBorderColor: { theme: 'background' }, + dotSize: 10, + dotColor: { theme: 'background' }, + dotBorderWidth: 2, + dotBorderColor: { from: 'color' }, enableDotLabel: true, dotLabel: 'value', dotLabelYOffset: -12,