From b08c691d0baecaf8e32d57892351894c2eecfaf1 Mon Sep 17 00:00:00 2001 From: plouc Date: Sun, 21 Jun 2020 09:12:46 +0900 Subject: [PATCH] feat(sankey): use hooks instead of recompose and migrate to react-spring --- packages/sankey/package.json | 5 +- packages/sankey/src/Sankey.js | 285 ++++++++++---------- packages/sankey/src/SankeyLabels.js | 114 +++----- packages/sankey/src/SankeyLinks.js | 106 ++------ packages/sankey/src/SankeyLinksItem.js | 132 ++++----- packages/sankey/src/SankeyNodes.js | 127 ++------- packages/sankey/src/SankeyNodesItem.js | 137 +++++----- packages/sankey/src/enhance.js | 147 ---------- packages/sankey/src/hooks.js | 166 ++++++++++++ packages/sankey/src/props.js | 5 +- website/src/data/components/sankey/props.js | 2 +- website/src/pages/sankey/index.js | 7 +- 12 files changed, 528 insertions(+), 705 deletions(-) delete mode 100644 packages/sankey/src/enhance.js create mode 100644 packages/sankey/src/hooks.js diff --git a/packages/sankey/package.json b/packages/sankey/package.json index fd60afc0a..637152b1f 100644 --- a/packages/sankey/package.json +++ b/packages/sankey/package.json @@ -24,16 +24,15 @@ ], "dependencies": { "@nivo/colors": "0.62.0", - "@nivo/core": "0.62.0", "@nivo/legends": "0.62.0", "@nivo/tooltip": "0.62.0", "d3-sankey": "^0.12.1", "d3-shape": "^1.3.5", "lodash": "^4.17.11", - "react-motion": "^0.5.2", - "recompose": "^0.30.0" + "react-spring": "^8.0.27" }, "peerDependencies": { + "@nivo/core": "0.62.0", "prop-types": ">= 15.5.10 < 16.0.0", "react": ">= 16.8.4 < 17.0.0" }, diff --git a/packages/sankey/src/Sankey.js b/packages/sankey/src/Sankey.js index ff00edeca..ef512eed6 100644 --- a/packages/sankey/src/Sankey.js +++ b/packages/sankey/src/Sankey.js @@ -8,66 +8,85 @@ */ import React, { Fragment } from 'react' import { uniq } from 'lodash' -import { Container, SvgWrapper } from '@nivo/core' +import { SvgWrapper, useDimensions, withContainer } from '@nivo/core' import { BoxLegendSvg } from '@nivo/legends' -import { SankeyPropTypes } from './props' -import enhance from './enhance' +import { SankeyDefaultProps, SankeyPropTypes } from './props' +import { useSankey } from './hooks' import SankeyNodes from './SankeyNodes' import SankeyLinks from './SankeyLinks' import SankeyLabels from './SankeyLabels' const Sankey = ({ - nodes, - links, + data, layout, - - margin, + sort, + align, width, height, - outerWidth, - outerHeight, - + margin: partialMargin, + colors, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + nodeBorderColor, nodeOpacity, nodeHoverOpacity, nodeHoverOthersOpacity, nodeBorderWidth, - getNodeBorderColor, // computed - setCurrentNode, // injected - currentNode, // injected - linkOpacity, linkHoverOpacity, linkHoverOthersOpacity, linkContract, linkBlendMode, enableLinkGradient, - setCurrentLink, // injected - currentLink, // injected - enableLabels, labelPosition, labelPadding, labelOrientation, - getLabelTextColor, // computed - - theme, - + label, + labelFormat, + labelTextColor, nodeTooltip, linkTooltip, - - animate, - motionDamping, - motionStiffness, - isInteractive, onClick, tooltipFormat, - legends, - legendData, - layers, }) => { + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + + const { + nodes, + links, + legendData, + getNodeBorderColor, + currentNode, + setCurrentNode, + currentLink, + setCurrentLink, + getLabelTextColor, + } = useSankey({ + data, + layout, + width: innerWidth, + height: innerHeight, + sort, + align, + colors, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + nodeBorderColor, + label, + labelFormat, + labelTextColor, + }) + let isCurrentNode = () => false let isCurrentLink = () => false @@ -94,133 +113,101 @@ const Sankey = ({ source.id === currentNode.id || target.id === currentNode.id } - return ( - - {({ showTooltip, hideTooltip }) => { - const layerProps = { - links, - nodes, - margin, - width, - height, - outerWidth, - outerHeight, - } + const layerProps = { + links, + nodes, + margin, + width, + height, + outerWidth, + outerHeight, + } - const layerById = { - links: ( - - ), - nodes: ( - - ), - labels: null, - legends: legends.map((legend, i) => ( - - )), - } + const layerById = { + links: ( + + ), + nodes: ( + + ), + labels: null, + legends: legends.map((legend, i) => ( + + )), + } - if (enableLabels) { - layerById.labels = ( - - ) - } + if (enableLabels) { + layerById.labels = ( + + ) + } - return ( - - {layers.map((layer, i) => { - if (typeof layer === 'function') { - return {layer(layerProps)} - } + return ( + + {layers.map((layer, i) => { + if (typeof layer === 'function') { + return {layer(layerProps)} + } - return layerById[layer] - })} - - ) - }} - + return layerById[layer] + })} + ) } Sankey.propTypes = SankeyPropTypes -const enhancedSankey = enhance(Sankey) -enhancedSankey.displayName = 'Sankey' +const WrappedSankey = withContainer(Sankey) +WrappedSankey.defaultProps = SankeyDefaultProps -export default enhancedSankey +export default WrappedSankey diff --git a/packages/sankey/src/SankeyLabels.js b/packages/sankey/src/SankeyLabels.js index a049b0555..446bb85b1 100644 --- a/packages/sankey/src/SankeyLabels.js +++ b/packages/sankey/src/SankeyLabels.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, { Fragment } from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' -import pure from 'recompose/pure' -import { TransitionMotion, spring } from 'react-motion' -import { motionPropTypes } from '@nivo/core' -import { interpolateColor, getInterpolatedColor } from '@nivo/colors' +import { useSprings, animated } from 'react-spring' +import { useTheme, useMotionConfig } from '@nivo/core' const SankeyLabels = ({ nodes, @@ -22,11 +20,9 @@ const SankeyLabels = ({ labelPadding, labelOrientation, getLabelTextColor, - theme, - animate, - motionDamping, - motionStiffness, }) => { + const theme = useTheme() + const labelRotation = labelOrientation === 'vertical' ? -90 : 0 const labels = nodes.map(node => { let x @@ -82,74 +78,36 @@ const SankeyLabels = ({ } }) - if (!animate) { - return ( - - {labels.map(label => { - return ( - - {label.label} - - ) - })} - - ) - } - - const springProps = { - damping: motionDamping, - stiffness: motionStiffness, - } + const { animate, config: springConfig } = useMotionConfig() + const springs = useSprings( + labels.length, + labels.map(label => ({ + transform: `translate(${label.x}, ${label.y}) rotate(${labelRotation})`, + color: label.color, + config: springConfig, + immediate: !animate, + })) + ) - return ( - { - return { - key: label.id, - data: label, - style: { - x: spring(label.x, springProps), - y: spring(label.y, springProps), - rotation: spring(labelRotation, springProps), - ...interpolateColor(label.color, springProps), - }, - } - })} - > - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, style, data }) => { - const color = getInterpolatedColor(style) + return springs.map((animatedProps, index) => { + const label = labels[index] - return ( - - {data.label} - - ) - })} - - )} - - ) + return ( + + {label.label} + + ) + }) } SankeyLabels.propTypes = { @@ -171,10 +129,6 @@ SankeyLabels.propTypes = { labelPadding: PropTypes.number.isRequired, labelOrientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, getLabelTextColor: PropTypes.func.isRequired, - - theme: PropTypes.object.isRequired, - - ...motionPropTypes, } -export default pure(SankeyLabels) +export default memo(SankeyLabels) diff --git a/packages/sankey/src/SankeyLinks.js b/packages/sankey/src/SankeyLinks.js index d4419303f..ff9fd7c89 100644 --- a/packages/sankey/src/SankeyLinks.js +++ b/packages/sankey/src/SankeyLinks.js @@ -6,39 +6,29 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Fragment } from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' -import pure from 'recompose/pure' -import { motionPropTypes, SmartMotion, blendModePropType } from '@nivo/core' +import { blendModePropType } from '@nivo/core' import SankeyLinksItem from './SankeyLinksItem' import { sankeyLinkHorizontal, sankeyLinkVertical } from './links' const SankeyLinks = ({ links, layout, - linkOpacity, linkHoverOpacity, linkHoverOthersOpacity, linkContract, linkBlendMode, enableLinkGradient, - - animate, - motionDamping, - motionStiffness, - - showTooltip, - hideTooltip, setCurrentLink, currentNode, currentLink, isCurrentLink, + isInteractive, onClick, tooltipFormat, - tooltip, - theme, }) => { const getOpacity = link => { if (!currentNode && !currentLink) return linkOpacity @@ -48,68 +38,23 @@ const SankeyLinks = ({ const getLinkPath = layout === 'horizontal' ? sankeyLinkHorizontal() : sankeyLinkVertical() - if (animate !== true) { - return ( - - {links.map(link => ( - - ))} - - ) - } - - const springConfig = { - stiffness: motionStiffness, - damping: motionDamping, - } - - return ( - - {links.map(link => ( - ({ - path: spring(getLinkPath(link, linkContract), springConfig), - color: spring(link.color, springConfig), - opacity: spring(getOpacity(link), springConfig), - })} - > - {style => ( - - )} - - ))} - - ) + return links.map(link => ( + + )) } SankeyLinks.propTypes = { @@ -128,25 +73,18 @@ SankeyLinks.propTypes = { color: PropTypes.string.isRequired, }) ).isRequired, - linkOpacity: PropTypes.number.isRequired, linkHoverOpacity: PropTypes.number.isRequired, linkHoverOthersOpacity: PropTypes.number.isRequired, linkContract: PropTypes.number.isRequired, linkBlendMode: blendModePropType.isRequired, enableLinkGradient: PropTypes.bool.isRequired, - - theme: PropTypes.object.isRequired, tooltip: PropTypes.func, - - ...motionPropTypes, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, setCurrentLink: PropTypes.func.isRequired, currentLink: PropTypes.object, isCurrentLink: PropTypes.func.isRequired, + isInteractive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, } -export default pure(SankeyLinks) +export default memo(SankeyLinks) diff --git a/packages/sankey/src/SankeyLinksItem.js b/packages/sankey/src/SankeyLinksItem.js index 143b4129e..9659c457c 100644 --- a/packages/sankey/src/SankeyLinksItem.js +++ b/packages/sankey/src/SankeyLinksItem.js @@ -6,11 +6,11 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Fragment } from 'react' +import React, { memo, useCallback, useMemo } from 'react' import PropTypes from 'prop-types' -import { compose, withPropsOnChange, withHandlers, pure } from 'recompose' -import { blendModePropType } from '@nivo/core' -import { BasicTooltip, Chip } from '@nivo/tooltip' +import { useSpring, animated } from 'react-spring' +import { blendModePropType, useMotionConfig } from '@nivo/core' +import { BasicTooltip, Chip, useTooltip } from '@nivo/tooltip' import SankeyLinkGradient from './SankeyLinkGradient' const tooltipStyles = { @@ -64,15 +64,62 @@ const SankeyLinksItem = ({ opacity, blendMode, enableGradient, - handleMouseEnter, - handleMouseMove, - handleMouseLeave, + setCurrent, + tooltip, + tooltipFormat, + isInteractive, onClick, }) => { const linkId = `${link.source.id}.${link.target.id}` + const { animate, config: springConfig } = useMotionConfig() + const animatedProps = useSpring({ + path, + color, + opacity, + config: springConfig, + immediate: !animate, + }) + + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const tooltipContent = useMemo(() => { + if (tooltip) { + return + } + + return } /> + }, [tooltip, tooltipFormat, link]) + + const handleMouseEnter = useCallback( + event => { + setCurrent(link) + showTooltipFromEvent(tooltipContent, event) + }, + [setCurrent, link, showTooltipFromEvent, tooltipContent] + ) + + const handleMouseMove = useCallback( + event => { + showTooltipFromEvent(tooltipContent, event) + }, + [showTooltipFromEvent, tooltipContent] + ) + + const handleMouseLeave = useCallback(() => { + setCurrent(null) + hideTooltip() + }, [setCurrent, hideTooltip]) + + const handleClick = useCallback( + event => { + onClick(link, event) + }, + [onClick, link] + ) + return ( - + <> {enableGradient && ( )} - - + ) } @@ -118,54 +165,11 @@ SankeyLinksItem.propTypes = { opacity: PropTypes.number.isRequired, blendMode: blendModePropType.isRequired, enableGradient: PropTypes.bool.isRequired, - - theme: PropTypes.object.isRequired, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, setCurrent: PropTypes.func.isRequired, + isInteractive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, - handleMouseEnter: PropTypes.func.isRequired, - handleMouseMove: PropTypes.func.isRequired, - handleMouseLeave: PropTypes.func.isRequired, + tooltip: PropTypes.func, + tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), } -const enhance = compose( - withPropsOnChange( - ['link', 'theme', 'tooltip', 'tooltipFormat'], - ({ link, theme, tooltip, tooltipFormat }) => { - if (tooltip) { - return { - tooltip: , - } - } - return { - tooltip: ( - } - theme={theme} - /> - ), - } - } - ), - withPropsOnChange(['onClick', 'link'], ({ onClick, link }) => ({ - onClick: event => onClick(link, event), - })), - withHandlers({ - handleMouseEnter: ({ showTooltip, setCurrent, link, tooltip }) => e => { - setCurrent(link) - showTooltip(tooltip, e) - }, - handleMouseMove: ({ showTooltip, tooltip }) => e => { - showTooltip(tooltip, e) - }, - handleMouseLeave: ({ hideTooltip, setCurrent }) => () => { - setCurrent(null) - hideTooltip() - }, - }), - pure -) - -export default enhance(SankeyLinksItem) +export default memo(SankeyLinksItem) diff --git a/packages/sankey/src/SankeyNodes.js b/packages/sankey/src/SankeyNodes.js index 773fe354b..52942c878 100644 --- a/packages/sankey/src/SankeyNodes.js +++ b/packages/sankey/src/SankeyNodes.js @@ -6,37 +6,24 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React, { Fragment } from 'react' +import React, { memo } from 'react' import PropTypes from 'prop-types' -import pure from 'recompose/pure' -import { TransitionMotion, spring } from 'react-motion' -import { motionPropTypes } from '@nivo/core' -import { interpolateColor, getInterpolatedColor } from '@nivo/colors' import SankeyNodesItem from './SankeyNodesItem' const SankeyNodes = ({ nodes, - nodeOpacity, nodeHoverOpacity, nodeHoverOthersOpacity, nodeBorderWidth, getNodeBorderColor, - - animate, - motionDamping, - motionStiffness, - - showTooltip, - hideTooltip, setCurrentNode, currentNode, currentLink, isCurrentNode, + isInteractive, onClick, - tooltip, - theme, }) => { const getOpacity = node => { if (!currentNode && !currentLink) return nodeOpacity @@ -44,87 +31,24 @@ const SankeyNodes = ({ return nodeHoverOthersOpacity } - if (!animate) { - return ( - - {nodes.map(node => { - return ( - - ) - })} - - ) - } - - const springProps = { - damping: motionDamping, - stiffness: motionStiffness, - } - - return ( - { - return { - key: node.id, - data: node, - style: { - x: spring(node.x, springProps), - y: spring(node.y, springProps), - width: spring(node.width, springProps), - height: spring(node.height, springProps), - opacity: spring(getOpacity(node), springProps), - ...interpolateColor(node.color, springProps), - }, - } - })} - > - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, style, data: node }) => { - const color = getInterpolatedColor(style) - - return ( - - ) - })} - - )} - - ) + return nodes.map(node => ( + + )) } SankeyNodes.propTypes = { @@ -138,25 +62,18 @@ SankeyNodes.propTypes = { color: PropTypes.string.isRequired, }) ).isRequired, - nodeOpacity: PropTypes.number.isRequired, nodeHoverOpacity: PropTypes.number.isRequired, nodeHoverOthersOpacity: PropTypes.number.isRequired, nodeBorderWidth: PropTypes.number.isRequired, getNodeBorderColor: PropTypes.func.isRequired, - - theme: PropTypes.object.isRequired, tooltip: PropTypes.func, - - ...motionPropTypes, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, setCurrentNode: PropTypes.func.isRequired, currentNode: PropTypes.object, currentLink: PropTypes.object, isCurrentNode: PropTypes.func.isRequired, + isInteractive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, } -export default pure(SankeyNodes) +export default memo(SankeyNodes) diff --git a/packages/sankey/src/SankeyNodesItem.js b/packages/sankey/src/SankeyNodesItem.js index e3d23a62e..3aed9f9c5 100644 --- a/packages/sankey/src/SankeyNodesItem.js +++ b/packages/sankey/src/SankeyNodesItem.js @@ -6,45 +6,91 @@ * 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, useCallback, useMemo } from 'react' import PropTypes from 'prop-types' -import compose from 'recompose/compose' -import withPropsOnChange from 'recompose/withPropsOnChange' -import withHandlers from 'recompose/withHandlers' -import pure from 'recompose/pure' -import { BasicTooltip } from '@nivo/tooltip' +import { useSpring, animated } from 'react-spring' +import { useMotionConfig } from '@nivo/core' +import { BasicTooltip, useTooltip } from '@nivo/tooltip' const SankeyNodesItem = ({ + node, x, y, width, height, - color, opacity, borderWidth, borderColor, - - handleMouseEnter, - handleMouseMove, - handleMouseLeave, + setCurrent, + isInteractive, onClick, + tooltip, }) => { + const { animate, config: springConfig } = useMotionConfig() + const animatedProps = useSpring({ + x, + y, + width, + height, + opacity, + color, + config: springConfig, + immediate: !animate, + }) + + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const tooltipContent = useMemo(() => { + if (tooltip) { + return + } + + return + }, [tooltip, node]) + + const handleMouseEnter = useCallback( + event => { + setCurrent(node) + showTooltipFromEvent(tooltipContent, event) + }, + [setCurrent, node, showTooltipFromEvent, tooltipContent] + ) + + const handleMouseMove = useCallback( + event => { + showTooltipFromEvent(tooltipContent, event) + }, + [showTooltipFromEvent, tooltipContent] + ) + + const handleMouseLeave = useCallback(() => { + setCurrent(null) + hideTooltip() + }, [setCurrent, hideTooltip]) + + const handleClick = useCallback( + event => { + onClick(node, event) + }, + [onClick, node] + ) + return ( - Math.max(v, 0))} + height={animatedProps.height.interpolate(v => Math.max(v, 0))} + fill={animatedProps.color} + fillOpacity={animatedProps.opacity} strokeWidth={borderWidth} stroke={borderColor} strokeOpacity={opacity} - onMouseEnter={handleMouseEnter} - onMouseMove={handleMouseMove} - onMouseLeave={handleMouseLeave} - onClick={onClick} + onMouseEnter={isInteractive ? handleMouseEnter : undefined} + onMouseMove={isInteractive ? handleMouseMove : undefined} + onMouseLeave={isInteractive ? handleMouseLeave : undefined} + onClick={isInteractive ? handleClick : undefined} /> ) } @@ -55,59 +101,18 @@ SankeyNodesItem.propTypes = { label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, color: PropTypes.string.isRequired, }), - x: PropTypes.number.isRequired, y: PropTypes.number.isRequired, width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, - color: PropTypes.string.isRequired, opacity: PropTypes.number.isRequired, borderWidth: PropTypes.number.isRequired, borderColor: PropTypes.string.isRequired, - - showTooltip: PropTypes.func.isRequired, - hideTooltip: PropTypes.func.isRequired, setCurrent: PropTypes.func.isRequired, + isInteractive: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, - handleMouseEnter: PropTypes.func.isRequired, - handleMouseMove: PropTypes.func.isRequired, - handleMouseLeave: PropTypes.func.isRequired, - - tooltip: PropTypes.element.isRequired, - theme: PropTypes.object.isRequired, + tooltip: PropTypes.func, } -const enhance = compose( - withPropsOnChange(['node', 'theme', 'tooltip'], ({ node, theme, tooltip }) => { - if (tooltip) { - return { - tooltip: , - } - } - return { - tooltip: ( - - ), - } - }), - withPropsOnChange(['onClick', 'node'], ({ onClick, node }) => ({ - onClick: event => onClick(node, event), - })), - withHandlers({ - handleMouseEnter: ({ showTooltip, setCurrent, node, tooltip }) => e => { - setCurrent(node) - showTooltip(tooltip, e) - }, - handleMouseMove: ({ showTooltip, tooltip }) => e => { - showTooltip(tooltip, e) - }, - handleMouseLeave: ({ hideTooltip, setCurrent }) => () => { - setCurrent(null) - hideTooltip() - }, - }), - pure -) - -export default enhance(SankeyNodesItem) +export default memo(SankeyNodesItem) diff --git a/packages/sankey/src/enhance.js b/packages/sankey/src/enhance.js deleted file mode 100644 index 5193006a3..000000000 --- a/packages/sankey/src/enhance.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * 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 { cloneDeep } from 'lodash' -import { compose, defaultProps, withState, withPropsOnChange, pure } from 'recompose' -import { sankey as d3Sankey } from 'd3-sankey' -import { getLabelGenerator, withTheme, withDimensions, withMotion } from '@nivo/core' -import { getOrdinalColorScale, getInheritedColorGenerator } from '@nivo/colors' -import { SankeyDefaultProps, sankeyAlignmentFromProp } from './props' - -const getId = d => d.id - -export default Component => - compose( - defaultProps(SankeyDefaultProps), - withState('currentNode', 'setCurrentNode', null), - withState('currentLink', 'setCurrentLink', null), - withTheme(), - withDimensions(), - withMotion(), - withPropsOnChange(['colors'], ({ colors }) => ({ - getColor: getOrdinalColorScale(colors, 'id'), - getLinkColor: getOrdinalColorScale(colors, 'source.id'), - })), - withPropsOnChange(['nodeBorderColor', 'theme'], ({ nodeBorderColor, theme }) => ({ - getNodeBorderColor: getInheritedColorGenerator(nodeBorderColor, theme), - })), - withPropsOnChange(['labelTextColor', 'theme'], ({ labelTextColor, theme }) => ({ - getLabelTextColor: getInheritedColorGenerator(labelTextColor, theme), - })), - withPropsOnChange(['label', 'labelFormat'], ({ label, labelFormat }) => ({ - getLabel: getLabelGenerator(label, labelFormat), - })), - withPropsOnChange(['sort'], ({ sort }) => { - let sortFunction = sort - if (sort === 'auto') { - sortFunction = undefined - } else if (sort === 'input') { - sortFunction = null - } else if (sort === 'ascending') { - sortFunction = (a, b) => a.value - b.value - } else if (sort === 'descending') { - sortFunction = (a, b) => b.value - a.value - } - - return { sortFunction } - }), - withPropsOnChange(['align'], ({ align }) => { - return { - alignFunction: sankeyAlignmentFromProp(align), - } - }), - withPropsOnChange( - [ - 'data', - 'layout', - 'alignFunction', - 'sortFunction', - 'nodeThickness', - 'nodeSpacing', - 'nodeInnerPadding', - 'width', - 'height', - 'getColor', - 'getLinkColor', - 'getLabel', - ], - ({ - data: _data, - layout, - alignFunction, - sortFunction, - nodeThickness, - nodeSpacing, - nodeInnerPadding, - width, - height, - getColor, - getLinkColor, - getLabel, - }) => { - const sankey = d3Sankey() - .nodeAlign(alignFunction) - .nodeSort(sortFunction) - .nodeWidth(nodeThickness) - .nodePadding(nodeSpacing) - .size(layout === 'horizontal' ? [width, height] : [height, width]) - .nodeId(getId) - - // deep clone is required as the sankey diagram mutates data - // we need a different identity for correct updates - const data = cloneDeep(_data) - sankey(data) - - data.nodes.forEach(node => { - node.color = getColor(node) - node.label = getLabel(node) - if (layout === 'horizontal') { - node.x = node.x0 + nodeInnerPadding - node.y = node.y0 - node.width = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0) - node.height = Math.max(node.y1 - node.y0, 0) - } else { - node.x = node.y0 - node.y = node.x0 + nodeInnerPadding - node.width = Math.max(node.y1 - node.y0, 0) - node.height = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0) - - const oldX0 = node.x0 - const oldX1 = node.x1 - - node.x0 = node.y0 - node.x1 = node.y1 - node.y0 = oldX0 - node.y1 = oldX1 - } - }) - - data.links.forEach(link => { - link.color = getLinkColor(link) - link.pos0 = link.y0 - link.pos1 = link.y1 - link.thickness = link.width - delete link.y0 - delete link.y1 - delete link.width - }) - - return data - } - ), - withPropsOnChange(['nodes'], ({ nodes }) => { - return { - legendData: nodes.map(node => ({ - id: node.id, - label: node.label, - color: node.color, - })), - } - }), - pure - )(Component) diff --git a/packages/sankey/src/hooks.js b/packages/sankey/src/hooks.js new file mode 100644 index 000000000..287eb505f --- /dev/null +++ b/packages/sankey/src/hooks.js @@ -0,0 +1,166 @@ +import { useState, useMemo } from 'react' +import { cloneDeep } from 'lodash' +import { sankey as d3Sankey } from 'd3-sankey' +import { useTheme, getLabelGenerator } from '@nivo/core' +import { useOrdinalColorScale, useInheritedColor } from '@nivo/colors' +import { sankeyAlignmentFromProp } from './props' + +const getId = d => d.id + +export const computeNodeAndLinks = ({ + data: _data, + layout, + alignFunction, + sortFunction, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + width, + height, + getColor, + getLinkColor, + getLabel, +}) => { + const sankey = d3Sankey() + .nodeAlign(alignFunction) + .nodeSort(sortFunction) + .nodeWidth(nodeThickness) + .nodePadding(nodeSpacing) + .size(layout === 'horizontal' ? [width, height] : [height, width]) + .nodeId(getId) + + // deep clone is required as the sankey diagram mutates data + // we need a different identity for correct updates + const data = cloneDeep(_data) + sankey(data) + + data.nodes.forEach(node => { + node.color = getColor(node) + node.label = getLabel(node) + if (layout === 'horizontal') { + node.x = node.x0 + nodeInnerPadding + node.y = node.y0 + node.width = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0) + node.height = Math.max(node.y1 - node.y0, 0) + } else { + node.x = node.y0 + node.y = node.x0 + nodeInnerPadding + node.width = Math.max(node.y1 - node.y0, 0) + node.height = Math.max(node.x1 - node.x0 - nodeInnerPadding * 2, 0) + + const oldX0 = node.x0 + const oldX1 = node.x1 + + node.x0 = node.y0 + node.x1 = node.y1 + node.y0 = oldX0 + node.y1 = oldX1 + } + }) + + data.links.forEach(link => { + link.color = getLinkColor(link) + link.pos0 = link.y0 + link.pos1 = link.y1 + link.thickness = link.width + delete link.y0 + delete link.y1 + delete link.width + }) + + return data +} + +export const useSankey = ({ + data, + layout, + width, + height, + sort, + align, + colors, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + nodeBorderColor, + label, + labelFormat, + labelTextColor, +}) => { + const [currentNode, setCurrentNode] = useState(null) + const [currentLink, setCurrentLink] = useState(null) + + const sortFunction = useMemo(() => { + if (sort === 'auto') return undefined + if (sort === 'input') return null + if (sort === 'ascending') return (a, b) => a.value - b.value + if (sort === 'descending') return (a, b) => b.value - a.value + + return sort + }, [sort]) + + const alignFunction = useMemo(() => sankeyAlignmentFromProp(align), [align]) + + const theme = useTheme() + + const getColor = useOrdinalColorScale(colors, 'id') + const getNodeBorderColor = useInheritedColor(nodeBorderColor, theme) + const getLinkColor = useOrdinalColorScale(colors, 'source.id') + + const getLabel = useMemo(() => getLabelGenerator(label, labelFormat), [label, labelFormat]) + const getLabelTextColor = useInheritedColor(labelTextColor, theme) + + const { nodes, links } = useMemo( + () => + computeNodeAndLinks({ + data, + layout, + alignFunction, + sortFunction, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + width, + height, + getColor, + getLinkColor, + getLabel, + }), + [ + data, + layout, + alignFunction, + sortFunction, + nodeThickness, + nodeSpacing, + nodeInnerPadding, + width, + height, + getColor, + getLinkColor, + getLabel, + ] + ) + + const legendData = useMemo( + () => + nodes.map(node => ({ + id: node.id, + label: node.label, + color: node.color, + })), + [nodes] + ) + + return { + nodes, + links, + legendData, + getNodeBorderColor, + currentNode, + setCurrentNode, + currentLink, + setCurrentLink, + getLabelTextColor, + } +} diff --git a/packages/sankey/src/props.js b/packages/sankey/src/props.js index 994e6953a..3596ccc7b 100644 --- a/packages/sankey/src/props.js +++ b/packages/sankey/src/props.js @@ -70,10 +70,8 @@ export const SankeyPropTypes = { labelPadding: PropTypes.number.isRequired, labelOrientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, labelTextColor: inheritedColorPropType, - getLabelTextColor: PropTypes.func.isRequired, // computed label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, labelFormat: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - getLabel: PropTypes.func.isRequired, // computed nodeTooltip: PropTypes.func, linkTooltip: PropTypes.func, @@ -130,4 +128,7 @@ export const SankeyDefaultProps = { legends: [], layers: ['links', 'nodes', 'labels', 'legends'], + + animate: true, + motionConfig: 'gentle', } diff --git a/website/src/data/components/sankey/props.js b/website/src/data/components/sankey/props.js index 2fc909de7..a981a7a42 100644 --- a/website/src/data/components/sankey/props.js +++ b/website/src/data/components/sankey/props.js @@ -367,7 +367,7 @@ const props = [ controlType: 'switch', group: 'Interactivity', }, - ...motionProperties(['svg'], defaults), + ...motionProperties(['svg'], defaults, 'react-spring'), ] export const groups = groupProperties(props) diff --git a/website/src/pages/sankey/index.js b/website/src/pages/sankey/index.js index 5628f7ac7..93c324930 100644 --- a/website/src/pages/sankey/index.js +++ b/website/src/pages/sankey/index.js @@ -55,11 +55,10 @@ const initialProperties = { modifiers: [['darker', 1]], }, - animate: true, - motionStiffness: 140, - motionDamping: 13, + animate: SankeyDefaultProps.animate, + motionConfig: 'wobbly', // SankeyDefaultProps.motionConfig, - isInteractive: true, + isInteractive: SankeyDefaultProps.isInteractive, legends: [ {