Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New component - PieChart #3470

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
020ee68
Added new component - PieChart
nitzanyiz Dec 25, 2024
7e67b6d
Added PieChart component with more props
nitzanyiz Dec 25, 2024
946865b
Added custom hook and richer api
nitzanyiz Dec 26, 2024
f255a3c
Removed default padding
nitzanyiz Dec 26, 2024
4654d42
Rename to constants
nitzanyiz Dec 26, 2024
cfc1a85
Moved more logic into the custom hook
nitzanyiz Dec 26, 2024
b7c9e80
Fixed divider width
nitzanyiz Dec 26, 2024
c908a35
Fixed props deconstruct
nitzanyiz Dec 26, 2024
6a2d2da
Improved PieChart api and added PieChart screen
nitzanyiz Jan 1, 2025
d17152b
Changed example to fit gidelines
nitzanyiz Jan 1, 2025
622300a
Removed padding prop from PieChart
nitzanyiz Jan 2, 2025
ede9897
Removed stylesheet from piechart screen
nitzanyiz Jan 2, 2025
07e9217
Fix import path for PieChart component
nitzanyiz Jan 8, 2025
6cf616b
Rename PartialCircle component to PieSegment and update props type
nitzanyiz Jan 8, 2025
1817bf5
Rename PartialCircle component to PieSegment and update export statement
nitzanyiz Jan 8, 2025
3aeeafd
Add error handling for missing "@react-native-svg" dependency in PieS…
nitzanyiz Jan 8, 2025
b6d84cb
Moved PieChart into Charts Category
nitzanyiz Jan 8, 2025
645e547
Add support for partial pie charts and update PieChartScreen
nitzanyiz Jan 8, 2025
ec6f9a1
Enhance PieSegment and PieChart components with dividerWidth and divi…
nitzanyiz Jan 8, 2025
0775c74
rename size to diameter and added documentation
nitzanyiz Jan 8, 2025
56c30c5
completed rename size -> diameter
nitzanyiz Jan 8, 2025
5849aa3
Merge remote-tracking branch 'origin' into feat/new-component-PieChart
nitzanyiz Jan 8, 2025
94af279
Replace PartialCircle with PieSegment in PieChart component
nitzanyiz Jan 12, 2025
4bf82d2
Moved default values to props deconstruct in PieSegment.
nitzanyiz Jan 15, 2025
62d23fa
Add dividerWidth and dividerColor props to PieChart API json
nitzanyiz Jan 15, 2025
9724813
Moved react-native-svg error handling to the PieChart component
nitzanyiz Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
3 changes: 2 additions & 1 deletion demo/src/screens/MenuStructure.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export const navigationData = {
screen: 'unicorn.components.SharedTransitionScreen'
},
{title: 'Stack Aggregator', tags: 'stack aggregator', screen: 'unicorn.components.StackAggregatorScreen'},
{title: 'Marquee', tags: 'sliding text', screen: 'unicorn.components.MarqueeScreen'}
{title: 'Marquee', tags: 'sliding text', screen: 'unicorn.components.MarqueeScreen'},
{title: 'PieChart', tags: 'pie chart data', screen: 'unicorn.components.PieChartScreen'}
ethanshar marked this conversation as resolved.
Show resolved Hide resolved
]
},
Form: {
Expand Down
86 changes: 86 additions & 0 deletions demo/src/screens/componentScreens/PieChartScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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 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} size={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)}
</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
78 changes: 78 additions & 0 deletions src/components/piechart/PartialCircle.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you are using an optional dependency it's possible the user won't have it installed, what we usually do is warn the user so they'll know to install it when using this component
moreover don't render the component if it's not installed, otherwise it will throw the user an error

You can see a reference for this behavior in our Card component that depends on blurView dep

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving the validation to the main component so the user won't get an error per segment but only once

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import {StyleSheet} from 'react-native';
import View from '../view';
import {SvgPackage} from '../../optionalDependencies';
const {Svg, Path} = SvgPackage;

export type PartialCircleProps = {
percentage: number;
radius: number;
color: string;
startAngle?: number;
padding?: number;
};

const DEFAULT_DIVIDER_COLOR = '#FFFFFF';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest using our Colors.$backgroundDefault here so in dark mode it won't stay white but use the default background color

Copy link
Contributor Author

@nitzanyiz nitzanyiz Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ok to set it in the global scope of the file or should it be at the component render? my meaning is using const DEFAULT_DIVIDER_COLOR = Colors.$backgroundDefault; inside the component and not out side of it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For Branded and Editor stage purposes it's better if you'll set it directly when destructing the prop here

dividerColor = DEFAULT_DIVIDER_COLOR

const DEFAULT_DIVIDER_WIDTH = 4;
const DEFAULT_PADDING = 0;

const PartialCircle = (props: PartialCircleProps) => {
ethanshar marked this conversation as resolved.
Show resolved Hide resolved
const {percentage, radius, color, startAngle = 0, padding = DEFAULT_PADDING} = 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={DEFAULT_DIVIDER_COLOR}
strokeWidth={DEFAULT_DIVIDER_WIDTH / 2}
strokeLinecap="round"
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 PartialCircle;

const styles = StyleSheet.create({
container: {
position: 'absolute'
},
svg: {
position: 'absolute'
}
});
41 changes: 41 additions & 0 deletions src/components/piechart/index.tsx
ethanshar marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, {useMemo} from 'react';
import _ from 'lodash';
import View from '../view';
import PartialCircle, {PartialCircleProps} from './PartialCircle';

export type PieChartSegmentProps = Pick<PartialCircleProps, 'percentage' | 'color'>;

export type PieChartProps = {
segments: PieChartSegmentProps[];
size?: number;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding a defaultSegmentProps where the user can control default UI for all segments like the divider color and width

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added for the border width and color as you suggested.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, don't forget to add them also to the api.json

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah. I forgot 👌

};

const DEFAULT_SIZE = 144;

const PieChart = (props: PieChartProps) => {
const {segments, size = DEFAULT_SIZE} = props;

const total = useMemo(() => {
return _.sum(segments.map(s => s.percentage));
}, [segments]);
if (total !== 100) {
ethanshar marked this conversation as resolved.
Show resolved Hide resolved
throw new Error('PieChart segments must sum up to 100');
}

const renderPieSegments = () => {
let currentStartAngle = 0;

return segments.map((segment, index) => {
const startAngle = currentStartAngle;
currentStartAngle += (segment.percentage / 100) * 360;
return <PartialCircle key={index} {...segment} startAngle={startAngle} radius={size / 2}/>;
});
};
return (
<View width={size} height={size}>
{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 @@ -82,6 +82,7 @@ export {default as HapticService, HapticType} from './services/HapticService';
export {default as Hint, HintProps} from './components/hint';
export {default as Icon, IconProps} from './components/icon';
export {default as Image, ImageProps} from './components/image';
export {default as PieChart, PieChartSegmentProps} from './components/piechart';
ethanshar marked this conversation as resolved.
Show resolved Hide resolved
// @ts-expect-error
export {default as KeyboardAwareScrollView} from './components/KeyboardAwareScrollView/KeyboardAwareScrollView';
// @ts-expect-error
Expand Down