diff --git a/packages/nivo-core/src/lib/polar/utils.js b/packages/nivo-core/src/lib/polar/utils.js index 624666b82..2aca6f86d 100644 --- a/packages/nivo-core/src/lib/polar/utils.js +++ b/packages/nivo-core/src/lib/polar/utils.js @@ -16,3 +16,74 @@ export const positionFromAngle = (angle, distance) => ({ x: Math.cos(angle) * distance, y: Math.sin(angle) * distance, }) + +/** + * Computes the bounding box for a circle arc. + * + * Assumptions: + * - Anywhere the arc intersects an axis will be a max or a min. + * - If the arc doesn't intersect an axis, then the center + * will be one corner of the bounding rectangle, + * and this is the only case when it will be. + * - The only other possible extreme points of the sector to consider + * are the endpoints of the radii. + * + * This script was built within the help of this answer on stackoverflow: + * https://stackoverflow.com/questions/1336663/2d-bounding-box-of-a-sector + * + * @param {number} ox - circle x origin + * @param {number} oy - circle y origin + * @param {number} radius - circle radius + * @param {number} startAngle - arc start angle + * @param {number} endAngle - arc end angle + * @param {boolean} [includeCenter=true] - if true, include the center + * + * @return {{ points: *[][], x: number, y: number, width: number, height: number }} + */ +export const computeArcBoundingBox = ( + ox, + oy, + radius, + startAngle, + endAngle, + includeCenter = true +) => { + let points = [] + + const p0 = positionFromAngle(degreesToRadians(startAngle), radius) + points.push([p0.x, p0.y]) + + const p1 = positionFromAngle(degreesToRadians(endAngle), radius) + points.push([p1.x, p1.y]) + + for ( + let angle = Math.round(Math.min(startAngle, endAngle)); + angle <= Math.round(Math.max(startAngle, endAngle)); + angle++ + ) { + if (angle % 90 === 0) { + const p = positionFromAngle(degreesToRadians(angle), radius) + points.push([p.x, p.y]) + } + } + + points = points.map(([x, y]) => [ox + x, oy + y]) + if (includeCenter === true) points.push([ox, oy]) + + const xs = points.map(([x]) => x) + const ys = points.map(([, y]) => y) + + const x0 = Math.min(...xs) + const x1 = Math.max(...xs) + + const y0 = Math.min(...ys) + const y1 = Math.max(...ys) + + return { + points, + x: x0, + y: y0, + width: x1 - x0, + height: y1 - y0, + } +} diff --git a/packages/nivo-pie/src/Pie.js b/packages/nivo-pie/src/Pie.js index 5e3801a25..24fe0e4d9 100644 --- a/packages/nivo-pie/src/Pie.js +++ b/packages/nivo-pie/src/Pie.js @@ -6,13 +6,11 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -import React from 'react' -import { pie as d3Pie, arc as d3Arc } from 'd3-shape' +import React, { Component } from 'react' import { Motion, spring } from 'react-motion' +import setDisplayName from 'recompose/setDisplayName' import { getInheritedColorGenerator } from '@nivo/core' import { getLabelGenerator } from '@nivo/core' -import { degreesToRadians, radiansToDegrees } from '@nivo/core' -import { bindDefs } from '@nivo/core' import { Container, SvgWrapper } from '@nivo/core' import { BoxLegendSvg } from '@nivo/legends' import PieSlice from './PieSlice' @@ -20,213 +18,269 @@ import PieRadialLabels from './PieRadialLabels' import PieSlicesLabels from './PieSlicesLabels' import { PiePropTypes } from './props' import enhance from './enhance' +import PieLayout from './PieLayout' -const Pie = ({ - data, - - // dimensions - margin, - width, - height, - outerWidth, - outerHeight, - - sortByValue, - innerRadius: _innerRadius, - padAngle: _padAngle, - cornerRadius, - - // border - borderWidth, - borderColor: _borderColor, - - // radial labels - enableRadialLabels, - radialLabel, - radialLabelsSkipAngle, - radialLabelsLinkOffset, - radialLabelsLinkDiagonalLength, - radialLabelsLinkHorizontalLength, - radialLabelsLinkStrokeWidth, - radialLabelsTextXOffset, - radialLabelsTextColor, - radialLabelsLinkColor, - - // slices labels - enableSlicesLabels, - sliceLabel, - slicesLabelsSkipAngle, - slicesLabelsTextColor, - - // styling - theme, - getColor, - defs, - fill, - - // motion - animate, - motionStiffness, - motionDamping, - - // interactivity - isInteractive, - onClick, - tooltipFormat, - tooltip, - - legends, -}) => { - const centerX = width / 2 - const centerY = height / 2 - - const padAngle = degreesToRadians(_padAngle) - - const borderColor = getInheritedColorGenerator(_borderColor) - - const motionProps = { - animate, - motionDamping, - motionStiffness, - } +class Pie extends Component { + static propTypes = PiePropTypes - const radialLabelsProps = { - label: getLabelGenerator(radialLabel), - skipAngle: radialLabelsSkipAngle, - linkOffset: radialLabelsLinkOffset, - linkDiagonalLength: radialLabelsLinkDiagonalLength, - linkHorizontalLength: radialLabelsLinkHorizontalLength, - linkStrokeWidth: radialLabelsLinkStrokeWidth, - textXOffset: radialLabelsTextXOffset, - textColor: getInheritedColorGenerator(radialLabelsTextColor, 'labels.textColor'), - linkColor: getInheritedColorGenerator(radialLabelsLinkColor, 'axis.tickColor'), - } + render() { + const { + data, + sortByValue, - const slicesLabelsProps = { - label: getLabelGenerator(sliceLabel), - skipAngle: slicesLabelsSkipAngle, - textColor: getInheritedColorGenerator(slicesLabelsTextColor, 'labels.textColor'), - } + startAngle, + endAngle, + padAngle, + innerRadius, + cornerRadius, - const radius = Math.min(width, height) / 2 - const innerRadius = radius * Math.min(_innerRadius, 1) - - const pie = d3Pie() - pie.value(d => d.value) - if (sortByValue !== true) pie.sortValues(null) - - const arc = d3Arc() - arc.outerRadius(radius) - - const enhancedData = data.map(d => ({ - ...d, - color: getColor(d), - })) - - const legendData = enhancedData.map(d => ({ - label: d.label, - fill: d.color, - })) - - const boundDefs = bindDefs(defs, enhancedData, fill) - - return ( - - {({ showTooltip, hideTooltip }) => ( - - - {interpolatingStyle => { - const interpolatedPie = pie.padAngle(interpolatingStyle.padAngle) - const interpolatedArc = arc - .cornerRadius(interpolatingStyle.cornerRadius) - .innerRadius(interpolatingStyle.innerRadius) - - const arcsData = interpolatedPie(enhancedData).map(d => { - const angle = d.endAngle - d.startAngle - - return { - ...d, - angle, - angleDegrees: radiansToDegrees(angle), - data: d.data, - } - }) - - return ( - + {({ centerX, centerY, radius, innerRadius, arcs, arcGenerator }) => { + //console.log(arcs) + return ( + + {({ showTooltip, hideTooltip }) => ( + - {arcsData.map(d => ( - - ))} - {enableSlicesLabels && ( - - )} - {enableRadialLabels && ( - - )} - - ) - }} - - {legends.map((legend, i) => ( - - ))} - - )} - - ) -} + + {arcs.map(arc => ( + + ))} + {enableRadialLabels && ( + + )} + {enableSlicesLabels && ( + + )} + + + )} + + ) + }} + + ) + + /* + const springConfig = { + motionDamping, + motionStiffness, + precision: 0.001, + } + + const motionProps = { + animate, + ...springConfig, + } -Pie.propTypes = PiePropTypes + const radialLabelsProps = { + } -const enhancedPie = enhance(Pie) -enhancedPie.displayName = 'enhance(Pie)' + const slicesLabelsProps = { + label: getLabelGenerator(sliceLabel), + skipAngle: slicesLabelsSkipAngle, + textColor: getInheritedColorGenerator(slicesLabelsTextColor, 'labels.textColor'), + } + + return ( + + {({ showTooltip, hideTooltip }) => ( + + + {interpolatingStyle => { + const interpolatedArc = arc + .cornerRadius(interpolatingStyle.cornerRadius) + .innerRadius(interpolatingStyle.innerRadius) + + return ( + + {arcsData.map(d => { + return ( + + ) + })} + {enableSlicesLabels && ( + + )} + {enableRadialLabels && ( + + )} + + ) + }} + + {legends.map((legend, i) => ( + + ))} + + )} + + ) + */ + } +} -export default enhancedPie +export default setDisplayName('Pie')(enhance(Pie)) diff --git a/packages/nivo-pie/src/PieCanvas.js b/packages/nivo-pie/src/PieCanvas.js new file mode 100644 index 000000000..05c58c653 --- /dev/null +++ b/packages/nivo-pie/src/PieCanvas.js @@ -0,0 +1,55 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { Component } from 'react' +import setDisplayName from 'recompose/setDisplayName' +import { PiePropTypes } from './props' +import enhance from './enhance' +import PieLayout from './PieLayout' +import PieCanvasRenderer from './PieCanvasRenderer' + +class PieCanvas extends Component { + static propTypes = PiePropTypes + + render() { + const { + data, + sortByValue, + startAngle, + endAngle, + padAngle, + innerRadius, + cornerRadius, + width, + height, + colors, + colorBy, + ...topProps + } = this.props + + return ( + + {props => } + + ) + } +} + +export default setDisplayName('PieCanvas')(enhance(PieCanvas)) diff --git a/packages/nivo-pie/src/PieCanvasRenderer.js b/packages/nivo-pie/src/PieCanvasRenderer.js new file mode 100644 index 000000000..b07527e16 --- /dev/null +++ b/packages/nivo-pie/src/PieCanvasRenderer.js @@ -0,0 +1,211 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { + getHoveredArc, + getRelativeCursor, + getLabelGenerator, + getInheritedColorGenerator, + Container, +} from '@nivo/core' +import { arcPropType } from './props' +import PieTooltip from './PieTooltip' + +class PieCanvasRenderer extends Component { + static propTypes = { + arcs: PropTypes.arrayOf(arcPropType).isRequired, + arcGenerator: PropTypes.func.isRequired, + + // resolution + pixelRatio: PropTypes.number.isRequired, + + // dimensions/layout + outerWidth: PropTypes.number.isRequired, + outerHeight: PropTypes.number.isRequired, + centerX: PropTypes.number.isRequired, + centerY: PropTypes.number.isRequired, + margin: PropTypes.object.isRequired, + radius: PropTypes.number.isRequired, + innerRadius: PropTypes.number.isRequired, + + // interactivity + isInteractive: PropTypes.bool.isRequired, + onClick: PropTypes.func.isRequired, + + // theming + theme: PropTypes.object.isRequired, + } + + componentDidMount() { + this.ctx = this.surface.getContext('2d') + this.draw(this.props) + } + + shouldComponentUpdate(props) { + // only update if the DOM needs to be updated + if ( + this.props.outerWidth !== props.outerWidth || + this.props.outerHeight !== props.outerHeight || + this.props.isInteractive !== props.isInteractive || + this.props.theme !== props.theme + ) { + return true + } + + this.draw(props) + return false + } + + componentDidUpdate() { + this.ctx = this.surface.getContext('2d') + this.draw(this.props) + } + + getArcFromMouse = event => { + const [x, y] = getRelativeCursor(this.surface, event) + const { centerX, centerY, margin, radius, innerRadius, arcs, theme } = this.props + + return getHoveredArc( + margin.left + centerX, + margin.top + centerY, + radius, + innerRadius, + arcs, + x, + y + ) + } + + handleMouseHover = (showTooltip, hideTooltip) => event => { + if (this.props.isInteractive !== true) return + + const arc = this.getArcFromMouse(event) + if (arc) { + showTooltip( + , + event + ) + } else { + hideTooltip() + } + } + + handleMouseLeave = hideTooltip => () => { + if (this.props.isInteractive !== true) return + + hideTooltip() + } + + handleClick = event => { + const arc = this.getArcFromMouse(event) + if (arc !== undefined) this.props.onClick(arc.data, event) + } + + draw(props) { + const { + arcs, + arcGenerator, + + // layout + centerX, + centerY, + outerWidth, + outerHeight, + pixelRatio, + margin, + + // slices labels + enableSlicesLabels, + sliceLabel, + slicesLabelsSkipAngle, + slicesLabelsTextColor, + + theme, + + legends, + } = props + + this.surface.width = outerWidth * pixelRatio + this.surface.height = outerHeight * pixelRatio + + this.ctx.scale(pixelRatio, pixelRatio) + this.ctx.clearRect(0, 0, outerWidth, outerHeight) + this.ctx.save() + this.ctx.translate(margin.left, margin.top) + + arcGenerator.context(this.ctx) + + this.ctx.save() + this.ctx.translate(centerX, centerY) + + arcs.forEach(arc => { + this.ctx.beginPath() + arcGenerator(arc) + this.ctx.fillStyle = arc.color + this.ctx.fill() + }) + + if (enableSlicesLabels === true) { + const getSliceLabel = getLabelGenerator(sliceLabel) + const getSliceLabelTextColor = getInheritedColorGenerator( + slicesLabelsTextColor, + 'labels.textColor' + ) + + this.ctx.textAlign = 'center' + this.ctx.textBaseline = 'middle' + + arcs + .filter(arc => slicesLabelsSkipAngle === 0 || arc.angleDeg > slicesLabelsSkipAngle) + .forEach(arc => { + const [centroidX, centroidY] = arcGenerator.centroid(arc) + + const sliceLabel = getSliceLabel(arc.data) + const sliceLabelTextColor = getSliceLabelTextColor(arc, theme) + + this.ctx.save() + this.ctx.translate(centroidX, centroidY) + this.ctx.fillStyle = sliceLabelTextColor + this.ctx.fillText(sliceLabel, 0, 0) + this.ctx.restore() + }) + } + + this.ctx.restore() + } + + 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={this.handleMouseHover(showTooltip, hideTooltip)} + onMouseMove={this.handleMouseHover(showTooltip, hideTooltip)} + onMouseLeave={this.handleMouseLeave(hideTooltip)} + onClick={this.handleClick} + /> + )} + + ) + } +} + +export default PieCanvasRenderer diff --git a/packages/nivo-pie/src/PieLayout.js b/packages/nivo-pie/src/PieLayout.js new file mode 100644 index 000000000..80b15a5db --- /dev/null +++ b/packages/nivo-pie/src/PieLayout.js @@ -0,0 +1,193 @@ +/* + * 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 PropTypes from 'prop-types' +import { arc as d3Arc, pie as d3Pie } from 'd3-shape' +import setDisplayName from 'recompose/setDisplayName' +import compose from 'recompose/compose' +import pure from 'recompose/pure' +import defaultProps from 'recompose/defaultProps' +import withPropsOnChange from 'recompose/withPropsOnChange' +import { withColors, degreesToRadians, radiansToDegrees, computeArcBoundingBox } from '@nivo/core' + +class PieLayout extends Component { + static propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + value: PropTypes.number.isRequired, + }) + ).isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + fit: PropTypes.bool.isRequired, + sortByValue: PropTypes.bool.isRequired, + startAngle: PropTypes.number.isRequired, + endAngle: PropTypes.number.isRequired, + padAngle: PropTypes.number.isRequired, + arcs: PropTypes.array.isRequired, // computed + arcGenerator: PropTypes.func.isRequired, // computed + centerX: PropTypes.number.isRequired, // computed + centerY: PropTypes.number.isRequired, // computed + radius: PropTypes.number.isRequired, // computed + innerRadius: PropTypes.number.isRequired, // re-computed + cornerRadius: PropTypes.number.isRequired, + boundingBox: PropTypes.shape({ + points: PropTypes.array.isRequired, + box: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + }).isRequired, + ratio: PropTypes.number.isRequired, + }), // computed + children: PropTypes.func.isRequired, + } + + render() { + const { + arcs, + arcGenerator, + startAngle, + endAngle, + width, + height, + centerX, + centerY, + radius, + innerRadius, + boundingBox, + children: render, + } = this.props + + return render({ + arcs, + arcGenerator, + startAngle, + endAngle, + width, + height, + centerX, + centerY, + radius, + innerRadius, + boundingBox, + }) + } +} + +export const PieLayoutDefaultProps = { + fit: true, + sortByValue: false, + innerRadius: 0, + startAngle: 0, + endAngle: 360, + padAngle: 0, + cornerRadius: 0, +} + +export const enhance = Component => + compose( + defaultProps(PieLayoutDefaultProps), + withColors(), + withPropsOnChange( + ['width', 'height', 'innerRadius', 'startAngle', 'endAngle', 'fit', 'cornerRadius'], + ({ + width, + height, + innerRadius: _innerRadius, + startAngle, + endAngle, + fit, + cornerRadius, + }) => { + let radius = Math.min(width, height) / 2 + let innerRadius = radius * Math.min(_innerRadius, 1) + + let centerX = width / 2 + let centerY = height / 2 + + let boundingBox + if (fit === true) { + const { points, ...box } = computeArcBoundingBox( + centerX, + centerY, + radius, + startAngle - 90, + endAngle - 90 + ) + const ratio = Math.min(width / box.width, height / box.height) + + const adjustedBox = { + width: box.width * ratio, + height: box.height * ratio, + } + adjustedBox.x = (width - adjustedBox.width) / 2 + adjustedBox.y = (height - adjustedBox.height) / 2 + + centerX = (centerX - box.x) / box.width * box.width * ratio + adjustedBox.x + centerY = (centerY - box.y) / box.height * box.height * ratio + adjustedBox.y + + boundingBox = { box, ratio, points } + + radius = radius * ratio + innerRadius = innerRadius * ratio + } + + const arcGenerator = d3Arc() + .outerRadius(radius) + .innerRadius(innerRadius) + .cornerRadius(cornerRadius) + + return { + centerX, + centerY, + radius, + innerRadius, + arcGenerator, + boundingBox, + } + } + ), + withPropsOnChange( + ['sortByValue', 'padAngle', 'startAngle', 'endAngle'], + ({ sortByValue, padAngle, startAngle, endAngle }) => { + const pie = d3Pie() + .value(d => d.value) + .padAngle(degreesToRadians(padAngle)) + .startAngle(degreesToRadians(startAngle)) + .endAngle(degreesToRadians(endAngle)) + + if (sortByValue !== true) pie.sortValues(null) + + return { pie } + } + ), + withPropsOnChange(['pie', 'data'], ({ pie, data }) => ({ + arcs: pie(data).map(arc => { + const angle = Math.abs(arc.endAngle - arc.startAngle) + + return { + ...arc, + angle, + angleDeg: radiansToDegrees(angle), + } + }), + })), + withPropsOnChange(['arcs', 'getColor'], ({ arcs, getColor }) => ({ + arcs: arcs.map(arc => ({ + ...arc, + color: getColor(arc.data), + })), + })), + pure + )(Component) + +export default setDisplayName('PieLayout')(enhance(PieLayout)) diff --git a/packages/nivo-pie/src/PieRadialLabels.js b/packages/nivo-pie/src/PieRadialLabels.js index 05bff2a82..2028d27e9 100644 --- a/packages/nivo-pie/src/PieRadialLabels.js +++ b/packages/nivo-pie/src/PieRadialLabels.js @@ -6,9 +6,8 @@ * 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, { Component, Fragment } from 'react' import PropTypes from 'prop-types' -import { Motion, TransitionMotion, spring } from 'react-motion' import { midAngle, positionFromAngle } from '@nivo/core' import { line } from 'd3-shape' @@ -62,8 +61,8 @@ export default class PieRadialLabels extends Component { } = this.props return ( - - {data.filter(d => skipAngle === 0 || d.angleDegrees > skipAngle).map(d => { + + {data.filter(d => skipAngle === 0 || d.angleDeg > skipAngle).map(d => { const angle = midAngle(d) - Math.PI / 2 const positionA = positionFromAngle(angle, radius + linkOffset) const positionB = positionFromAngle( @@ -90,11 +89,11 @@ export default class PieRadialLabels extends Component { } return ( - + @@ -109,10 +108,10 @@ export default class PieRadialLabels extends Component { {label(d.data)} - + ) })} - + ) } } diff --git a/packages/nivo-pie/src/PieSlicesLabels.js b/packages/nivo-pie/src/PieSlicesLabels.js index 3f1747593..e1aefed29 100644 --- a/packages/nivo-pie/src/PieSlicesLabels.js +++ b/packages/nivo-pie/src/PieSlicesLabels.js @@ -6,10 +6,8 @@ * 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, { Component, Fragment } from 'react' import PropTypes from 'prop-types' -import merge from 'lodash/merge' -import { Motion, TransitionMotion, spring } from 'react-motion' import { midAngle, positionFromAngle } from '@nivo/core' const sliceStyle = { @@ -41,8 +39,8 @@ export default class PieSlicesLabels extends Component { const centerRadius = innerRadius + (radius - innerRadius) / 2 return ( - - {data.filter(d => skipAngle === 0 || d.angleDegrees > skipAngle).map(d => { + + {data.filter(d => skipAngle === 0 || d.angleDeg > skipAngle).map(d => { const angle = midAngle(d) - Math.PI / 2 const position = positionFromAngle(angle, centerRadius) @@ -64,7 +62,7 @@ export default class PieSlicesLabels extends Component { ) })} - + ) } } diff --git a/packages/nivo-pie/src/PieTooltip.js b/packages/nivo-pie/src/PieTooltip.js new file mode 100644 index 000000000..8a69d151d --- /dev/null +++ b/packages/nivo-pie/src/PieTooltip.js @@ -0,0 +1,43 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React from 'react' +import PropTypes from 'prop-types' +import pure from 'recompose/pure' +import { BasicTooltip } from '@nivo/core' + +const PieTooltip = ({ data, color, tooltipFormat, tooltip, theme }) => { + return ( + + ) +} + +PieTooltip.propTypes = { + data: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.number.isRequired, + }).isRequired, + color: PropTypes.string.isRequired, + tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + tooltip: PropTypes.func, + theme: PropTypes.shape({ + tooltip: PropTypes.shape({}).isRequired, + }).isRequired, +} + +export default pure(PieTooltip) diff --git a/packages/nivo-pie/src/ResponsivePieCanvas.js b/packages/nivo-pie/src/ResponsivePieCanvas.js new file mode 100644 index 000000000..b5e91f73e --- /dev/null +++ b/packages/nivo-pie/src/ResponsivePieCanvas.js @@ -0,0 +1,19 @@ +/* + * This file is part of the nivo project. + * + * Copyright 2016-present, Raphaël Benitte. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +import React from 'react' +import { ResponsiveWrapper } from '@nivo/core' +import PieCanvas from './PieCanvas' + +const ResponsivePieCanvas = props => ( + + {({ width, height }) => } + +) + +export default ResponsivePieCanvas diff --git a/packages/nivo-pie/src/enhance.js b/packages/nivo-pie/src/enhance.js index af4bf99d3..16564b21e 100644 --- a/packages/nivo-pie/src/enhance.js +++ b/packages/nivo-pie/src/enhance.js @@ -6,13 +6,43 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +import { arc as d3Arc, pie as d3Pie } from 'd3-shape' import compose from 'recompose/compose' import defaultProps from 'recompose/defaultProps' import pure from 'recompose/pure' -import { withTheme, withDimensions, withColors } from '@nivo/core' +import withPropsOnChange from 'recompose/withPropsOnChange' +import { + withTheme, + withDimensions, + withColors, + bindDefs, + degreesToRadians, + radiansToDegrees, +} from '@nivo/core' import { PieDefaultProps } from './props' export default Component => - compose(defaultProps(PieDefaultProps), withTheme(), withDimensions(), withColors(), pure)( - Component - ) + compose( + defaultProps(PieDefaultProps), + withTheme(), + withDimensions(), + /* + withPropsOnChange( + ['enhancedData', 'defs', 'fill'], + ({ enhancedData: _enhancedData, defs, fill }) => { + const enhancedData = _enhancedData.map(datum => ({ ...datum })) + const boundDefs = bindDefs(defs, enhancedData, fill) + + return { + enhancedData, + boundDefs, + legendData: enhancedData.map(datum => ({ + label: datum.label, + fill: datum.fill || datum.color, + })), + } + } + ), + */ + pure + )(Component) diff --git a/packages/nivo-pie/src/index.js b/packages/nivo-pie/src/index.js index 8145c8db3..72d376287 100644 --- a/packages/nivo-pie/src/index.js +++ b/packages/nivo-pie/src/index.js @@ -6,6 +6,9 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ +export { default as PieLayout } from './PieLayout' export { default as Pie } from './Pie' export { default as ResponsivePie } from './ResponsivePie' +export { default as PieCanvas } from './PieCanvas' +export { default as ResponsivePieCanvas } from './ResponsivePieCanvas' export * from './props' diff --git a/packages/nivo-pie/src/props.js b/packages/nivo-pie/src/props.js index 9ea319d6e..fec70ab57 100644 --- a/packages/nivo-pie/src/props.js +++ b/packages/nivo-pie/src/props.js @@ -7,9 +7,21 @@ * file that was distributed with this source code. */ import PropTypes from 'prop-types' -import { noop } from '@nivo/core' +import { noop, radiansToDegrees } from '@nivo/core' import { LegendPropShape } from '@nivo/legends' +export const arcPropType = PropTypes.shape({ + startAngle: PropTypes.number.isRequired, + endAngle: PropTypes.number.isRequired, + angle: PropTypes.number.isRequired, + angleDeg: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + data: PropTypes.shape({ + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.number.isRequired, + }).isRequired, +}) + export const PiePropTypes = { data: PropTypes.arrayOf( PropTypes.shape({ @@ -18,9 +30,13 @@ export const PiePropTypes = { }) ).isRequired, + // layout + startAngle: PropTypes.number.isRequired, + endAngle: PropTypes.number.isRequired, + fit: PropTypes.bool.isRequired, + padAngle: PropTypes.number.isRequired, sortByValue: PropTypes.bool.isRequired, innerRadius: PropTypes.number.isRequired, - padAngle: PropTypes.number.isRequired, cornerRadius: PropTypes.number.isRequired, // border @@ -58,15 +74,27 @@ export const PiePropTypes = { .isRequired, }) ).isRequired, + //boundDefs: PropTypes.array.isRequired, // computed // interactivity isInteractive: PropTypes.bool, onClick: PropTypes.func.isRequired, + + // tooltip + lockTooltip: PropTypes.bool.isRequired, tooltipFormat: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), tooltip: PropTypes.func, // legends legends: PropTypes.arrayOf(PropTypes.shape(LegendPropShape)).isRequired, + /* + legendData: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + fill: PropTypes.string.isRequired, + }) + ).isRequired, + */ } export const PieDefaultProps = { @@ -75,6 +103,11 @@ export const PieDefaultProps = { padAngle: 0, cornerRadius: 0, + // layout + startAngle: 0, + endAngle: radiansToDegrees(Math.PI * 2), + fit: true, + // border borderWidth: 0, borderColor: 'inherit:darker(1)', @@ -98,5 +131,9 @@ export const PieDefaultProps = { isInteractive: true, onClick: noop, + // tooltip + lockTooltip: true, + + // legends legends: [], } diff --git a/website/src/SiteMap.js b/website/src/SiteMap.js index 6b8f0939d..510527ffa 100644 --- a/website/src/SiteMap.js +++ b/website/src/SiteMap.js @@ -24,6 +24,7 @@ import StreamPage from './components/charts/stream/StreamPage' import Stream from './components/charts/stream/Stream' import PiePage from './components/charts/pie/PiePage' import Pie from './components/charts/pie/Pie' +import PieCanvas from './components/charts/pie/PieCanvas' import PieAPI from './components/charts/pie/PieAPI' import RadarPage from './components/charts/radar/RadarPage' import Radar from './components/charts/radar/Radar' @@ -260,6 +261,14 @@ const SITEMAP = [ exact: true, tags: ['svg', 'isomorphic'], }, + { + className: 'canvas', + path: '/canvas', + label: 'PieCanvas', + component: PieCanvas, + exact: true, + tags: ['canvas'], + }, { className: 'api', path: '/api', diff --git a/website/src/components/charts/pie/Pie.js b/website/src/components/charts/pie/Pie.js index 0b09972d2..65e84b021 100644 --- a/website/src/components/charts/pie/Pie.js +++ b/website/src/components/charts/pie/Pie.js @@ -13,7 +13,7 @@ import MediaQuery from 'react-responsive' import ChartHeader from '../../ChartHeader' import ChartTabs from '../../ChartTabs' import PieControls from './PieControls' -import { ResponsivePie, PieDefaultProps } from '@nivo/pie' +import { ResponsivePie, PieDefaultProps, PieLayout } from '@nivo/pie' import generateCode from '../../../lib/generateChartCode' import ComponentPropsDocumentation from '../../properties/ComponentPropsDocumentation' import properties from './props' @@ -29,14 +29,17 @@ export default class Pie extends Component { 'custom tooltip example': false, tooltip: null, theme: nivoTheme, + 'showcase pattern usage': true, + defs: [], + fill: [], legends: [ { anchor: 'bottom', direction: 'row', translateY: 56, itemWidth: 100, - itemHeight: 14, - symbolSize: 14, + itemHeight: 18, + symbolSize: 18, symbolShape: 'circle', }, ], @@ -127,6 +130,56 @@ export default class Pie extends Component { {...mappedSettings} onClick={this.handleNodeClick} /> + {/* + + + {(props) => { + return ( + + {props.arcs.map(arc => { + return ( + + ) + })} + + ) + }} + + + {(props) => { + console.log(props) + return ( + + {props.arcs.map(arc => { + return ( + + ) + })} + + ) + }} + + + */} { + this.setState({ settings }) + } + + handleNodeClick = (node, event) => { + alert(`${node.label}: ${node.value}\nclicked at x: ${event.clientX}, y: ${event.clientY}`) + } + + render() { + const { data, diceRoll } = this.props + const { settings } = this.state + + const mappedSettings = propsMapper(settings) + + const code = generateCode('ResponsivePieCanvas', mappedSettings, { + pkg: '@nivo/pie', + defaults: PieDefaultProps, + }) + + const header = + + const description = ( +
+

+ Generates a pie chart from an array of data, each datum must have an id and a + value property.
+ Note that margin object does not take radial labels into account,  so you + should adjust it to leave enough room for it. +

+

+ The responsive alternative of this component is  + ResponsivePie. +

+

+ This component is available in the{' '} + + nivo-api + , see{' '} + + sample + {' '} + or try it using the API client. You can also see more + example usages in{' '} + + the storybook + . +

+

+ See the dedicated guide on how to setup + legends for this component. +

+
+ ) + + return ( +
+
+ + {header} + {description} + + + + + + +
+
+ + {header} + {description} + +
+
+ ) + } +} diff --git a/website/src/components/charts/pie/PieControls.js b/website/src/components/charts/pie/PieControls.js index 48869db70..b6163bae6 100644 --- a/website/src/components/charts/pie/PieControls.js +++ b/website/src/components/charts/pie/PieControls.js @@ -14,6 +14,7 @@ import properties from './props' const groupsByScope = { Pie: getPropertiesGroupsControls(properties, 'Pie'), + PieCanvas: getPropertiesGroupsControls(properties, 'Pie'), api: getPropertiesGroupsControls(properties, 'api'), } diff --git a/website/src/components/charts/pie/PiePage.js b/website/src/components/charts/pie/PiePage.js index 14e3ed69f..81de28220 100644 --- a/website/src/components/charts/pie/PiePage.js +++ b/website/src/components/charts/pie/PiePage.js @@ -11,7 +11,7 @@ import Helmet from 'react-helmet' import { generateProgrammingLanguageStats } from '@nivo/generators' const generateData = () => { - return generateProgrammingLanguageStats(true, 7).map(d => ({ + return generateProgrammingLanguageStats(true, 32).map(d => ({ id: d.label, ...d, })) diff --git a/website/src/components/charts/pie/defaultProps.js b/website/src/components/charts/pie/defaultProps.js index c4c0f0c0b..53d848bed 100644 --- a/website/src/components/charts/pie/defaultProps.js +++ b/website/src/components/charts/pie/defaultProps.js @@ -18,13 +18,15 @@ export default { left: 80, }, + startAngle: 0, + endAngle: 360, sortByValue: false, innerRadius: 0.5, padAngle: 0.7, cornerRadius: 3, // Styling - colors: 'd320c', + colors: 'nivo', colorBy: 'id', // border diff --git a/website/src/components/charts/pie/props.js b/website/src/components/charts/pie/props.js index 815851d90..14791f4bb 100644 --- a/website/src/components/charts/pie/props.js +++ b/website/src/components/charts/pie/props.js @@ -7,9 +7,10 @@ * file that was distributed with this source code. */ import React from 'react' +import { Link } from 'react-router-dom' import dedent from 'dedent-js' import { PieDefaultProps as defaults } from '@nivo/pie' -import { marginProperties } from '../../../lib/componentProperties' +import { marginProperties, defsProperties } from '../../../lib/componentProperties' export default [ { @@ -73,6 +74,38 @@ export default [ step: 5, }, }, + { + key: 'startAngle', + scopes: ['Pie', 'PieCanvas'], + description: 'Start angle (deg.) useful to make gauges for example.', + type: '{number}', + required: false, + default: defaults.startAngle, + controlType: 'range', + controlGroup: 'Base', + controlOptions: { + unit: 'degrees', + min: -180, + max: 360, + step: 5, + }, + }, + { + key: 'endAngle', + scopes: ['Pie', 'PieCanvas'], + description: 'End angle (deg.) useful to make gauges for example.', + type: '{number}', + required: false, + default: defaults.endAngle, + controlType: 'range', + controlGroup: 'Base', + controlOptions: { + unit: 'degrees', + min: -360, + max: 360, + step: 5, + }, + }, { key: 'innerRadius', description: `Donut chart if greater than 0 (animated). Value should be between 0~1 as it's a ratio from original radius.`, @@ -120,6 +153,7 @@ export default [ }, { key: 'sortByValue', + scopes: ['Pie', 'PieCanvas'], description: `If 'true', arcs will be ordered according to their associated value.`, type: '{boolean}', required: false, @@ -165,6 +199,21 @@ export default [ ], }, }, + ...defsProperties(['Pie', 'api']), + { + key: 'showcase pattern usage', + scopes: ['Pie', 'api'], + excludeFromDoc: true, + description: ( + + You can use defs and fill properties to use patterns, see{' '} + dedicated guide for further information. + + ), + type: '{boolean}', + controlType: 'switch', + controlGroup: 'Style', + }, { key: 'borderWidth', description: 'Slices border width.', diff --git a/website/src/components/charts/pie/propsMapper.js b/website/src/components/charts/pie/propsMapper.js index 41eb2287d..e37c6585f 100644 --- a/website/src/components/charts/pie/propsMapper.js +++ b/website/src/components/charts/pie/propsMapper.js @@ -8,6 +8,7 @@ */ import React from 'react' import styled from 'styled-components' +import { patternDotsDef, patternLinesDef } from '@nivo/core' import { settingsMapper, mapInheritedColor } from '../../../lib/settings' const TooltipWrapper = styled.div` @@ -71,8 +72,42 @@ export default settingsMapper( }, } }, + defs: (value, values) => { + if (!values['showcase pattern usage']) return + + return [ + patternDotsDef('dots', { + background: 'inherit', + color: 'rgba(255, 255, 255, 0.3)', + size: 4, + padding: 1, + stagger: true, + }), + patternLinesDef('lines', { + background: 'inherit', + color: 'rgba(255, 255, 255, 0.3)', + rotation: -45, + lineWidth: 6, + spacing: 10, + }), + ] + }, + fill: (value, values) => { + if (!values['showcase pattern usage']) return + + return [ + { match: { id: 'ruby' }, id: 'dots' }, + { match: { id: 'c' }, id: 'dots' }, + { match: { id: 'go' }, id: 'dots' }, + { match: { id: 'python' }, id: 'dots' }, + { match: { id: 'scala' }, id: 'lines' }, + { match: { id: 'lisp' }, id: 'lines' }, + { match: { id: 'elixir' }, id: 'lines' }, + { match: { id: 'javascript' }, id: 'lines' }, + ] + }, }, { - exclude: ['custom tooltip example'], + exclude: ['custom tooltip example', 'showcase pattern usage'], } ) diff --git a/website/src/components/pages/Home.js b/website/src/components/pages/Home.js index 7fc854afd..e2d3db846 100644 --- a/website/src/components/pages/Home.js +++ b/website/src/components/pages/Home.js @@ -352,6 +352,7 @@ class Home extends Component { nodePadding={12} nodeBorderWidth={0} linkOpacity={0.2} + linkBlendMode="normal" linkContract={1} labelTextColor="inherit" /> diff --git a/website/src/lib/generateChartCode.js b/website/src/lib/generateChartCode.js index b1b114040..be73af81e 100644 --- a/website/src/lib/generateChartCode.js +++ b/website/src/lib/generateChartCode.js @@ -61,6 +61,7 @@ const generate = ( ``, `// make sure parent container have a defined height when using responsive component,`, `// otherwise height will be 0 and no chart will be rendered.`, + `// website examples showcase many properties, you'll often use just a few of them.`, ].join('\n') }