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 ( + <View row gap-s1 marginB-s1 key={text}> + <Badge size={10} containerStyle={{justifyContent: 'center'}} backgroundColor={color}/> + <View> + <Text>{text}</Text> + <Text marginL-s1>{percentage}%</Text> + </View> + </View> + ); + }; + + const renderPieChartCard = (segments: PieChartSegmentProps[]) => { + return ( + <Card row spread paddingL-s2 paddingR-s10 paddingV-s2> + <View centerV> + <PieChart segments={segments} diameter={150}/> + </View> + <View height={'100%'} gap-s1> + {segments.map((segment, index) => renderSegmentLabel(segment, `Value ${index + 1}`))} + </View> + </Card> + ); + }; + + return ( + <ScrollView> + <View padding-page gap-s2> + <Text text50L marginB-s2> + PieChart + </Text> + {renderPieChartCard(SEGMENTS)} + <Text text50L marginV-s2> + Monochrome colors + </Text> + {renderPieChartCard(MONOCHROME_SEGMENTS)} + <Text text50L marginV-s2> + Not Full PieChart + </Text> + {renderPieChartCard(NOT_FULL_PIECHART)} + </View> + </ScrollView> + ); +}; + +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": [ + "<PieChart segments={[{percentage: 50, color: Colors.blue30}, {percentage: 30, color: Colors.red30}, {percentage: 20, color: Colors.green30}]} diameter={144}/>" + ] +} 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 = <Path d={arcPath} fill={color}/>; + const borders = ( + <Path + d={`${startBorderLine} ${endBorderLine}`} + fill="none" + stroke={dividerColor} + strokeWidth={dividerWidth / 2} + strokeLinejoin="round" + /> + ); + const totalSize = radius * 2 + padding; + + return ( + <View style={styles.container}> + <Svg width={totalSize} height={totalSize} viewBox={`0 0 ${totalSize} ${totalSize}`} style={styles.svg}> + {arc} + {borders} + </Svg> + </View> + ); +}; + +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<PieSegmentProps, 'percentage' | 'color'>; + +export type PieChartProps = { + /** + * Pie chart segments array + */ + segments: PieChartSegmentProps[]; + /** + * Pie chart diameter + */ + diameter?: number; +} & Pick<PieSegmentProps, 'dividerWidth' | 'dividerColor'>; + +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 ( + <PieSegment key={index} {...segment} {...others} startAngle={startAngle} radius={diameter / 2}/> + ); + }); + }; + return ( + <View width={diameter} height={diameter}> + {renderPieSegments()} + </View> + ); +}; + +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';