diff --git a/demo/components/victory-pie-demo.js b/demo/components/victory-pie-demo.js index f9c4e0284..7706af593 100644 --- a/demo/components/victory-pie-demo.js +++ b/demo/components/victory-pie-demo.js @@ -1,51 +1,10 @@ /*global window:false*/ /*eslint-disable no-magic-numbers,react/no-multi-comp*/ -import { merge, random, range } from "lodash"; -import PropTypes from "prop-types"; +import { random, range } from "lodash"; import React from "react"; -import { VictoryPie, Slice } from "../../packages/victory-pie/src/index"; +import { VictoryPie } from "../../packages/victory-pie/src/index"; import { VictoryTooltip } from "../../packages/victory-tooltip/src/index"; -import { - VictoryContainer, VictoryTheme, VictoryLabel -} from "../../packages/victory-core/src/index"; - -class BorderLabelSlice extends React.Component { - static propTypes = { - ...Slice.propTypes, - index: PropTypes.number - }; - - renderSlice(props) { - return ; - } - - renderLabel(props) { - const { pathFunction, datum, slice, index, origin } = props; - const path = pathFunction({ ...slice, endAngle: slice.startAngle }); - const pathId = `textPath-path-${index}`; - return ( - - - - - {datum.label || datum.xName || datum.x} - - - - ); - } - - render() { - const { index } = this.props; - - return ( - - {this.renderSlice(this.props)} - {this.renderLabel(this.props)} - - ); - } -} +import { VictoryTheme } from "../../packages/victory-core/src/index"; export default class App extends React.Component { constructor(props) { @@ -145,6 +104,14 @@ export default class App extends React.Component { labelRadius={60} padding={{ bottom: 50, left: 50, right: 10 }} width={400} height={200} + radius={(d) => d.radius} + data={[ + { x: 1, y: 1, radius: 40 }, + { x: 2, y: 3, radius: 50 }, + { x: 3, y: 5, radius: 70 }, + { x: 4, y: 2, radius: 80 }, + { x: 5, y: 3, radius: 60 } + ]} /> } /> } - labels={() => "click me!"} events={[{ target: "data", eventHandlers: { - onClick: () => { - return [ - { - mutation: (props) => { - return { - style: merge({}, props.style, { fill: "#F50057" }) - }; - } - }, { - target: "labels", - eventKey: [0, 2, 4], - mutation: () => { - return { text: "Nice." }; - } - } - ]; - } + onMouseOver: () => ({ + mutation: (props) => ({ + radius: 135, + sliceStartAngle: props.slice.startAngle + 0.05, + sliceEndAngle: props.slice.endAngle - 0.05 + }) + }), + onMouseOut: () => ({ + mutation: () => null + }) } }]} /> @@ -266,7 +217,6 @@ export default class App extends React.Component { innerRadius={100} animate={{ duration: 2000 }} colorScale={this.state.colorScale} - dataComponent={} /> { - return degrees * (Math.PI / 180); -}; - const checkForValidText = (text) => { if (text === undefined || text === null) { return text; @@ -24,7 +20,7 @@ const getColor = (style, colors, index) => { }; const getRadius = (props, padding) => { - if (props.radius) { + if (typeof props.radius === "number") { return props.radius; } return Math.min( @@ -45,9 +41,9 @@ const getOrigin = (props, padding) => { const getSlices = (props, data) => { const layoutFunction = d3Shape.pie() .sort(null) - .startAngle(degreesToRadians(props.startAngle)) - .endAngle(degreesToRadians(props.endAngle)) - .padAngle(degreesToRadians(props.padAngle)) + .startAngle(Helpers.degreesToRadians(props.startAngle)) + .endAngle(Helpers.degreesToRadians(props.endAngle)) + .padAngle(Helpers.degreesToRadians(props.padAngle)) .value((datum) => { return datum._y; }); return layoutFunction(data); }; @@ -58,15 +54,11 @@ const getCalculatedValues = (props) => { const style = Helpers.getStyles(props.style, styleObject, "auto", "100%"); const colors = Array.isArray(colorScale) ? colorScale : Style.getColorScale(colorScale); const padding = Helpers.getPadding(props); - const radius = getRadius(props, padding); + const defaultRadius = getRadius(props, padding); const origin = getOrigin(props, padding); const data = Data.getData(props); const slices = getSlices(props, data); - const pathFunction = d3Shape.arc() - .cornerRadius(props.cornerRadius) - .outerRadius(radius) - .innerRadius(props.innerRadius); - return { style, colors, padding, radius, data, slices, pathFunction, origin }; + return { style, colors, padding, defaultRadius, data, slices, origin }; }; const getSliceStyle = (index, calculatedValues) => { @@ -138,12 +130,12 @@ const getVerticalAnchor = (orientation) => { const getLabelProps = (props, dataProps, calculatedValues) => { const { index, datum, data, slice } = dataProps; - const { style, radius, origin } = calculatedValues; + const { style, defaultRadius, origin } = calculatedValues; const labelStyle = Helpers.evaluateStyle( assign({ padding: 0 }, style.labels), datum, props.active ); const labelRadius = Helpers.evaluateProp(props.labelRadius, datum); - const labelArc = getLabelArc(radius, labelRadius, labelStyle); + const labelArc = getLabelArc(defaultRadius, labelRadius, labelStyle); const position = getLabelPosition(labelArc, slice, props.labelPosition); const orientation = getLabelOrientation(slice); return { @@ -161,17 +153,27 @@ const getLabelProps = (props, dataProps, calculatedValues) => { export const getBaseProps = (props, fallbackProps) => { props = Helpers.modifyProps(props, fallbackProps, "pie"); const calculatedValues = getCalculatedValues(props); - const { slices, style, pathFunction, data, origin } = calculatedValues; - const { labels, events, sharedEvents, height, width, standalone, name } = props; + const { slices, style, data, origin, defaultRadius } = calculatedValues; + const { + labels, events, sharedEvents, height, width, standalone, name, + innerRadius, cornerRadius, padAngle + } = props; + const radius = props.radius || defaultRadius; const initialChildProps = { - parent: { standalone, height, width, slices, pathFunction, name, style: style.parent } + parent: { standalone, height, width, slices, name, style: style.parent } }; return slices.reduce((childProps, slice, index) => { - const datum = data[index]; + const datum = defaults( + {}, data[index], { + startAngle: Helpers.radiansToDegrees(slice.startAngle), + endAngle: Helpers.radiansToDegrees(slice.endAngle), + padAngle: Helpers.radiansToDegrees(slice.padAngle) + } + ); const eventKey = datum.eventKey || index; const dataProps = { - index, slice, pathFunction, datum, data, origin, + index, slice, datum, data, origin, innerRadius, radius, cornerRadius, padAngle, style: getSliceStyle(index, calculatedValues) }; childProps[eventKey] = { diff --git a/packages/victory-pie/src/slice.js b/packages/victory-pie/src/slice.js index 544665861..79c4acd3c 100644 --- a/packages/victory-pie/src/slice.js +++ b/packages/victory-pie/src/slice.js @@ -1,32 +1,65 @@ import React from "react"; import PropTypes from "prop-types"; import { Helpers, CommonProps, Path } from "victory-core"; -import { isFunction } from "lodash"; +import { defaults, isFunction } from "lodash"; +import * as d3Shape from "d3-shape"; + export default class Slice extends React.Component { static propTypes = { ...CommonProps.primitiveProps, + cornerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), datum: PropTypes.object, + innerRadius: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + padAngle: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), pathComponent: PropTypes.element, pathFunction: PropTypes.func, - slice: PropTypes.object + radius: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + slice: PropTypes.object, + sliceEndAngle: PropTypes.oneOfType([PropTypes.number, PropTypes.func]), + sliceStartAngle: PropTypes.oneOfType([PropTypes.number, PropTypes.func]) + }; static defaultProps = { pathComponent: }; + getPath(props) { + const { datum, active, slice } = props; + if (isFunction(props.pathFunction)) { + return props.pathFunction(slice); + } + const cornerRadius = Helpers.evaluateProp(props.cornerRadius, datum, active); + const innerRadius = Helpers.evaluateProp(props.innerRadius, datum, active); + const radius = Helpers.evaluateProp(props.radius, datum, active); + const padAngle = Helpers.degreesToRadians( + Helpers.evaluateProp(props.padAngle, datum, active) + ); + const startAngle = Helpers.degreesToRadians( + Helpers.evaluateProp(props.sliceStartAngle, datum, active) + ); + const endAngle = Helpers.degreesToRadians( + Helpers.evaluateProp(props.sliceEndAngle, datum, active) + ); + const pathFunction = d3Shape.arc() + .cornerRadius(cornerRadius) + .outerRadius(radius) + .innerRadius(innerRadius); + return pathFunction(defaults({ startAngle, endAngle, padAngle }, slice)); + } + render() { const { - datum, slice, active, role, shapeRendering, className, - origin, events, pathComponent, pathFunction, style, clipPath + datum, active, role, shapeRendering, className, + origin, events, pathComponent, style, clipPath } = this.props; const defaultTransform = origin ? `translate(${origin.x}, ${origin.y})` : undefined; const transform = this.props.transform || defaultTransform; return React.cloneElement(pathComponent, { className, role, shapeRendering, events, transform, clipPath, style: Helpers.evaluateStyle(style, datum, active), - d: isFunction(pathFunction) ? pathFunction(slice) : undefined + d: this.getPath(this.props) }); } } diff --git a/packages/victory-pie/src/victory-pie.js b/packages/victory-pie/src/victory-pie.js index 712aa8ec7..61d64af1d 100644 --- a/packages/victory-pie/src/victory-pie.js +++ b/packages/victory-pie/src/victory-pie.js @@ -49,7 +49,9 @@ class VictoryPie extends React.Component { onEnter: { duration: 500, before: () => ({ _y: 0, label: " " }), - after: (datum) => ({ y_: datum._y, label: datum.label }) + after: (datum) => ({ + y_: datum._y, label: datum.label + }) } }; @@ -62,7 +64,7 @@ class VictoryPie extends React.Component { ]) ]), containerComponent: PropTypes.element, - cornerRadius: CustomPropTypes.nonNegative, + cornerRadius: PropTypes.oneOfType([ CustomPropTypes.nonNegative, PropTypes.func ]), data: PropTypes.array, dataComponent: PropTypes.element, endAngle: PropTypes.number, @@ -99,7 +101,7 @@ class VictoryPie extends React.Component { })), groupComponent: PropTypes.element, height: CustomPropTypes.nonNegative, - innerRadius: CustomPropTypes.nonNegative, + innerRadius: PropTypes.oneOfType([ CustomPropTypes.nonNegative, PropTypes.func ]), labelComponent: PropTypes.element, labelPosition: PropTypes.oneOf(["startAngle", "centroid", "endAngle"]), labelRadius: PropTypes.oneOfType([ CustomPropTypes.nonNegative, PropTypes.func ]), @@ -109,7 +111,7 @@ class VictoryPie extends React.Component { x: CustomPropTypes.nonNegative, y: CustomPropTypes.nonNegative }), - padAngle: CustomPropTypes.nonNegative, + padAngle: PropTypes.oneOfType([ CustomPropTypes.nonNegative, PropTypes.func ]), padding: PropTypes.oneOfType([ PropTypes.number, PropTypes.shape({ @@ -117,7 +119,7 @@ class VictoryPie extends React.Component { left: PropTypes.number, right: PropTypes.number }) ]), - radius: CustomPropTypes.nonNegative, + radius: PropTypes.oneOfType([ CustomPropTypes.nonNegative, PropTypes.func ]), sharedEvents: PropTypes.shape({ events: PropTypes.array, getEventState: PropTypes.func diff --git a/stories/victory-pie.js b/stories/victory-pie.js index b3ed91ccd..86bf31e55 100644 --- a/stories/victory-pie.js +++ b/stories/victory-pie.js @@ -2,7 +2,7 @@ import React from "react"; import { storiesOf } from "@storybook/react"; import { action } from "@storybook/addon-actions"; -import { VictoryPie } from "../packages/victory-pie/src/index"; +import { VictoryPie, Slice } from "../packages/victory-pie/src/index"; storiesOf("VictoryPie", module) .addDecorator((story) => ( @@ -129,6 +129,63 @@ storiesOf("VictoryPie", module) ]} /> )) + .add("with functional radius", () => ( + d.y + 100} + labelRadius={(d) => d.y + 50} + style={{ + labels: { fill: "white" } + }} + data={[ + { x: "Cat", y: 62 }, + { x: "Dog", y: 91 }, + { x: "Fish", y: 55 }, + { x: "Bird", y: 55 } + ]} + /> + )) + .add("with functional innerRadius", () => ( + d.y} + data={[ + { x: "Cat", y: 62 }, + { x: "Dog", y: 91 }, + { x: "Fish", y: 55 }, + { x: "Bird", y: 55 } + ]} + /> + )) + .add("with functional cornerRadius", () => ( + d.y > 70 ? 10 : 0 } + innerRadius={100} + data={[ + { x: "Cat", y: 62 }, + { x: "Dog", y: 91 }, + { x: "Fish", y: 55 }, + { x: "Bird", y: 55 } + ]} + /> + )) + .add("with sliceStartAngle and sliceEndAngle", () => ( + d.endAngle} + /> + } + labels={() => " "} + radius={(d) => d.radius} + innerRadius={(d) => d.innerRadius} + data={[ + { x: "Cat", y: 62, innerRadius: 0, radius: 30 }, + { x: "Dog", y: 91, innerRadius: 35, radius: 65 }, + { x: "Fish", y: 55, innerRadius: 70, radius: 100 }, + { x: "Bird", y: 55, innerRadius: 105, radius: 135, endAngle: 360 } + ]} + /> + )) .add("events: click handler", () => (
{ expect(clickHandler).called; // the first argument is the standard evt object expect(clickHandler.args[0][1]) - .to.include.keys("slices", "pathFunction", "width", "height", "style"); + .to.include.keys("slices", "width", "height", "style"); }); it("attaches an event to data", () => {