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

Use recharts instead of react native svg charts #35

Merged
merged 7 commits into from
Jan 21, 2025
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"dependencies": {
"@react-navigation/native": "^6.0.10",
"@react-navigation/native-stack": "^6.6.2",
"d3-shape": "^3.1.0",
"d3-shape": "^3.2.0",
"deepmerge": "^4.2.2",
"expo": "~45.0.0",
"expo-status-bar": "~1.3.0",
Expand All @@ -31,13 +31,12 @@
"react-native-safe-area-context": "4.2.4",
"react-native-screens": "~3.11.1",
"react-native-select-dropdown": "1.7.0",
"react-native-svg": "12.3.0",
"react-native-svg-charts": "^5.4.0",
"react-native-web": "0.17.7"
"react-native-web": "0.17.7",
"recharts": "^2.15.0"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"gh-pages": "^4.0.0"
},
"private": true
}
}
5 changes: 4 additions & 1 deletion src/HighLevelAssessment.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ const styles = StyleSheet.create({
justifyContent: 'center',
},
activityPie: {
position: 'absolute',
position: 'relative',
width: '90%',
height: '90%',
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
flex: 1,
minHeight: 420,
}
})
4 changes: 4 additions & 0 deletions src/Theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const DefaultTheme = {
surface: '#dfdfdf',
primary: '#005f3d',
accent: '#7de8ab',
text: '#333',
onSurface: '#999',
}
};
export const DarkTheme = {
Expand All @@ -28,5 +30,7 @@ export const DarkTheme = {
...CombinedDarkTheme.colors,
primary: '#66ff99',
accent: '#3a6b5a',
text: '#dfdfdf',
onSurface: '#aaa',
}
};
141 changes: 106 additions & 35 deletions src/analytics/ActivityPie.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import React from "react";
import { PieChart } from "react-native-svg-charts";
import { PieChart, Pie, Sector, Cell, ResponsiveContainer } from 'recharts';
import { View, StyleSheet } from "react-native";
import { useTheme } from "react-native-paper";
import ErrorBoundary from "../components/ErrorBoundary";
import { getFirstNWords } from "../utils/utils";

export function ActivityPie(props) {
const FREE_TIME_KEY = "free-time";
const { activities, totalHoursInWeek } = props;
const [data, setData] = React.useState([])
const theme = useTheme();
const [activeIndex, setActiveIndex] = React.useState(null);

const constructDataFromActivities = (activities) => {
const pieRadius = 140;
const styles = StyleSheet.create({
pieChart: {
alignSelf: 'center',
maxWidth: '100%',
maxHeight: '100%',
aspectRatio: 1,
width: '100%',
minHeight: 3 * pieRadius,
}
})

const constructDataFromActivities = (activities) => {
let freeHours = totalHoursInWeek;
const pieData = activities
.filter((activity) => activity.hours * activity.duration.multiplier > 0)
Expand All @@ -20,23 +34,19 @@ export function ActivityPie(props) {
return {
value: hoursInWeek,
key: index,
svg: {
fill: activity.color,
onPress: () => console.log('press', activity.name),
}
color: activity.color,
name: activity.name,
}
})

pieData.push({
value: freeHours,
key: "free-time",
svg: {
fill: theme.colors.background,
stroke: theme.colors.placeholder,
strokeWidth: 2,
onPress: () => console.log('press', 'free-time'),
},
arc: { outerRadius: '100%', innerRadius: '40%', cornerRadius: 10, }
key: FREE_TIME_KEY,
color: theme.colors.background,
name: "Free Time",
stroke: theme.colors.placeholder,
strokeWidth: 2,
cornerRadius: 10,
})
return pieData
}
Expand All @@ -45,30 +55,91 @@ export function ActivityPie(props) {
setData(constructDataFromActivities(activities));
}, [activities, theme])

const onPieEnter = (_, index) => {
setActiveIndex(index);
};

const onPieLeave = (_) => {
setActiveIndex(null);
};

