From f823ea61e5766d98ef80db16f6b88f1ed473dbb2 Mon Sep 17 00:00:00 2001 From: plouc Date: Sat, 20 Jun 2020 09:13:13 +0900 Subject: [PATCH] feat(heatmap): use hooks instead of recompose for HeatMapCanvas --- packages/core/src/motion/context.js | 5 +- packages/core/src/props/index.js | 10 +- packages/heatmap/package.json | 6 +- packages/heatmap/src/HeatMap.js | 21 +- packages/heatmap/src/HeatMapCanvas.js | 360 +++++++++++------- packages/heatmap/src/HeatMapCellCircle.js | 10 +- packages/heatmap/src/HeatMapCellRect.js | 10 +- packages/heatmap/src/HeatMapCellTooltip.js | 14 +- packages/heatmap/src/HeatMapCells.js | 3 +- packages/heatmap/src/canvas.js | 4 +- packages/heatmap/src/enhance.js | 142 ------- .../tests/__snapshots__/HeatMap.test.js.snap | 54 +-- website/src/pages/heatmap/canvas.js | 4 +- website/src/pages/heatmap/index.js | 6 +- 14 files changed, 280 insertions(+), 369 deletions(-) delete mode 100644 packages/heatmap/src/enhance.js diff --git a/packages/core/src/motion/context.js b/packages/core/src/motion/context.js index 5dc799c5fd..5dea31adef 100644 --- a/packages/core/src/motion/context.js +++ b/packages/core/src/motion/context.js @@ -50,9 +50,12 @@ MotionConfigProvider.propTypes = { }), ]), } -MotionConfigProvider.defaultProps = { + +export const MotionDefaultProps = { animate: true, stiffness: 90, damping: 15, config: 'default', } + +MotionConfigProvider.defaultProps = MotionDefaultProps diff --git a/packages/core/src/props/index.js b/packages/core/src/props/index.js index 463d92246f..c9c56df35a 100644 --- a/packages/core/src/props/index.js +++ b/packages/core/src/props/index.js @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ import PropTypes from 'prop-types' -import { MotionConfigProvider } from '../motion' +import { MotionDefaultProps } from '../motion' export const marginPropType = PropTypes.shape({ top: PropTypes.number, @@ -17,10 +17,10 @@ export const marginPropType = PropTypes.shape({ }).isRequired export const motionPropTypes = { - animate: MotionConfigProvider.propTypes.animate, - motionStiffness: MotionConfigProvider.propTypes.stiffness, - motionDamping: MotionConfigProvider.propTypes.damping, - motionConfig: MotionConfigProvider.propTypes.config, + animate: MotionDefaultProps.animate, + motionStiffness: MotionDefaultProps.stiffness, + motionDamping: MotionDefaultProps.damping, + motionConfig: MotionDefaultProps.config, } export const blendModes = [ diff --git a/packages/heatmap/package.json b/packages/heatmap/package.json index 710e9e1065..d3cec0f26e 100644 --- a/packages/heatmap/package.json +++ b/packages/heatmap/package.json @@ -25,14 +25,12 @@ "dependencies": { "@nivo/axes": "0.62.0", "@nivo/colors": "0.62.0", - "@nivo/core": "0.62.0", "@nivo/tooltip": "0.62.0", "d3-scale": "^3.0.0", - "lodash": "^4.17.11", - "react-spring": "^8.0.27", - "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/heatmap/src/HeatMap.js b/packages/heatmap/src/HeatMap.js index 2f63b4b88d..0ee6dc00fc 100644 --- a/packages/heatmap/src/HeatMap.js +++ b/packages/heatmap/src/HeatMap.js @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ import React from 'react' -import { SvgWrapper, withContainer, useTheme, useDimensions } from '@nivo/core' +import { SvgWrapper, withContainer, useDimensions } from '@nivo/core' import { Axes, Grid } from '@nivo/axes' import { useTooltip } from '@nivo/tooltip' import { HeatMapPropTypes, HeatMapDefaultProps } from './props' @@ -52,19 +52,12 @@ const HeatMap = ({ tooltipFormat, tooltip, }) => { - const theme = useTheme() - const { showTooltipFromEvent, hideTooltip } = useTooltip() const handleNodeHover = (node, event) => { setCurrentNode(node) showTooltipFromEvent( - , + , event ) } @@ -156,8 +149,8 @@ const HeatMap = ({ ) diff --git a/packages/heatmap/src/HeatMapCanvas.js b/packages/heatmap/src/HeatMapCanvas.js index 63a7087f42..1844dcd6b8 100644 --- a/packages/heatmap/src/HeatMapCanvas.js +++ b/packages/heatmap/src/HeatMapCanvas.js @@ -6,90 +6,128 @@ * 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 partial from 'lodash/partial' -import { Container, getRelativeCursor, isCursorInRect } from '@nivo/core' +import React, { useEffect, useRef, useCallback } from 'react' +import { + getRelativeCursor, + isCursorInRect, + useDimensions, + useTheme, + withContainer, +} from '@nivo/core' import { renderAxesToCanvas } from '@nivo/axes' +import { useTooltip } from '@nivo/tooltip' +import { useHeatMap } from './hooks' +import { HeatMapDefaultProps, HeatMapPropTypes } from './props' import { renderRect, renderCircle } from './canvas' import computeNodes from './computeNodes' import HeatMapCellTooltip from './HeatMapCellTooltip' -import { HeatMapPropTypes } from './props' -import enhance from './enhance' - -class HeatMapCanvas extends Component { - componentDidMount() { - this.ctx = this.surface.getContext('2d') - this.draw(this.props) - } - - shouldComponentUpdate(props) { - if ( - this.props.outerWidth !== props.outerWidth || - this.props.outerHeight !== props.outerHeight || - this.props.isInteractive !== props.isInteractive || - this.props.theme !== props.theme - ) { - return true - } else { - this.draw(props) - return false - } - } - - componentDidUpdate() { - this.ctx = this.surface.getContext('2d') - this.draw(this.props) - } - - draw(props) { - const { - width, - height, - outerWidth, - outerHeight, - pixelRatio, - margin, - offsetX, - offsetY, +const HeatMapCanvas = ({ + data, + keys, + indexBy, + minValue, + maxValue, + width, + height, + margin: partialMargin, + forceSquare, + padding, + sizeVariation, + cellShape, + cellOpacity, + cellBorderColor, + axisTop, + axisRight, + axisBottom, + axisLeft, + enableLabels, + labelTextColor, + colors, + nanColor, + isInteractive, + onClick, + hoverTarget, + cellHoverOpacity, + cellHoverOthersOpacity, + tooltipFormat, + tooltip, + pixelRatio, +}) => { + const canvasEl = useRef(null) + const { margin, innerWidth, innerHeight, outerWidth, outerHeight } = useDimensions( + width, + height, + partialMargin + ) + + const { + getIndex, + xScale, + yScale, + cellWidth, + cellHeight, + offsetX, + offsetY, + sizeScale, + currentNode, + setCurrentNode, + colorScale, + getLabelTextColor, + } = useHeatMap({ + data, + keys, + indexBy, + minValue, + maxValue, + width: innerWidth, + height: innerHeight, + padding, + forceSquare, + sizeVariation, + colors, + cellBorderColor, + labelTextColor, + }) + + const nodes = computeNodes({ + data, + keys, + getIndex, + xScale, + yScale, + sizeScale, + cellOpacity, + cellWidth, + cellHeight, + colorScale, + nanColor, + getLabelTextColor, + currentNode, + hoverTarget, + cellHoverOpacity, + cellHoverOthersOpacity, + }) + + const theme = useTheme() + + useEffect(() => { + canvasEl.current.width = outerWidth * pixelRatio + canvasEl.current.height = outerHeight * pixelRatio + + const ctx = canvasEl.current.getContext('2d') + + ctx.scale(pixelRatio, pixelRatio) + + ctx.fillStyle = theme.background + ctx.fillRect(0, 0, outerWidth, outerHeight) + ctx.translate(margin.left + offsetX, margin.top + offsetY) + + renderAxesToCanvas(ctx, { xScale, yScale, - - axisTop, - axisRight, - axisBottom, - axisLeft, - - cellShape, - - enableLabels, - - theme, - } = props - - this.surface.width = outerWidth * pixelRatio - this.surface.height = outerHeight * pixelRatio - - this.ctx.scale(pixelRatio, pixelRatio) - - let renderNode - if (cellShape === 'rect') { - renderNode = partial(renderRect, this.ctx, { enableLabels, theme }) - } else { - renderNode = partial(renderCircle, this.ctx, { enableLabels, theme }) - } - - const nodes = computeNodes(props) - - this.ctx.fillStyle = theme.background - this.ctx.fillRect(0, 0, outerWidth, outerHeight) - this.ctx.translate(margin.left + offsetX, margin.top + offsetY) - - renderAxesToCanvas(this.ctx, { - xScale, - yScale, - width: width - offsetX * 2, - height: height - offsetY * 2, + width: innerWidth - offsetX * 2, + height: innerHeight - offsetY * 2, top: axisTop, right: axisRight, bottom: axisBottom, @@ -97,78 +135,112 @@ class HeatMapCanvas extends Component { theme, }) - this.ctx.textAlign = 'center' - this.ctx.textBaseline = 'middle' - - nodes.forEach(renderNode) - - this.nodes = nodes - } - - handleMouseHover = (showTooltip, hideTooltip, event) => { - if (!this.nodes) return - - const [x, y] = getRelativeCursor(this.surface, event) - - const { margin, offsetX, offsetY, theme, setCurrentNode, tooltip } = this.props - const node = this.nodes.find(node => - isCursorInRect( - node.x + margin.left + offsetX - node.width / 2, - node.y + margin.top + offsetY - node.height / 2, - node.width, - node.height, - x, - y - ) - ) + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' - if (node !== undefined) { - setCurrentNode(node) - showTooltip(, event) + let renderNode + if (cellShape === 'rect') { + renderNode = renderRect } else { - setCurrentNode(null) - hideTooltip() + renderNode = renderCircle } - } - - handleMouseLeave = hideTooltip => { - this.props.setCurrentNode(null) + nodes.forEach(node => { + renderNode(ctx, { enableLabels, theme }, node) + }) + }, [ + canvasEl, + nodes, + outerWidth, + outerHeight, + innerWidth, + innerHeight, + margin, + offsetX, + offsetY, + cellShape, + axisTop, + axisRight, + axisBottom, + axisLeft, + theme, + enableLabels, + pixelRatio, + ]) + + const { showTooltipFromEvent, hideTooltip } = useTooltip() + + const handleMouseHover = useCallback( + event => { + const [x, y] = getRelativeCursor(canvasEl.current, event) + + const node = nodes.find(node => + isCursorInRect( + node.x + margin.left + offsetX - node.width / 2, + node.y + margin.top + offsetY - node.height / 2, + node.width, + node.height, + x, + y + ) + ) + if (node !== undefined) { + setCurrentNode(node) + showTooltipFromEvent( + , + event + ) + } else { + setCurrentNode(null) + hideTooltip() + } + }, + [ + canvasEl, + nodes, + margin, + offsetX, + offsetY, + setCurrentNode, + showTooltipFromEvent, + hideTooltip, + tooltip, + ] + ) + + const handleMouseLeave = useCallback(() => { + setCurrentNode(null) hideTooltip() - } - - handleClick = event => { - if (!this.props.currentNode) return - - this.props.onClick(this.props.currentNode, event) - } - - render() { - const { outerWidth, outerHeight, pixelRatio, isInteractive, theme } = this.props - - return ( - - {({ showTooltip, hideTooltip }) => ( - { - this.surface = surface - }} - width={outerWidth * pixelRatio} - height={outerHeight * pixelRatio} - style={{ - width: outerWidth, - height: outerHeight, - }} - onMouseEnter={partial(this.handleMouseHover, showTooltip, hideTooltip)} - onMouseMove={partial(this.handleMouseHover, showTooltip, hideTooltip)} - onMouseLeave={partial(this.handleMouseLeave, hideTooltip)} - onClick={this.handleClick} - /> - )} - - ) - } + }, [setCurrentNode, hideTooltip]) + + const handleClick = useCallback( + event => { + if (currentNode === null) return + + onClick(currentNode, event) + }, + [currentNode, onClick] + ) + + return ( + + ) } HeatMapCanvas.propTypes = HeatMapPropTypes -export default enhance(HeatMapCanvas) +const WrappedHeatMapCanvas = withContainer(HeatMapCanvas) +WrappedHeatMapCanvas.defaultProps = HeatMapDefaultProps + +export default WrappedHeatMapCanvas diff --git a/packages/heatmap/src/HeatMapCellCircle.js b/packages/heatmap/src/HeatMapCellCircle.js index 833445dfd8..50c1e44407 100644 --- a/packages/heatmap/src/HeatMapCellCircle.js +++ b/packages/heatmap/src/HeatMapCellCircle.js @@ -50,9 +50,7 @@ const HeatMapCellCircle = ({ onMouseEnter={onHover} onMouseMove={onHover} onMouseLeave={onLeave} - onClick={e => { - onClick(data, e) - }} + onClick={onClick ? event => onClick(data, event) : undefined} > { - onClick(data, e) - }} + onClick={onClick ? event => onClick(data, event) : undefined} > ( +const HeatMapCellTooltip = ({ node, format, tooltip }) => ( @@ -32,12 +30,6 @@ HeatMapCellTooltip.propTypes = { }).isRequired, format: PropTypes.func, tooltip: PropTypes.func, - theme: PropTypes.shape({ - tooltip: PropTypes.shape({ - container: PropTypes.object.isRequired, - basic: PropTypes.object.isRequired, - }).isRequired, - }).isRequired, } -export default pure(HeatMapCellTooltip) +export default memo(HeatMapCellTooltip) diff --git a/packages/heatmap/src/HeatMapCells.js b/packages/heatmap/src/HeatMapCells.js index a3140da14c..26dd8b965e 100644 --- a/packages/heatmap/src/HeatMapCells.js +++ b/packages/heatmap/src/HeatMapCells.js @@ -7,7 +7,6 @@ * file that was distributed with this source code. */ import React from 'react' -import partial from 'lodash/partial' const HeatMapCells = ({ nodes, @@ -35,7 +34,7 @@ const HeatMapCells = ({ borderColor: getCellBorderColor(node), enableLabel: enableLabels, textColor: getLabelTextColor(node), - onHover: partial(handleNodeHover, node), + onHover: handleNodeHover ? event => handleNodeHover(node, event) : undefined, onLeave: handleNodeLeave, onClick, }) diff --git a/packages/heatmap/src/canvas.js b/packages/heatmap/src/canvas.js index 8d5976906c..1c1d0bcb7a 100644 --- a/packages/heatmap/src/canvas.js +++ b/packages/heatmap/src/canvas.js @@ -16,7 +16,7 @@ * @param {number} y * @param {number} width * @param {number} height - * @param {string) color + * @param {string} color * @param {number} opacity * @param {string} labelTextColor * @param {number} value @@ -50,7 +50,7 @@ export const renderRect = ( * @param {number} y * @param {number} width * @param {number} height - * @param {string) color + * @param {string} color * @param {number} opacity * @param {string} labelTextColor * @param {number} value diff --git a/packages/heatmap/src/enhance.js b/packages/heatmap/src/enhance.js deleted file mode 100644 index 42049db6f9..0000000000 --- a/packages/heatmap/src/enhance.js +++ /dev/null @@ -1,142 +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 min from 'lodash/min' -import max from 'lodash/max' -import isEqual from 'lodash/isEqual' -import compose from 'recompose/compose' -import defaultProps from 'recompose/defaultProps' -import withPropsOnChange from 'recompose/withPropsOnChange' -import withState from 'recompose/withState' -import pure from 'recompose/pure' -import { scaleOrdinal, scaleLinear } from 'd3-scale' -import { - withTheme, - withDimensions, - withMotion, - guessQuantizeColorScale, - getAccessorFor, -} from '@nivo/core' -import { getInheritedColorGenerator } from '@nivo/colors' -import { HeatMapDefaultProps } from './props' - -const computeX = (column, cellWidth, padding) => { - return column * cellWidth + cellWidth * 0.5 + padding * column + padding -} -const computeY = (row, cellHeight, padding) => { - return row * cellHeight + cellHeight * 0.5 + padding * row + padding -} - -export default Component => - compose( - defaultProps(HeatMapDefaultProps), - withState('currentNode', 'setCurrentNode', null), - withTheme(), - withDimensions(), - withMotion(), - withPropsOnChange(['colors'], ({ colors }) => ({ - colorScale: guessQuantizeColorScale(colors), - })), - withPropsOnChange(['indexBy'], ({ indexBy }) => ({ - getIndex: getAccessorFor(indexBy), - })), - withPropsOnChange( - ['data', 'keys', 'width', 'height', 'padding', 'forceSquare'], - ({ data, keys, width, height, padding, forceSquare }) => { - const columns = keys.length - const rows = data.length - - let cellWidth = Math.max((width - padding * (columns + 1)) / columns, 0) - let cellHeight = Math.max((height - padding * (rows + 1)) / rows, 0) - - let offsetX = 0 - let offsetY = 0 - if (forceSquare === true) { - const cellSize = Math.min(cellWidth, cellHeight) - cellWidth = cellSize - cellHeight = cellSize - - offsetX = (width - ((cellWidth + padding) * columns + padding)) / 2 - offsetY = (height - ((cellHeight + padding) * rows + padding)) / 2 - } - - return { - cellWidth, - cellHeight, - offsetX, - offsetY, - } - } - ), - withPropsOnChange(['data', 'getIndex'], ({ data, getIndex }) => ({ - indices: data.map(getIndex), - })), - withPropsOnChange( - (prev, next) => - prev.keys !== next.keys || - prev.cellWidth !== next.cellWidth || - prev.cellHeight !== next.cellHeight || - prev.padding !== next.padding || - !isEqual(prev.indices, next.indices), - ({ indices, keys, cellWidth, cellHeight, padding }) => ({ - xScale: scaleOrdinal(keys.map((key, i) => computeX(i, cellWidth, padding))).domain( - keys - ), - yScale: scaleOrdinal( - indices.map((d, i) => computeY(i, cellHeight, padding)) - ).domain(indices), - }) - ), - withPropsOnChange( - ['data', 'keys', 'minValue', 'maxValue'], - ({ data, keys, minValue: _minValue, maxValue: _maxValue }) => { - let minValue = _minValue - let maxValue = _maxValue - if (minValue === 'auto' || maxValue === 'auto') { - const allValues = data.reduce( - (acc, row) => acc.concat(keys.map(key => row[key])), - [] - ) - - if (minValue === 'auto') minValue = min(allValues) - if (maxValue === 'auto') maxValue = max(allValues) - } - - return { - minValue: Math.min(minValue, maxValue), - maxValue: Math.max(maxValue, minValue), - } - } - ), - withPropsOnChange( - ['colorScale', 'minValue', 'maxValue'], - ({ colorScale, minValue, maxValue }) => ({ - colorScale: colorScale.domain([minValue, maxValue]), - }) - ), - withPropsOnChange( - ['sizeVariation', 'minValue', 'maxValue'], - ({ sizeVariation, minValue, maxValue }) => { - let sizeScale - if (sizeVariation > 0) { - sizeScale = scaleLinear() - .range([1 - sizeVariation, 1]) - .domain([minValue, maxValue]) - } - - return { sizeScale } - } - ), - withPropsOnChange(['cellBorderColor', 'theme'], ({ cellBorderColor, theme }) => ({ - getCellBorderColor: getInheritedColorGenerator(cellBorderColor, theme), - })), - withPropsOnChange(['labelTextColor', 'theme'], ({ labelTextColor, theme }) => ({ - getLabelTextColor: getInheritedColorGenerator(labelTextColor, theme), - })), - pure - )(Component) diff --git a/packages/heatmap/tests/__snapshots__/HeatMap.test.js.snap b/packages/heatmap/tests/__snapshots__/HeatMap.test.js.snap index 202137e05f..da10185933 100644 --- a/packages/heatmap/tests/__snapshots__/HeatMap.test.js.snap +++ b/packages/heatmap/tests/__snapshots__/HeatMap.test.js.snap @@ -277,10 +277,10 @@ exports[`should render a basic heat map chart 1`] = ` transform="translate(83.33333333333333, 50)" >