From 1ffc37290bafaaa56121f1a02e862e5b9f4e87cc Mon Sep 17 00:00:00 2001 From: Nitzan Yizhar Date: Wed, 22 Jan 2025 16:52:47 +0200 Subject: [PATCH] New component - PieChart (#3470) * Added new component - PieChart * Add null return for PieChart component when Svg or Path is not available --- demo/src/index.js | 3 + demo/src/screens/MenuStructure.js | 6 + .../componentScreens/PieChartScreen.tsx | 101 +++++++++++++++++ demo/src/screens/componentScreens/index.js | 1 + src/components/pieChart/PieChart.api.json | 15 +++ src/components/pieChart/PieSegment.tsx | 106 ++++++++++++++++++ src/components/pieChart/index.tsx | 48 ++++++++ src/index.ts | 1 + 8 files changed, 281 insertions(+) create mode 100644 demo/src/screens/componentScreens/PieChartScreen.tsx create mode 100644 src/components/pieChart/PieChart.api.json create mode 100644 src/components/pieChart/PieSegment.tsx create mode 100644 src/components/pieChart/index.tsx diff --git a/demo/src/index.js b/demo/src/index.js index ed47fc9fad..a90343bdc8 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -267,6 +267,9 @@ module.exports = { get Pinterest() { return require('./screens/realExamples/Pinterest').default; }, + get PieChartScreen() { + return require('./screens/componentScreens/PieChartScreen.tsx').default; + }, get ListActionsScreen() { return require('./screens/realExamples/ListActions/ListActionsScreen').default; }, diff --git a/demo/src/screens/MenuStructure.js b/demo/src/screens/MenuStructure.js index 3b2bfbe003..c755c44436 100644 --- a/demo/src/screens/MenuStructure.js +++ b/demo/src/screens/MenuStructure.js @@ -104,6 +104,12 @@ export const navigationData = { {title: 'SortableGridList', tags: 'sort grid list drag', screen: 'unicorn.components.SortableGridListScreen'} ] }, + Charts: { + title: 'Charts', + screens: [ + {title: 'PieChart', tags: 'pie chart data', screen: 'unicorn.components.PieChartScreen'} + ] + }, LayoutsAndTemplates: { title: 'Layouts & Templates', screens: [ diff --git a/demo/src/screens/componentScreens/PieChartScreen.tsx b/demo/src/screens/componentScreens/PieChartScreen.tsx new file mode 100644 index 0000000000..f057536432 --- /dev/null +++ b/demo/src/screens/componentScreens/PieChartScreen.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import {ScrollView} from 'react-native'; +import {View, PieChart, Card, Text, Badge, PieChartSegmentProps, Colors} from 'react-native-ui-lib'; + +const SEGMENTS: PieChartSegmentProps[] = [ + { + percentage: 40, + color: Colors.blue30 + }, + { + percentage: 30, + color: Colors.red30 + }, + { + percentage: 20, + color: Colors.green30 + }, + { + percentage: 10, + color: Colors.purple30 + } +]; + +const MONOCHROME_SEGMENTS: PieChartSegmentProps[] = [ + { + percentage: 40, + color: Colors.blue70 + }, + { + percentage: 30, + color: Colors.blue50 + }, + { + percentage: 20, + color: Colors.blue30 + }, + { + percentage: 10, + color: Colors.blue10 + } +]; + +const NOT_FULL_PIECHART: PieChartSegmentProps[] = [ + { + percentage: 30, + color: Colors.blue30 + }, + { + percentage: 40, + color: Colors.red30 + } +]; + +const PieChartScreen = () => { + const renderSegmentLabel = (segment: PieChartSegmentProps, text: string) => { + const {percentage, color} = segment; + return ( + + + + {text} + {percentage}% + + + ); + }; + + const renderPieChartCard = (segments: PieChartSegmentProps[]) => { + return ( + + + + + + {segments.map((segment, index) => renderSegmentLabel(segment, `Value ${index + 1}`))} + + + ); + }; + + return ( + + + + PieChart + + {renderPieChartCard(SEGMENTS)} + + Monochrome colors + + {renderPieChartCard(MONOCHROME_SEGMENTS)} + + Not Full PieChart + + {renderPieChartCard(NOT_FULL_PIECHART)} + + + ); +}; + +export default PieChartScreen; diff --git a/demo/src/screens/componentScreens/index.js b/demo/src/screens/componentScreens/index.js index 4805496025..3007fb4bfd 100644 --- a/demo/src/screens/componentScreens/index.js +++ b/demo/src/screens/componentScreens/index.js @@ -1,5 +1,6 @@ export function registerScreens(registrar) { registrar('unicorn.components.ActionSheetScreen', () => require('./ActionSheetScreen').default); + registrar('unicorn.components.PieChartScreen', () => require('./PieChartScreen').default); registrar('unicorn.components.ActionBarScreen', () => require('./ActionBarScreen').default); registrar('unicorn.components.AvatarsScreen', () => require('./AvatarsScreen').default); registrar('unicorn.components.AnimatedImageScreen', () => require('./AnimatedImageScreen').default); diff --git a/src/components/pieChart/PieChart.api.json b/src/components/pieChart/PieChart.api.json new file mode 100644 index 0000000000..de940a3dfb --- /dev/null +++ b/src/components/pieChart/PieChart.api.json @@ -0,0 +1,15 @@ +{ + "name": "PieChart", + "category": "charts", + "description": "Pie Chart", + "example": "https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/PieChartScreen.tsx", + "props": [ + {"name": "segments", "type": "PieChartSegmentProps[]", "description": "Pie chart segments array"}, + {"name": "diameter", "type": "number", "description": "Pie chart diameter"}, + {"name": "dividerWidth", "type": "number", "description": "The width of the divider between the segments"}, + {"name": "dividerColor", "type": "ColorValue", "description": "The color of the divider between the segments"} + ], + "snippet": [ + "" + ] +} diff --git a/src/components/pieChart/PieSegment.tsx b/src/components/pieChart/PieSegment.tsx new file mode 100644 index 0000000000..b6ee707228 --- /dev/null +++ b/src/components/pieChart/PieSegment.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import {ColorValue, StyleSheet} from 'react-native'; +import View from '../view'; +import {SvgPackage} from '../../optionalDependencies'; +import {Colors} from '../../style'; +const {Svg, Path} = SvgPackage; + +export type PieSegmentProps = { + /** + * The percentage of pie the segment should cover + */ + percentage: number; + /** + * The radius of the containing pie + */ + radius: number; + /** + * The color of the segment + */ + color: string; + /** + * The start angle of the segment + */ + startAngle?: number; + /** + * The padding between the segments and the container of the pie. + */ + padding?: number; + /** + * The width of the divider between the segments + */ + dividerWidth?: number; + /** + * The color of the divider between the segments + */ + dividerColor?: ColorValue; +}; + +const PieSegment = (props: PieSegmentProps) => { + const { + percentage, + radius, + color, + startAngle = 0, + padding = 0, + dividerWidth = 4, + dividerColor = Colors.$backgroundDefault + } = props; + + const actualRadius = radius - padding; + const centerXAndY = radius; + const amountToCover = (percentage / 100) * 360; + const angleFromTop = startAngle - 90; + + const startRad = (angleFromTop * Math.PI) / 180; + const endRad = startRad + (amountToCover * Math.PI) / 180; + + const startX = centerXAndY + Math.cos(startRad) * actualRadius; + const startY = centerXAndY + Math.sin(startRad) * actualRadius; + const endX = centerXAndY + Math.cos(endRad) * actualRadius; + const endY = centerXAndY + Math.sin(endRad) * actualRadius; + + const largeArcFlag = amountToCover > 180 ? 1 : 0; + const sweepFlag = 1; + + const arcPath = ` + M ${centerXAndY} ${centerXAndY} + L ${startX} ${startY} + A ${actualRadius} ${actualRadius} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY} + Z + `; + const startBorderLine = `M ${centerXAndY} ${centerXAndY} L ${startX} ${startY}`; + const endBorderLine = `M ${centerXAndY} ${centerXAndY} L ${endX} ${endY}`; + + const arc = ; + const borders = ( + + ); + const totalSize = radius * 2 + padding; + + return ( + + + {arc} + {borders} + + + ); +}; + +export default PieSegment; + +const styles = StyleSheet.create({ + container: { + position: 'absolute' + }, + svg: { + position: 'absolute' + } +}); diff --git a/src/components/pieChart/index.tsx b/src/components/pieChart/index.tsx new file mode 100644 index 0000000000..adda6fae1a --- /dev/null +++ b/src/components/pieChart/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import View from '../view'; +import PieSegment, {PieSegmentProps} from './PieSegment'; +import {SvgPackage} from '../../optionalDependencies'; +const {Svg, Path} = SvgPackage; + +export type PieChartSegmentProps = Pick; + +export type PieChartProps = { + /** + * Pie chart segments array + */ + segments: PieChartSegmentProps[]; + /** + * Pie chart diameter + */ + diameter?: number; +} & Pick; + +const DEFAULT_DIAMETER = 144; + +const PieChart = (props: PieChartProps) => { + const {segments, diameter = DEFAULT_DIAMETER, ...others} = props; + + if (!Svg || !Path) { + console.error(`RNUILib PieChart requires installing "@react-native-svg" dependency`); + return null; + } + + const renderPieSegments = () => { + let currentStartAngle = 0; + + return segments.map((segment, index) => { + const startAngle = currentStartAngle; + currentStartAngle += (segment.percentage / 100) * 360; + return ( + + ); + }); + }; + return ( + + {renderPieSegments()} + + ); +}; + +export default PieChart; diff --git a/src/index.ts b/src/index.ts index db833aea08..b2b3cc845c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -123,6 +123,7 @@ export { PickerItemsListProps, PickerMethods } from './components/picker'; +export {default as PieChart, PieChartSegmentProps} from './components/pieChart'; export {default as ProgressBar, ProgressBarProps} from './components/progressBar'; export {default as ProgressiveImage, ProgressiveImageProps} from './components/progressiveImage'; export {default as RadioButton, RadioButtonProps} from './components/radioButton';