const renderActiveShape = (props) => {
const RADIAN = Math.PI / 180;
const { cx, cy, midAngle, innerRadius, outerRadius, startAngle, endAngle, fill, payload } = props;
const final_fill = (payload.key === FREE_TIME_KEY) ? theme.colors.placeholder : fill;
const sin = Math.sin(-RADIAN * midAngle);
const cos = Math.cos(-RADIAN * midAngle);
const sx = cx + (outerRadius + 10) * cos;
const sy = cy + (outerRadius + 10) * sin;
const mx = cx + (outerRadius + 30) * cos;
const my = cy + (outerRadius + 30) * sin;
const ex = mx;
const ey = my + (sin >= 0 ? (1 - sin) : -Math.abs(1 + sin)) * outerRadius * 3 / 4;
const textAnchor = 'middle';

return (
<g>
<Sector
cx={cx}
cy={cy}
innerRadius={innerRadius}
outerRadius={outerRadius}
startAngle={startAngle}
endAngle={endAngle}
fill={(payload.key === FREE_TIME_KEY) ? theme.colors.background : fill}
stroke={final_fill}
strokeWidth={(payload.key === FREE_TIME_KEY) ? 3 : 0}
cornerRadius={(payload.key === FREE_TIME_KEY) ? 10 : 5}
/>
<Sector
cx={cx}
cy={cy}
startAngle={startAngle}
endAngle={endAngle}
innerRadius={outerRadius + 6}
outerRadius={outerRadius + 10}
fill={final_fill}
/>
<path d={`M${sx},${sy}L${mx},${my}L${ex},${ey}`} stroke={final_fill} fill="none" />
<circle cx={ex} cy={ey} r={2} fill={final_fill} stroke="none" />
<text x={ex} y={ey + 5 + (sin >= 0 ? 1 : -1) * 12} textAnchor={textAnchor} fill={theme.colors.text}>{`${getFirstNWords(payload.name, 2)}`}</text>
<text x={ex} y={ey + 5 + (sin >= 0 ? 1 : -1) * 12} dy={(sin >= 0 ? 1 : -1) * 18} textAnchor={textAnchor} fill={theme.colors.onSurface}>
{Number(payload.value.toFixed(1))} hr
</text>
</g>
);
};

