Skip to content

Commit

Permalink
New component - PieChart (#3470)
Browse files Browse the repository at this point in the history
* Added new component - PieChart

* Add null return for PieChart component when Svg or Path is not available
  • Loading branch information
nitzanyiz authored Jan 22, 2025
1 parent a50b5f1 commit 1ffc372
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 0 deletions.
3 changes: 3 additions & 0 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
Expand Down
6 changes: 6 additions & 0 deletions demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
101 changes: 101 additions & 0 deletions demo/src/screens/componentScreens/PieChartScreen.tsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions demo/src/screens/componentScreens/index.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
15 changes: 15 additions & 0 deletions src/components/pieChart/PieChart.api.json
Original file line number Diff line number Diff line change
@@ -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}/>"
]
}
106 changes: 106 additions & 0 deletions src/components/pieChart/PieSegment.tsx
Original file line number Diff line number Diff line change
@@ -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'
}
});
48 changes: 48 additions & 0 deletions src/components/pieChart/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit 1ffc372

Please sign in to comment.