diff --git a/demo/js/components/victory-pie-demo.js b/demo/js/components/victory-pie-demo.js index 5b0a49ed7..614e46e34 100644 --- a/demo/js/components/victory-pie-demo.js +++ b/demo/js/components/victory-pie-demo.js @@ -3,7 +3,7 @@ import { random, range } from "lodash"; import React from "react"; import { VictoryPie } from "victory-pie"; import { VictoryTooltip } from "victory-tooltip"; -import { VictoryTheme } from "victory-core"; +import { VictoryTheme, LineSegment } from "victory-core"; export default class App extends React.Component { constructor(props) { @@ -228,7 +228,7 @@ export default class App extends React.Component { <VictoryPie style={{ ...this.state.style, labels: { fontSize: 0 } }} data={this.state.data} - innerRadius={100} + innerRadius={90} animate={{ duration: 2000 }} colorScale={this.state.colorScale} /> @@ -317,6 +317,20 @@ export default class App extends React.Component { { x: 8, y: 1, l: 315 }, ]} /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator + /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator={<LineSegment style={{opacity:"1",strokeWidth:"1px",stroke: "red"}}/>} + /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator={<LineSegment style={{opacity:"1",strokeWidth:"1px",strokeDasharray: "1",stroke: "red"}}/>} + labelIndicatorInnerOffset={35} + labelIndicatorOuterOffset={4} + /> </div> </div> ); diff --git a/demo/ts/components/victory-pie-demo.tsx b/demo/ts/components/victory-pie-demo.tsx index a6bae53a4..eb66307f2 100644 --- a/demo/ts/components/victory-pie-demo.tsx +++ b/demo/ts/components/victory-pie-demo.tsx @@ -2,7 +2,7 @@ import React from "react"; import { random, range } from "lodash"; import { VictoryPie } from "victory-pie"; import { VictoryTooltip } from "victory-tooltip"; -import { VictoryTheme } from "victory-core"; +import { VictoryTheme, LineSegment } from "victory-core"; interface VictoryPieDemoState { data: { @@ -306,6 +306,20 @@ export default class VictoryPieDemo extends React.Component< animate={{ duration: 2000 }} innerRadius={140} /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator + /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator={<LineSegment style={{opacity:"1",strokeWidth:"1px",stroke: "red"}}/>} + /> + <VictoryPie + style={{ parent: parentStyle }} + labelIndicator={<LineSegment style={{opacity:"1",strokeWidth:"1px",strokeDasharray: "1",stroke: "red"}}/>} + labelIndicatorInnerOffset={45} + labelIndicatorOuterOffset={15} + /> </div> </div> ); diff --git a/docs/src/content/docs/victory-pie.md b/docs/src/content/docs/victory-pie.md index 7a1d75220..769b14198 100644 --- a/docs/src/content/docs/victory-pie.md +++ b/docs/src/content/docs/victory-pie.md @@ -292,7 +292,7 @@ The `labelPosition` prop specifies the position of each label relative to its co `type: number || function` -The `labelRadius` prop defines the radius of the arc that will be used for positioning each slice label. If this prop is not set, the label radius will default to the radius of the pie + label padding. If this prop is given as a function, it will be evaluated for each label `VictoryPie` renders, and will be evaluated with the props that correspond to that label, as well as the radius and innerRadius of the corresponding slice. +The `labelRadius` prop defines the radius of the arc that will be used for positioning each slice label. If this prop is not set, the label radius will default to the radius of the pie + label padding. If this prop is given as a function, it will be evaluated for each label `VictoryPie` renders, and will be evaluated with the props that correspond to that label, as well as the radius and innerRadius of the corresponding slice. If `labelIndicator` prop is being used, passed `labelRadius`(> radius) is used to calculate the co-ordinates of the outer indicator line. If no specific value for labelRadius is passed , default values will be considered. The outer indicator line length is the difference between `labelRadius` and `labelIndicatorOuterOffset`. ```playground <VictoryPie @@ -519,6 +519,61 @@ See the [Data Accessors Guide][] for more detail on formatting and processing da y={(d) => d.value + d.error} ``` +## labelIndicator + +`type: boolean || element` + +The `labelIndicator` prop defines the label indicator line between labels and the pie chart. If this prop is used as a boolean,then the default indicator will be displayed. To customize or pass your own styling `<LineSegment/>` can be passed to labelIndicator. LabelIndicator is functional only when labelPosition = "centroid". To adjust the labelIndicator length, `labelIndicatorInnerOffset` and `labelIndicatorOuterOffset` props can be used alongside labelIndicator. + +```playground +<VictoryPie + data={sampleData} + labelIndicator + style={{ labels: { fill: "white", fontSize: 20, fontWeight: "bold" } }} +/> +<VictoryPie + data={sampleData} + labelIndicator={<LineSegment style = {{stroke:"red", strokeDasharray:1,fill: "none",}}/>} + style={{ labels: { fill: "white", fontSize: 20, fontWeight: "bold" } }} +/> +<VictoryPie + data={sampleData} + labelIndicator={<LineSegment style = {{stroke:"red", strokeDasharray:1,fill: "none",}}/>} + style={{ labels: { fill: "white", fontSize: 20, fontWeight: "bold" } }} + labelIndicatorInnerOffset={10} + labelIndicatorOuterOffset={15} +/> +``` +## labelIndicatorInnerOffset + +`type: number` + +The `labelIndicatorInnerOffset` prop defines the offset by which the indicator length inside pie chart is being drawn. Higher the number shorter the length. + +```playground +<VictoryPie + data={sampleData} + labelIndicator + style={{ labels: { fill: "white", fontSize: 20, fontWeight: "bold" } }} + labelIndicatorInnerOffset={10} +/> +``` + +## labelIndicatorOuterOffset + +`type: number` + +The `labelIndicatorOuterOffset` prop defines the offset by which the indicator length outside the pie chart is being drawn. Higher the number shorter the length. + +```playground +<VictoryPie + data={sampleData} + labelIndicator + style={{ labels: { fill: "white", fontSize: 20, fontWeight: "bold" } }} + labelIndicatorOuterOffset={10} +/> +``` + [animations guide]: /guides/animations [data accessors guide]: /guides/data-accessors [custom components guide]: /guides/custom-components diff --git a/packages/victory-pie/src/helper-methods.js b/packages/victory-pie/src/helper-methods.js index 91f28fdda..9e107460d 100644 --- a/packages/victory-pie/src/helper-methods.js +++ b/packages/victory-pie/src/helper-methods.js @@ -100,10 +100,13 @@ const getLabelText = (props, datum, index) => { return checkForValidText(text); }; -const getLabelArc = (radius, labelRadius, style) => { +const getLabelArc = (labelRadius) => { + return d3Shape.arc().outerRadius(labelRadius).innerRadius(labelRadius); +}; + +const getCalculatedLabelRadius = (radius, labelRadius, style) => { const padding = (style && style.padding) || 0; - const arcRadius = labelRadius || radius + padding; - return d3Shape.arc().outerRadius(arcRadius).innerRadius(arcRadius); + return labelRadius || radius + padding; }; const getLabelPosition = (arc, slice, position) => { @@ -195,7 +198,12 @@ const getLabelProps = (text, dataProps, calculatedValues) => { labelStyle, assign({ labelRadius, text }, dataProps), ); - const labelArc = getLabelArc(defaultRadius, labelRadius, evaluatedStyle); + const calculatedLabelRadius = getCalculatedLabelRadius( + defaultRadius, + labelRadius, + evaluatedStyle, + ); + const labelArc = getLabelArc(calculatedLabelRadius); const position = getLabelPosition(labelArc, slice, labelPosition); const baseAngle = getBaseLabelAngle(slice, labelPosition, labelStyle); const labelAngle = getLabelAngle(baseAngle, labelPlacement); @@ -219,6 +227,7 @@ const getLabelProps = (text, dataProps, calculatedValues) => { textAnchor, verticalAnchor, angle: labelAngle, + calculatedLabelRadius, }; if (!Helpers.isTooltip(labelComponent)) { @@ -228,6 +237,57 @@ const getLabelProps = (text, dataProps, calculatedValues) => { return defaults({}, labelProps, Helpers.omit(tooltipTheme, ["style"])); }; +export const getXOffsetMultiplayerByAngle = (angle) => + Math.cos(angle - Helpers.degreesToRadians(90)); +export const getYOffsetMultiplayerByAngle = (angle) => + Math.sin(angle - Helpers.degreesToRadians(90)); +export const getXOffset = (offset, angle) => + offset * getXOffsetMultiplayerByAngle(angle); +export const getYOffset = (offset, angle) => + offset * getYOffsetMultiplayerByAngle(angle); +export const getAverage = (array) => + array.reduce((acc, cur) => acc + cur, 0) / array.length; + +export const getLabelIndicatorPropsForLineSegment = ( + props, + calculatedValues, + labelProps, +) => { + const { + innerRadius, + radius, + slice: { startAngle, endAngle }, + labelIndicatorInnerOffset, + labelIndicatorOuterOffset, + index, + } = props; + + const { height, width } = calculatedValues; + const { calculatedLabelRadius } = labelProps; + // calculation + const middleRadius = getAverage([innerRadius, radius]); + const midAngle = getAverage([endAngle, startAngle]); + const centerX = width / 2; + const centerY = height / 2; + const innerOffset = middleRadius + labelIndicatorInnerOffset; + const outerOffset = calculatedLabelRadius - labelIndicatorOuterOffset; + + const x1 = centerX + getXOffset(innerOffset, midAngle); + const y1 = centerY + getYOffset(innerOffset, midAngle); + + const x2 = centerX + getXOffset(outerOffset, midAngle); + const y2 = centerY + getYOffset(outerOffset, midAngle); + + const labelIndicatorProps = { + x1, + y1, + x2, + y2, + index, + }; + return defaults({}, labelIndicatorProps); +}; + export const getBaseProps = (initialProps, fallbackProps) => { const props = Helpers.modifyProps(initialProps, fallbackProps, "pie"); const calculatedValues = getCalculatedValues(props); @@ -248,6 +308,7 @@ export const getBaseProps = (initialProps, fallbackProps) => { cornerRadius, padAngle, disableInlineStyles, + labelIndicator, } = calculatedValues; const radius = props.radius || defaultRadius; const initialChildProps = { @@ -288,6 +349,17 @@ export const getBaseProps = (initialProps, fallbackProps) => { assign({}, props, dataProps), calculatedValues, ); + if (labelIndicator) { + const labelProps = childProps[eventKey].labels; + if (labelProps.calculatedLabelRadius > radius) { + childProps[eventKey].labelIndicators = + getLabelIndicatorPropsForLineSegment( + assign({}, props, dataProps), + calculatedValues, + labelProps, + ); + } + } } return childProps; }, initialChildProps); diff --git a/packages/victory-pie/src/index.d.ts b/packages/victory-pie/src/index.d.ts index ba4724d8d..cf1e1bbfc 100644 --- a/packages/victory-pie/src/index.d.ts +++ b/packages/victory-pie/src/index.d.ts @@ -64,6 +64,9 @@ export interface VictoryPieProps >[]; eventKey?: StringOrNumberOrCallback; innerRadius?: NumberOrCallback; + labelIndicator?: boolean | React.ReactElement; + labelIndicatorInnerOffset: number; + labelIndicatorOuterOffset: number; labelPlacement?: | VictorySliceLabelPlacementType | ((props: SliceProps) => VictorySliceLabelPlacementType); diff --git a/packages/victory-pie/src/victory-pie.js b/packages/victory-pie/src/victory-pie.js index 57ddb921f..6ac9391dd 100644 --- a/packages/victory-pie/src/victory-pie.js +++ b/packages/victory-pie/src/victory-pie.js @@ -5,6 +5,7 @@ import { addEvents, Helpers, Data, + LineSegment, PropTypes as CustomPropTypes, VictoryContainer, VictoryLabel, @@ -12,11 +13,13 @@ import { UserProps, } from "victory-core"; import Slice from "./slice"; +import { isNil } from "lodash"; import { getBaseProps } from "./helper-methods"; const fallbackProps = { endAngle: 360, height: 400, + radius: 100, innerRadius: 0, cornerRadius: 0, padAngle: 0, @@ -35,6 +38,12 @@ const fallbackProps = { "#000000", ], labelPosition: "centroid", + labelIndicatorInnerOffset: 15, + labelIndicatorOuterOffset: 5, +}; + +const datumHasXandY = (datum) => { + return !isNil(datum._x) && !isNil(datum._y); }; class VictoryPie extends React.Component { @@ -141,6 +150,10 @@ class VictoryPie extends React.Component { PropTypes.func, ]), labelComponent: PropTypes.element, + labelIndicator: PropTypes.oneOfType([PropTypes.element, PropTypes.bool]), + labelIndicatorInnerOffset: PropTypes.number, + labelIndicatorMiddleOffset: PropTypes.number, + labelIndicatorOuterOffset: PropTypes.number, labelPlacement: PropTypes.oneOfType([ PropTypes.func, PropTypes.oneOf(["parallel", "perpendicular", "vertical"]), @@ -240,6 +253,7 @@ class VictoryPie extends React.Component { "labelComponent", "groupComponent", "containerComponent", + "labelIndicatorComponent", ]; // Overridden in victory-native @@ -247,6 +261,67 @@ class VictoryPie extends React.Component { return Boolean(this.props.animate); } + renderComponents(props, shouldRenderDatum = datumHasXandY) { + const { + dataComponent, + labelComponent, + groupComponent, + labelIndicator, + labelPosition, + } = props; + const showIndicator = labelIndicator && labelPosition === "centroid"; + + let labelIndicatorComponents = null; + + const dataComponents = this.dataKeys.reduce( + (validDataComponents, _dataKey, index) => { + const dataProps = this.getComponentProps(dataComponent, "data", index); + if (shouldRenderDatum(dataProps.datum)) { + validDataComponents.push( + React.cloneElement(dataComponent, dataProps), + ); + } + return validDataComponents; + }, + [], + ); + + const labelComponents = this.dataKeys + .map((_dataKey, index) => { + const labelProps = this.getComponentProps( + labelComponent, + "labels", + index, + ); + if (labelProps.text !== undefined && labelProps.text !== null) { + return React.cloneElement(labelComponent, labelProps); + } + return undefined; + }) + .filter(Boolean); + + if (showIndicator) { + let labelIndicatorComponent = <LineSegment />; + if (typeof labelIndicator === "object") { + // pass user provided react component + labelIndicatorComponent = labelIndicator; + } + + labelIndicatorComponents = this.dataKeys.map((_dataKey, index) => { + const labelIndicatorProps = this.getComponentProps( + labelIndicatorComponent, + "labelIndicators", + index, + ); + return React.cloneElement(labelIndicatorComponent, labelIndicatorProps); + }); + } + const children = showIndicator + ? [...dataComponents, ...labelComponents, ...labelIndicatorComponents] + : [...dataComponents, ...labelComponents]; + return this.renderContainer(groupComponent, children); + } + render() { const { animationWhitelist, role } = VictoryPie; const props = Helpers.modifyProps(this.props, fallbackProps, role); @@ -255,7 +330,7 @@ class VictoryPie extends React.Component { return this.animateComponent(props, animationWhitelist); } - const children = this.renderData(props); + const children = this.renderComponents(props); const component = props.standalone ? this.renderContainer(props.containerComponent, children) diff --git a/stories/victory-pie.stories.js b/stories/victory-pie.stories.js index c119acce8..c18251b1c 100644 --- a/stories/victory-pie.stories.js +++ b/stories/victory-pie.stories.js @@ -4,7 +4,7 @@ import React from "react"; import { VictoryPie, Slice } from "victory-pie"; import { VictoryTooltip } from "victory-tooltip"; -import { VictoryTheme, Helpers } from "victory-core"; +import { LineSegment, VictoryTheme, Helpers } from "victory-core"; import { fromJS } from "immutable"; import styled from "styled-components"; @@ -586,3 +586,49 @@ export const DisableInlineStyles = () => { </div> ); }; + +export const LabelIndicator = () => { + return ( + <div style={containerStyle}> + <VictoryPie style={parentStyle} labelIndicator /> + + <VictoryPie + style={parentStyle} + labelIndicator + radius={90} + labelRadius={100} + labelIndicatorInnerOffset={25} + labelIndicatorOuterOffset={4} + /> + <VictoryPie style={parentStyle} innerRadius={50} labelIndicator /> + <VictoryPie + style={parentStyle} + innerRadius={50} + labelIndicator + labelIndicatorInnerOffset={25} + labelIndicatorOuterOffset={10} + /> + <VictoryPie + style={parentStyle} + innerRadius={50} + labelIndicator={<LineSegment />} + /> + <VictoryPie + style={parentStyle} + labelRadius={90} + innerRadius={50} + radius={75} + labelIndicator={ + <LineSegment + style={{ + stroke: "red", + strokeDasharray: 1, + strokeWidth: 2, + fill: "none", + }} + /> + } + /> + </div> + ); +};