return (
<View style={props.style}>
<ErrorBoundary >
<PieChart
style={styles.pieChart}
data={data}
sort={(a, b) => (b.key === "free-time") ? 1
: (a.key === "free-time") ? -1
: b.value - a.value
}
>
{props.children}
</PieChart>
<ResponsiveContainer style={styles.pieChart}>
<PieChart width={600} height={600}>
<Pie
data={data}
dataKey='value'
cx="50%"
cy="50%"
innerRadius={10 + pieRadius / 2}
outerRadius={pieRadius}
fill="#8884d8"
activeIndex={activeIndex}
activeShape={renderActiveShape}
onMouseEnter={onPieEnter}
onMouseLeave={onPieLeave}
startAngle={90}
endAngle={450}
paddingAngle={2}
cornerRadius={5}
strokeWidth={0}
>
{data.map((entry, index) => (
<Cell key={index} fill={entry.color} />
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</ErrorBoundary>
</View >
)
}

const styles = StyleSheet.create({
pieChart: {
alignSelf: 'center',
maxWidth: '100%',
maxHeight: '100%',
aspectRatio: 1,
width: '100%',
}
})
}
88 changes: 47 additions & 41 deletions src/pages/Help.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { useState } from 'react';
import EnablePromptAPI from './help/EnablePromptAPI';
import { useTheme } from 'react-native-paper';
import { useStylesheet } from 'react-native-responsive-ui';
import { Footer } from '../Footer';
import { StatusBar } from 'expo-status-bar';

const HelpSupport = () => {
const [activeTab, setActiveTab] = useState('enablePromptAPI');
Expand Down Expand Up @@ -105,49 +107,53 @@ const HelpSupport = () => {
const styles = merge(commonStyles, useStylesheet(responsiveStyles));

return (
<View >
<Text style={styles.title}>Help & Support</Text>
<View style={styles.container}>
<View style={styles.tabContainer}>
<TouchableOpacity onPress={() => setActiveTab('enablePromptAPI')}>
<Text style={activeTab === 'enablePromptAPI' ? styles.activeTab : styles.tab}>How to enable PromptAPI in my browser?</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setActiveTab('freeTimeCalculator')}>
<Text style={activeTab === 'freeTimeCalculator' ? styles.activeTab : styles.tab}>How to use the free time calculator?</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setActiveTab('bestPractices')}>
<Text style={activeTab === 'bestPractices' ? styles.activeTab : styles.tab}>Best practices for getting suggestions for free time.</Text>
</TouchableOpacity>
<ScrollView contentContainerStyle={{ minHeight: '100%' }}>
<View>
<Text style={styles.title}>Help & Support</Text>
<View style={styles.container}>
<View style={styles.tabContainer}>
<TouchableOpacity onPress={() => setActiveTab('enablePromptAPI')}>
<Text style={activeTab === 'enablePromptAPI' ? styles.activeTab : styles.tab}>How to enable PromptAPI in my browser?</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setActiveTab('freeTimeCalculator')}>
<Text style={activeTab === 'freeTimeCalculator' ? styles.activeTab : styles.tab}>How to use the free time calculator?</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setActiveTab('bestPractices')}>
<Text style={activeTab === 'bestPractices' ? styles.activeTab : styles.tab}>Best practices for getting suggestions for free time.</Text>
</TouchableOpacity>
</View>
<ScrollView style={styles.contentArea}>
{activeTab === 'enablePromptAPI' && (
<EnablePromptAPI />
)}
{activeTab === 'freeTimeCalculator' && (
<View>
<Text style={styles.question}>How to use the free time calculator?</Text>
<Text style={styles.answer}>To get started, think about all the activities that take up your time, such as your job, commute, family responsibilities, rest and recovery, leisure activities, hobbies, and any other regular commitments. For each activity, you'll need to fill in three fields:</Text>
<Text style={styles.step}>1. Activity: Give a brief name to the activity, e.g. "Full-time job" or "Daily commute"</Text>
<Text style={styles.step}>2. Time: Enter the number of hours you spend on this activity, e.g. 40 hours per week or 1 hour per day</Text>
<Text style={styles.step}>3. Frequency: Choose how often you do this activity from the dropdown options, e.g. "per day", "per week", "per month", etc.</Text>
<Text style={styles.answer}>For example, if you work 40 hours a week, you would fill in "Full-time job" as the activity, "40" as the time, and "per week" as the frequency. Don't forget to include daily chores and other unavoidable activities in your list!</Text>
<Text style={styles.answer}>Once you've added all your activities, you'll see a pie-chart showing how your weekly time is distributed, as well as the number of hours of free time you have available. You can then use this information to make informed decisions about how to use your time more effectively.</Text>
</View>
)}
{activeTab === 'bestPractices' && (
<View>
<Text style={styles.question}>Best practices for getting suggestions for free time.</Text>
<Text style={styles.answer}>To get the most out of the free time suggestions, make sure to add all your activities, including both essential and non-essential ones. Be as detailed as possible when describing each activity, as this will help the AI generate more relevant suggestions.</Text>
<Text style={styles.step}>1. Be honest about how you spend your time: Include all your activities, even if they seem insignificant or unimportant.</Text>
<Text style={styles.step}>2. Be specific: Use clear and concise language when describing each activity.</Text>
<Text style={styles.step}>3. Think about your goals: Consider what you want to achieve with your free time, and how you can use it to live a more fulfilling life.</Text>
<Text style={styles.answer}>By following these best practices, you'll get personalized suggestions for saving time in non-essential activities and finding new ways to utilize your free time.</Text>
</View>
)}
</ScrollView>
</View>
<ScrollView style={styles.contentArea}>
{activeTab === 'enablePromptAPI' && (
<EnablePromptAPI />
)}
{activeTab === 'freeTimeCalculator' && (
<View>
<Text style={styles.question}>How to use the free time calculator?</Text>
<Text style={styles.answer}>To get started, think about all the activities that take up your time, such as your job, commute, family responsibilities, rest and recovery, leisure activities, hobbies, and any other regular commitments. For each activity, you'll need to fill in three fields:</Text>
<Text style={styles.step}>1. Activity: Give a brief name to the activity, e.g. "Full-time job" or "Daily commute"</Text>
<Text style={styles.step}>2. Time: Enter the number of hours you spend on this activity, e.g. 40 hours per week or 1 hour per day</Text>
<Text style={styles.step}>3. Frequency: Choose how often you do this activity from the dropdown options, e.g. "per day", "per week", "per month", etc.</Text>
<Text style={styles.answer}>For example, if you work 40 hours a week, you would fill in "Full-time job" as the activity, "40" as the time, and "per week" as the frequency. Don't forget to include daily chores and other unavoidable activities in your list!</Text>
<Text style={styles.answer}>Once you've added all your activities, you'll see a pie-chart showing how your weekly time is distributed, as well as the number of hours of free time you have available. You can then use this information to make informed decisions about how to use your time more effectively.</Text>
</View>
)}
{activeTab === 'bestPractices' && (
<View>
<Text style={styles.question}>Best practices for getting suggestions for free time.</Text>
<Text style={styles.answer}>To get the most out of the free time suggestions, make sure to add all your activities, including both essential and non-essential ones. Be as detailed as possible when describing each activity, as this will help the AI generate more relevant suggestions.</Text>
<Text style={styles.step}>1. Be honest about how you spend your time: Include all your activities, even if they seem insignificant or unimportant.</Text>
<Text style={styles.step}>2. Be specific: Use clear and concise language when describing each activity.</Text>
<Text style={styles.step}>3. Think about your goals: Consider what you want to achieve with your free time, and how you can use it to live a more fulfilling life.</Text>
<Text style={styles.answer}>By following these best practices, you'll get personalized suggestions for saving time in non-essential activities and finding new ways to utilize your free time.</Text>
</View>
)}
</ScrollView>
<Text style={styles.contact}>For any support or issues, please visit our <Text style={{ textDecorationLine: "underline", textDecorationColor: theme.colors.accent }} onPress={() => Linking.openURL('https://github.com/avikalpg/free-time')}>GitHub repository</Text> and raise an issue or start a discussion.</Text>
</View>
<Text style={styles.contact}>For any support or issues, please visit our <Text style={{ textDecoration: "underline", textDecorationColor: theme.colors.accent }} onPress={() => Linking.openURL('https://github.com/avikalpg/free-time')}>GitHub repository</Text> and raise an issue or start a discussion.</Text>
</View>
<Footer />
<StatusBar style="auto" />
</ScrollView>
);
};

