From 94ddd27b106179e0d0981ec6a693061770a95cfb Mon Sep 17 00:00:00 2001 From: Abhinandan-Kushwaha Date: Fri, 27 May 2022 05:03:45 +0530 Subject: [PATCH] Added focusOnPress to Pie and Donut charts --- docs/PieChart/PieChartProps.md | 3 + src/PieChart/colors.js | 10 + src/PieChart/index.tsx | 467 +++++-------------------------- src/PieChart/main.tsx | 492 +++++++++++++++++++++++++++++++++ 4 files changed, 573 insertions(+), 399 deletions(-) create mode 100644 src/PieChart/colors.js create mode 100644 src/PieChart/main.tsx diff --git a/docs/PieChart/PieChartProps.md b/docs/PieChart/PieChartProps.md index 69c3240..504ebf3 100644 --- a/docs/PieChart/PieChartProps.md +++ b/docs/PieChart/PieChartProps.md @@ -9,6 +9,9 @@ | showGradient | Boolean | Prop to enable radial gradient for the Pie sections | false | | gradientCenterColor | ColorValue | Gradient color at the center of the Pie chart | 'white' | | onPress | Function | Callback function called on press of Pie sections (takes item and index as parameter) | null | +| focusOnPress | Boolean | When set to true, the pressed section of the Pie chart will have a bigger radius, hence appear focused | false | +| toggleFocusOnPress | Boolean | When set to true, if the user presses an already focused pie section, it will be unfocused | true | +| extraRadiusForFocused | number | Extra radius for the focused Pie section | radius/10 | | onLabelPress | Function | Callback function called on press of a Label (takes item and index as parameter) | onPress OR null | | tiltAngle | Angle in deg | The angle by which the chart should be tilted | '55deg' for 3D charts, otherwise 0 | | shadow | Boolean | Shadow to the Pie chart, when set to true, it enhances the 3D effect | false | diff --git a/src/PieChart/colors.js b/src/PieChart/colors.js new file mode 100644 index 0000000..7f6d44b --- /dev/null +++ b/src/PieChart/colors.js @@ -0,0 +1,10 @@ +export const colors = [ + 'cyan', + 'green', + 'orange', + 'purple', + '#bbff00', + 'red', + 'blue', + 'pink', +]; diff --git a/src/PieChart/index.tsx b/src/PieChart/index.tsx index c1e9378..64b056a 100644 --- a/src/PieChart/index.tsx +++ b/src/PieChart/index.tsx @@ -1,14 +1,8 @@ -import React from 'react'; -import {ColorValue, View} from 'react-native'; -import Svg, { - Path, - Circle, - Text as SvgText, - FontStyle, - Defs, - RadialGradient, - Stop, -} from 'react-native-svg'; +import React, {useState} from 'react'; +import {View, ColorValue} from 'react-native'; +import {colors} from './colors'; +import {PieChartMain} from './main'; +import {FontStyle} from 'react-native-svg'; type propTypes = { radius?: number; @@ -47,7 +41,12 @@ type propTypes = { showGradient?: boolean; gradientCenterColor?: string; onPress?: Function; + focusOnPress?: Boolean; + toggleFocusOnPress?: Boolean; + selectedIndex?: number; + setSelectedIndex?: Function; onLabelPress?: Function; + extraRadiusForFocused?: number; }; type itemType = { value: number; @@ -71,399 +70,69 @@ type itemType = { }; export const PieChart = (props: propTypes) => { - const {data, isThreeD} = props; const radius = props.radius || 120; - const canvasWidth = radius * 2; - const canvasHeight = isThreeD ? radius * 2.3 : radius * 2; - const shadowWidth = props.shadowWidth || radius / 5; - const backgroundColor = props.backgroundColor || 'transparent'; - const shadowColor = props.shadowColor || 'lightgray'; - const semiCircle = props.semiCircle || false; - let pi = Math.PI; - const initialAngle = props.initialAngle || (semiCircle ? pi / -2 : 0); - const shadow = props.shadow || false; - const donut = props.donut || false; - const strokeWidth = props.strokeWidth || 0; - const strokeColor = - props.strokeColor || (strokeWidth ? 'gray' : 'transparent'); - const innerRadius = props.innerRadius || radius / 2.5; - const innerCircleColor = - props.innerCircleColor || props.backgroundColor || 'white'; - const innerCircleBorderWidth = - props.innerCircleBorderWidth || - (props.innerCircleBorderColor ? strokeWidth || 2 : 0); - const innerCircleBorderColor = props.innerCircleBorderColor || 'lightgray'; - const shiftInnerCenterX = props.shiftInnerCenterX || 0; - const shiftInnerCenterY = props.shiftInnerCenterY || 0; + const extraRadiusForFocused = props.extraRadiusForFocused || radius / 10; + const pi = props.semiCircle ? Math.PI / 2 : Math.PI; + const [selectedIndex, setSelectedIndex] = useState(-1); - const showText = props.showText || false; - const textColor = props.textColor || ''; - const textSize = props.textSize ? Math.min(props.textSize, radius / 5) : 16; - let tiltAngle = props.tiltAngle || '55deg'; - if ( - tiltAngle && - !isNaN(Number(tiltAngle)) && - !(tiltAngle + '').endsWith('deg') - ) { - tiltAngle += 'deg'; - } - // const tilt = props.tilt ? Math.min(props.tilt, 1) : props.isThreeD ? 0.5 : 1; - const labelsPosition = props.labelsPosition - ? props.labelsPosition - : donut || props.centerLabelComponent - ? 'outward' - : 'mid'; - - const showTextBackground = props.showTextBackground || false; - const textBackgroundColor = props.textBackgroundColor || 'white'; - const showValuesAsLabels = props.showValuesAsLabels || false; - const showGradient = props.showGradient || false; - const gradientCenterColor = props.gradientCenterColor || 'white'; - - const colors = [ - 'cyan', - 'green', - 'orange', - 'purple', - 'yellow', - 'red', - 'blue', - 'pink', - ]; - let isDataShifted = false; - let minShiftX = 0, - maxShiftX = 0, - minShiftY = 0, - maxShiftY = 0; - data.forEach((item: any) => { - total += item.value; - if (item.shiftX || item.shiftY) { - isDataShifted = true; - if (minShiftX > item.shiftX) { - minShiftX = item.shiftX; - } - if (minShiftY > item.shiftY) { - minShiftY = item.shiftY; - } - if (maxShiftX < item.shiftX) { - maxShiftX = item.shiftX; - } - if (maxShiftY < item.shiftY) { - maxShiftY = item.shiftY; + if (props.data.length <= 1 || !props.focusOnPress || selectedIndex === -1) { + return ( + + ); + } else { + let startAngle = props.initialAngle || (props.semiCircle ? -pi : 0); + // let startColor; + let total = 0; + props.data.forEach(item => { + total += item.value; + }); + if (selectedIndex !== 0) { + let start = 0; + for (let i = 0; i < selectedIndex; i++) { + start += props.data[i].value; } + startAngle += (2 * pi * start) / total; } - }); - - const horizAdjustment = maxShiftX - minShiftX; - const vertAdjustment = maxShiftY - minShiftY; - - if (semiCircle) { - pi = Math.PI / 2; - } - - let cx = radius, - cy = radius; - - let total = - data && data.length - ? data.map(item => item.value).reduce((v, a) => v + a) - : 0; - let acc = 0; - let pData = data.map(item => { - acc += item.value / total; - return acc; - }); - acc = 0; - let mData = data.map(item => { - let pAcc = acc; - acc += item.value / total; - return pAcc + (acc - pAcc) / 2; - }); - pData = [0, ...pData]; - - return ( - - - - {data.map((item, index) => { - return ( - - - - - ); - })} - - {data.length === 1 ? ( - <> - { - data[0].onPress - ? data[0].onPress - : props.onPress - ? props.onPress(data[0], 0) - : null; - }} - /> - - ) : ( - data.map((item, index) => { - // console.log('index', index); - let nextItem; - if (index === pData.length - 1) nextItem = pData[0]; - else nextItem = pData[index + 1]; - let sx = - cx * (1 + Math.sin(2 * pi * pData[index] + initialAngle)) + - (item.shiftX || 0); - let sy = - cy * (1 - Math.cos(2 * pi * pData[index] + initialAngle)) + - (item.shiftY || 0); - let ax = - cx * (1 + Math.sin(2 * pi * nextItem + initialAngle)) + - (item.shiftX || 0); - let ay = - cy * (1 - Math.cos(2 * pi * nextItem + initialAngle)) + - (item.shiftY || 0); - - // console.log('sx', sx); - // console.log('sy', sy); - // console.log('ax', ax); - // console.log('ay', ay); - return ( - total / 2 ? 1 : 0 - } 1 ${ax} ${ay} L ${cx + (item.shiftX || 0)} ${ - cy + (item.shiftY || 0) - }`} - stroke={strokeColor} - strokeWidth={strokeWidth} - fill={ - showGradient - ? `url(#grad${index})` - : item.color || colors[index % 9] - } - onPress={() => { - item.onPress - ? item.onPress - : props.onPress - ? props.onPress(item, index) - : null; - }} - /> - ); - }) - )} - - {showText && - data.map((item, index) => { - let mx = cx * (1 + Math.sin(2 * pi * mData[index] + initialAngle)); - let my = cy * (1 - Math.cos(2 * pi * mData[index] + initialAngle)); - - let midx = (mx + cx) / 2; - let midy = (my + cy) / 2; - - let x = midx, - y = midy; - - const labelPosition = item.labelPosition || labelsPosition; - - if (labelPosition === 'onBorder') { - x = mx; - y = my; - } else if (labelPosition === 'outward') { - x = (midx + mx) / 2; - y = (midy + my) / 2; - } else if (labelPosition === 'inward') { - x = (midx + cx) / 2; - y = (midy + cy) / 2; - } - - x += item.shiftX || 0; - y += item.shiftY || 0; - - if (data.length === 1) { - if (donut) { - y = - (radius - - innerRadius + - (item.textBackgroundRadius || - props.textBackgroundRadius || - item.textSize || - textSize)) / - 2; - } else { - y = cy; - } - } - - // console.log('sx', sx); - // console.log('sy', sy); - // console.log('ax', ax); - // console.log('ay', ay); - return ( - <> - {/* */} - {showTextBackground && ( - { - item.onLabelPress - ? item.onLabelPress() - : props.onLabelPress - ? props.onLabelPress(item, index) - : item.onPress - ? item.onPress() - : props.onPress - ? props.onPress(item, index) - : null; - }} - /> - )} - { - item.onLabelPress - ? item.onLabelPress() - : props.onLabelPress - ? props.onLabelPress(item, index) - : item.onPress - ? item.onPress() - : props.onPress - ? props.onPress(item, index) - : null; - }}> - {item.text || (showValuesAsLabels ? item.value + '' : '')} - - - ); - })} - - {(props.centerLabelComponent || (donut && !isDataShifted)) && ( - - - {props.centerLabelComponent ? props.centerLabelComponent() : null} - - - )} - {isThreeD && shadow && !semiCircle ? ( + return ( + - ) : null} - - ); + top: -extraRadiusForFocused, + left: -extraRadiusForFocused, + }}> + + + + + + + ); + } }; diff --git a/src/PieChart/main.tsx b/src/PieChart/main.tsx new file mode 100644 index 0000000..01b8a17 --- /dev/null +++ b/src/PieChart/main.tsx @@ -0,0 +1,492 @@ +import React from 'react'; +import {ColorValue, View} from 'react-native'; +import Svg, { + Path, + Circle, + Text as SvgText, + FontStyle, + Defs, + RadialGradient, + Stop, +} from 'react-native-svg'; +import {colors} from './colors'; + +type propTypes = { + radius?: number; + isThreeD?: Boolean; + donut?: Boolean; + innerRadius?: number; + shadow?: Boolean; + innerCircleColor?: ColorValue; + innerCircleBorderWidth?: number; + innerCircleBorderColor?: ColorValue; + shiftInnerCenterX?: number; + shiftInnerCenterY?: number; + shadowColor?: string; + shadowWidth?: number; + strokeWidth?: number; + strokeColor?: string; + backgroundColor?: string; + data: Array; + semiCircle?: Boolean; + + showText?: Boolean; + textColor?: string; + textSize?: number; + fontStyle?: FontStyle; + fontWeight?: string; + font?: string; + showTextBackground?: Boolean; + textBackgroundColor?: string; + textBackgroundRadius?: number; + showValuesAsLabels?: Boolean; + + centerLabelComponent?: Function; + tiltAngle?: string; + initialAngle?: number; + labelsPosition?: 'onBorder' | 'outward' | 'inward' | 'mid'; + showGradient?: boolean; + gradientCenterColor?: string; + onPress?: Function; + focusOnPress?: Boolean; + toggleFocusOnPress?: Boolean; + selectedIndex?: number; + setSelectedIndex?: Function; + onLabelPress?: Function; +}; +type itemType = { + value: number; + shiftX?: number; + shiftY?: number; + color?: string; + gradientCenterColor?: string; + text?: string; + textColor?: string; + textSize?: number; + fontStyle?: FontStyle; + fontWeight?: string; + font?: string; + textBackgroundColor?: string; + textBackgroundRadius?: number; + shiftTextX?: number; + shiftTextY?: number; + labelPosition?: 'onBorder' | 'outward' | 'inward' | 'mid'; + onPress?: Function; + onLabelPress?: Function; +}; + +export const PieChartMain = (props: propTypes) => { + const {data, isThreeD} = props; + const radius = props.radius || 120; + const canvasWidth = radius * 2; + const canvasHeight = isThreeD ? radius * 2.3 : radius * 2; + const shadowWidth = props.shadowWidth || radius / 5; + const backgroundColor = props.backgroundColor || 'transparent'; + const shadowColor = props.shadowColor || 'lightgray'; + const semiCircle = props.semiCircle || false; + let pi = Math.PI; + const initialAngle = props.initialAngle || (semiCircle ? pi / -2 : 0); + const shadow = props.shadow || false; + const donut = props.donut || false; + const strokeWidth = props.strokeWidth || 0; + const strokeColor = + props.strokeColor || (strokeWidth ? 'gray' : 'transparent'); + const innerRadius = props.innerRadius || radius / 2.5; + const innerCircleColor = + props.innerCircleColor || props.backgroundColor || 'white'; + const innerCircleBorderWidth = + props.innerCircleBorderWidth || + (props.innerCircleBorderColor ? strokeWidth || 2 : 0); + const innerCircleBorderColor = props.innerCircleBorderColor || 'lightgray'; + const shiftInnerCenterX = props.shiftInnerCenterX || 0; + const shiftInnerCenterY = props.shiftInnerCenterY || 0; + + const showText = props.showText || false; + const textColor = props.textColor || ''; + const textSize = props.textSize ? Math.min(props.textSize, radius / 5) : 16; + let tiltAngle = props.tiltAngle || '55deg'; + if ( + tiltAngle && + !isNaN(Number(tiltAngle)) && + !(tiltAngle + '').endsWith('deg') + ) { + tiltAngle += 'deg'; + } + // const tilt = props.tilt ? Math.min(props.tilt, 1) : props.isThreeD ? 0.5 : 1; + const labelsPosition = props.labelsPosition + ? props.labelsPosition + : donut || props.centerLabelComponent + ? 'outward' + : 'mid'; + + const showTextBackground = props.showTextBackground || false; + const textBackgroundColor = props.textBackgroundColor || 'white'; + const showValuesAsLabels = props.showValuesAsLabels || false; + const showGradient = props.showGradient || false; + const gradientCenterColor = props.gradientCenterColor || 'white'; + const toggleFocusOnPress = props.toggleFocusOnPress === false ? false : true; + + let isDataShifted = false; + let minShiftX = 0, + maxShiftX = 0, + minShiftY = 0, + maxShiftY = 0; + data.forEach((item: any) => { + total += item.value; + if (item.shiftX || item.shiftY) { + isDataShifted = true; + if (minShiftX > item.shiftX) { + minShiftX = item.shiftX; + } + if (minShiftY > item.shiftY) { + minShiftY = item.shiftY; + } + if (maxShiftX < item.shiftX) { + maxShiftX = item.shiftX; + } + if (maxShiftY < item.shiftY) { + maxShiftY = item.shiftY; + } + } + }); + + const horizAdjustment = maxShiftX - minShiftX; + const vertAdjustment = maxShiftY - minShiftY; + + if (semiCircle) { + pi = Math.PI / 2; + } + + let cx = radius, + cy = radius; + + let total = + data && data.length + ? data.map(item => item.value).reduce((v, a) => v + a) + : 0; + let acc = 0; + let pData = data.map(item => { + acc += item.value / total; + return acc; + }); + acc = 0; + let mData = data.map(item => { + let pAcc = acc; + acc += item.value / total; + return pAcc + (acc - pAcc) / 2; + }); + pData = [0, ...pData]; + + return ( + + + + {data.map((item, index) => { + return ( + + + + + ); + })} + + {data.length === 1 ? ( + <> + { + data[0].onPress + ? data[0].onPress() + : props.onPress + ? props.onPress(data[0], 0) + : null; + }} + /> + + ) : ( + data.map((item, index) => { + // console.log('index', index); + let nextItem; + if (index === pData.length - 1) nextItem = pData[0]; + else nextItem = pData[index + 1]; + let sx = + cx * (1 + Math.sin(2 * pi * pData[index] + initialAngle)) + + (item.shiftX || 0); + let sy = + cy * (1 - Math.cos(2 * pi * pData[index] + initialAngle)) + + (item.shiftY || 0); + let ax = + cx * (1 + Math.sin(2 * pi * nextItem + initialAngle)) + + (item.shiftX || 0); + let ay = + cy * (1 - Math.cos(2 * pi * nextItem + initialAngle)) + + (item.shiftY || 0); + + // console.log('sx', sx); + // console.log('sy', sy); + // console.log('ax', ax); + // console.log('ay', ay); + return ( + total / 2 ? 1 : 0 + } 1 ${ax} ${ay} L ${cx + (item.shiftX || 0)} ${ + cy + (item.shiftY || 0) + }`} + stroke={strokeColor} + strokeWidth={strokeWidth} + fill={ + showGradient + ? `url(#grad${index})` + : item.color || colors[index % 9] + } + onPress={() => { + if (item.onPress) { + item.onPress(); + } else if (props.onPress) { + props.onPress(item, index); + } + if (props.focusOnPress) { + if (props.selectedIndex === index) { + if (toggleFocusOnPress) { + props.setSelectedIndex(-1); + } + } else { + props.setSelectedIndex(index); + } + } + }} + /> + ); + }) + )} + + {showText && + data.map((item, index) => { + let mx = cx * (1 + Math.sin(2 * pi * mData[index] + initialAngle)); + let my = cy * (1 - Math.cos(2 * pi * mData[index] + initialAngle)); + + let midx = (mx + cx) / 2; + let midy = (my + cy) / 2; + + let x = midx, + y = midy; + + const labelPosition = item.labelPosition || labelsPosition; + + if (labelPosition === 'onBorder') { + x = mx; + y = my; + } else if (labelPosition === 'outward') { + x = (midx + mx) / 2; + y = (midy + my) / 2; + } else if (labelPosition === 'inward') { + x = (midx + cx) / 2; + y = (midy + cy) / 2; + } + + x += item.shiftX || 0; + y += item.shiftY || 0; + + if (data.length === 1) { + if (donut) { + y = + (radius - + innerRadius + + (item.textBackgroundRadius || + props.textBackgroundRadius || + item.textSize || + textSize)) / + 2; + } else { + y = cy; + } + } + + // console.log('sx', sx); + // console.log('sy', sy); + // console.log('ax', ax); + // console.log('ay', ay); + return ( + <> + {/* */} + {showTextBackground && ( + { + item.onLabelPress + ? item.onLabelPress() + : props.onLabelPress + ? props.onLabelPress(item, index) + : item.onPress + ? item.onPress() + : props.onPress + ? props.onPress(item, index) + : null; + if (props.focusOnPress) { + if (props.selectedIndex === index) { + if (toggleFocusOnPress) { + props.setSelectedIndex(-1); + } + } else { + props.setSelectedIndex(index); + } + } + }} + /> + )} + { + item.onLabelPress + ? item.onLabelPress() + : props.onLabelPress + ? props.onLabelPress(item, index) + : item.onPress + ? item.onPress() + : props.onPress + ? props.onPress(item, index) + : null; + if (props.focusOnPress) { + if (props.selectedIndex === index) { + if (toggleFocusOnPress) { + props.setSelectedIndex(-1); + } + } else { + props.setSelectedIndex(index); + } + } + }}> + {item.text || (showValuesAsLabels ? item.value + '' : '')} + + + ); + })} + + {(props.centerLabelComponent || (donut && !isDataShifted)) && ( + + + {props.centerLabelComponent ? props.centerLabelComponent() : null} + + + )} + {isThreeD && shadow && !semiCircle ? ( + + ) : null} + + ); +};