diff --git a/plugins/woocommerce-admin/client/components/d3/base/README.md b/plugins/woocommerce-admin/client/components/d3/base/README.md new file mode 100644 index 00000000000..7c6a227cdc1 --- /dev/null +++ b/plugins/woocommerce-admin/client/components/d3/base/README.md @@ -0,0 +1,23 @@ +# D3 Base Component + +Integrate React Lifecyle methods with d3.js charts. + +### Base Component Responsibilities + +* Create and manage mounting and unmounting parent `div` and `svg` +* Handle resize events, resulting re-renders, and event listeners +* Handle re-renders as a result of new props + +## Props + +### className +{ string } A class to be applied to the parent `div` + +### getParams( node ) +{ function } A function returning an object containing required properties for drawing a chart. This object is created before re-render, making it an ideal place for calculating scales and other props or user input based properties. +* `svg` { node } The parent `div`. Useful for calculating available widths + +### drawChart( svg, params ) +{ function } draw the chart +* `svg` { node } Base element +* `params` { Object } Properties created by the `getParams` function \ No newline at end of file diff --git a/plugins/woocommerce-admin/client/components/d3/base/index.js b/plugins/woocommerce-admin/client/components/d3/base/index.js new file mode 100644 index 00000000000..1a93ff40b9e --- /dev/null +++ b/plugins/woocommerce-admin/client/components/d3/base/index.js @@ -0,0 +1,80 @@ +/** @format */ + +/** + * External dependencies + */ + +import { Component } from '@wordpress/element'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { select as d3Select } from 'd3-selection'; + +class D3Base extends Component { + constructor() { + super( ...arguments ); + this.updateParams = this.updateParams.bind( this ); + this.setNodeRef = this.setNodeRef.bind( this ); + this.state = {}; + } + + componentDidMount() { + window.addEventListener( 'resize', this.updateParams ); + this.updateParams(); + } + + componentWillReceiveProps( nextProps ) { + this.updateParams( nextProps ); + } + + componentDidUpdate() { + this.draw(); + } + + componentWillUnmount() { + window.removeEventListener( 'resize', this.updateParams ); + delete this.node; + } + + updateParams( nextProps ) { + const getParams = ( nextProps && nextProps.getParams ) || this.props.getParams; + this.setState( getParams( this.node ), this.draw ); + } + + draw() { + this.props.drawChart( this.createNewContext(), this.state ); + } + + createNewContext() { + const { className } = this.props; + const { width, height } = this.state; + + d3Select( this.node ) + .selectAll( 'svg' ) + .remove(); + const newNode = d3Select( this.node ) + .append( 'svg' ) + .attr( 'class', `${ className }__viewbox` ) + .attr( 'viewBox', `0 0 ${ width } ${ height }` ) + .attr( 'preserveAspectRatio', 'xMidYMid meet' ) + .append( 'g' ); + return newNode; + } + + setNodeRef( node ) { + this.node = node; + } + + render() { + return ( +
+ ); + } +} + +D3Base.propTypes = { + className: PropTypes.string, + drawChart: PropTypes.func.isRequired, + getParams: PropTypes.func.isRequired, +}; + +export default D3Base; diff --git a/plugins/woocommerce-admin/client/components/d3/base/style.scss b/plugins/woocommerce-admin/client/components/d3/base/style.scss new file mode 100644 index 00000000000..6d2f519d07c --- /dev/null +++ b/plugins/woocommerce-admin/client/components/d3/base/style.scss @@ -0,0 +1,3 @@ +.d3-base { + width:100%; +} diff --git a/plugins/woocommerce-admin/client/components/d3/base/test/index.js b/plugins/woocommerce-admin/client/components/d3/base/test/index.js new file mode 100644 index 00000000000..5b88312041e --- /dev/null +++ b/plugins/woocommerce-admin/client/components/d3/base/test/index.js @@ -0,0 +1,45 @@ +/** + * External dependencies + * + * @format + */ +import { shallow, mount } from 'enzyme'; +import { noop } from 'lodash'; + +/** + * Internal dependencies + */ +import D3Base from '../index'; + +describe( 'D3base', () => { + const shallowWithoutLifecycle = arg => shallow( arg, { disableLifecycleMethods: true } ); + + test( 'should have d3Base class', () => { + const base = shallowWithoutLifecycle( ); + expect( base.find( '.d3-base' ) ).toHaveLength( 1 ); + } ); + + test( 'should render an svg', () => { + const base = mount( ); + expect( base.render().find( 'svg' ) ).toHaveLength( 1 ); + } ); + + test( 'should render a result of the drawChart prop', () => { + const drawChart = svg => { + return svg.append( 'circle' ); + }; + const base = mount( ); + expect( base.render().find( 'circle' ) ).toHaveLength( 1 ); + } ); + + test( 'should pass a property of getParams output to drawChart function', () => { + const getParams = () => ( { + tagName: 'circle', + } ); + const drawChart = ( svg, params ) => { + return svg.append( params.tagName ); + }; + const base = mount( ); + expect( base.render().find( 'circle' ) ).toHaveLength( 1 ); + } ); +} ); diff --git a/plugins/woocommerce-admin/package.json b/plugins/woocommerce-admin/package.json index acb1798dda5..06dade0ff9e 100755 --- a/plugins/woocommerce-admin/package.json +++ b/plugins/woocommerce-admin/package.json @@ -63,6 +63,7 @@ }, "dependencies": { "classnames": "^2.2.5", + "d3-selection": "^1.3.0", "lodash": "^4.17.10", "prop-types": "^15.6.1", "react-slot-fill": "^2.0.1"