Expand Down
4 changes: 2 additions & 2 deletions src/pages/help/EnablePromptAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const EnablePromptAPI = () => {
borderRadius: 5,
},
link: {
color: 'blue',
textDecorationLine: 'underline',
textDecorationColor: theme.colors.accent,
marginBottom: 5,
},
});
Expand Down Expand Up @@ -73,7 +73,7 @@ const EnablePromptAPI = () => {
<ol type='i'>
<li>
<Text style={styles.step}>
Go to <Text style={{ color: 'blue' }} onPress={() => Linking.openURL('chrome://flags')}>chrome://flags</Text> in your browser.
Go to <Text style={styles.link} onPress={() => Linking.openURL('chrome://flags')}>chrome://flags</Text> in your browser.
</Text>
</li>
<li>
Expand Down
22 changes: 22 additions & 0 deletions src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,26 @@ export const validateHours = (activity) => {
return { valid: false, reason: "Hours exceed 168 hours per week." };
}
return { valid: true, reason: "" };
}

/**
*
* @param {string} str string from which we want the substring
* @param {number} n number of words you want to extract
* @returns n-word long prefix substring from str
*/
export function getFirstNWords(str, n) {
if (typeof str !== 'string' || str.trim() === "") {
return ""; // Or handle invalid input as needed (e.g., throw an error)
}

const words = str.trim().split(/\s+/); // Split by any whitespace (including multiple spaces)
if (words.length >= n) {
return words.slice(0, n).join(" ");
} else if (words.length === 1) {
return words[0];
}
else {
return ""; // or null, or whatever you want to return if it's an empty string.
}
}
Loading
Loading