in my firestore database i have a collection called expense categories. In the expense categories, I have documents like; food, clothing, housing etc. these documents have objects as fields. The field are in this format:{uid,color,itemname,amount}, how do I represent this data on a pie chart using victory native. it is an expo react native project with typescript. I provided a screenshot of my database structure. I watched a tutorial on this and the code of the tutor looks like this:
import {
View,
Text,
TouchableOpacity,
ScrollView,
TouchableWithoutFeedback,
FlatList,
Image,
Animated,
Platform,
Dimensions,
StatusBar,
StyleSheet,
Modal,
} from "react-native";
import React, { useContext, useRef, useState, useEffect } from "react";
import ThemeContext from "../themes/themeContext";
import { colors } from "../themes/theme";
import { Ionicons } from "@expo/vector-icons";
import { currentdate } from "../utilities/date";
import { CategoriesData } from "../data/categoriesData";
import icons from "../constants/icons";
import { Svg } from "react-native-svg";
import { Easing } from "react-native-reanimated";
import { VictoryPie } from "victory-native";
import COLORS from "../constants/colors";
import { db } from "../config/firebase";
import { collection, doc, getDocs, updateDoc } from "firebase/firestore";
import { Input } from "react-native-elements";
const { width, height } = Dimensions.get("window");
export default function TransactionHistory() {
const categoryListHeightAnimationValue = useRef(
new Animated.Value(170)
).current;
const { theme, UpdateTheme } = useContext<any>(ThemeContext);
const [categories, setCategories] = useState([]);
useEffect(() => {
const fetchCategories = async () => {
try {
const querySnapshot = await getDocs(collection(db, "categories"));
const categoriesData = querySnapshot.docs.map((doc) => doc.data());
setCategories(categoriesData);
} catch (error) {
console.error("Error fetching categories:", error);
}
};
fetchCategories();
}, []);
const icoSize = 25;
const size = `size:${icoSize}`;
let activeColors = colors[theme.mode];
const [viewMode, setViewMode] = useState("chart");
const [selectedCategory, setselectedCategory] = useState<any>(null);
const [toggleShowMore, setToggleShowMore] = useState(false);
const status = {
confirmed: "confirmed ✅",
pending: `Pending ${(
<Ionicons
name="timer-outline"
size={icoSize}
color={activeColors.color}
/>
)}`,
};
const [isActive, setIsActive] = useState<any>(theme.mode === "dark");
function RenderNavbar() {
return (
<View
className="flex flex-row h-[50px] items-center justify-between px-4"
style={{ backgroundColor: activeColors.background }}
>
<TouchableOpacity className=" ">
<Ionicons
name="arrow-back-outline"
size={25}
color={activeColors.color}
/>
</TouchableOpacity>
<TouchableOpacity
className="items-end justify-center"
onPress={() => {}}
>
<Ionicons
name="ellipsis-horizontal"
size={25}
color={activeColors.color}
/>
</TouchableOpacity>
</View>
);
}
function RenderHeader() {
return (
<View className="px-4">
<View>
<Text
className="text-2xl font-medium"
style={{
fontFamily: "SourceSansPro-Black",
color: activeColors.accent,
}}
>
Expenses
</Text>
{/* <Text
className="font-medium"
style={{
fontFamily: "SourceSansPro-SemiBold",
color: activeColors.tint,
}}
>
Summary
</Text> */}
</View>
<View className="flex flex-row items-center gap-2">
<View>
<Ionicons name="calendar" size={15} color={activeColors.blue} />
</View>
<View className="ml-3 text-lg">
<Text style={{ color: activeColors.color }}>{currentdate}</Text>
</View>
</View>
</View>
);
}
function CategoryHeaders(props: { title: string; shownOnlyTitle: boolean }) {
return (
<View
className={`flex flex-row p-2 justify-between items-center`}
style={{ backgroundColor: activeColors.background }}
>
<View>
<Text
className="font-bold uppercase text-lg"
style={{ letterSpacing: 1, color: activeColors.color }}
>
{props.title}
</Text>
{props.shownOnlyTitle ? (
<></>
) : (
<Text
className=""
style={{ letterSpacing: 1, color: activeColors.tint }}
>
{categories.length} total
</Text>
)}
</View>
{props.shownOnlyTitle ? (
<></>
) : (
<View className="flex flex-row gap-x-3.5 pr-1.5">
<TouchableOpacity
className=" items-center justify-center h-10 w-10 rounded-md "
style={{
backgroundColor:
viewMode === "chart"
? activeColors.primary
: activeColors.tint,
}}
onPress={() => {
setViewMode("chart");
}}
>
<Ionicons
name="add-outline"
size={25}
color={
viewMode === "items"
? activeColors.background
: activeColors.tint
}
/>
</TouchableOpacity>
<TouchableOpacity
className=" items-center justify-center h-10 w-10 rounded-md"
style={{
backgroundColor:
viewMode === "items"
? activeColors.primary
: activeColors.tint,
}}
onPress={() => {
setViewMode("items");
}}
>
<Ionicons
name="analytics-outline"
size={25}
color={
viewMode === "chart"
? activeColors.background
: activeColors.tint
}
/>
</TouchableOpacity>
</View>
)}
</View>
);
}
function RenderCategoryList() {
const RenderItem: React.FC<any> = ({ item }) => {
return (
<TouchableOpacity
className="flex flex-1 flex-row mx-1.5 my-2 px-3 py-4 rounded-md overflow-hidden w-fit items-center"
style={{ backgroundColor: activeColors.accent, ...styles.shadow }}
onPress={() => {
setselectedCategory(item);
}}
>
<Image
source={item.icoName}
style={{
width: 35,
height: 35,
borderRadius: 10,
}}
/>
<Text
className="text-base text-white"
style={{ color: activeColors.color }}
>
{item.name}
</Text>
</TouchableOpacity>
);
};
return (
<View className="px-1 my-1">
<FlatList
data={categories}
renderItem={({ item }) => <RenderItem item={item} />}
keyExtractor={(item) => `${item.id}+${item.name}`}
numColumns={2}
pagingEnabled
// horizontal
/>
{/* <Animated.ScrollView
contentContainerStyle={{paddingBottom:100}}
className={"ease-in-out duration-500"}
style={{ height: categoryListHeightAnimationValue }}
>
<FlatList
data={categories}
renderItem={({ item }) => <RenderItem item={item} />}
keyExtractor={(item) => `${item.id}+${item.name}`}
numColumns={2}
pagingEnabled
// horizontal
/>
</Animated.ScrollView> */}
{/* <TouchableOpacity
className="flex flex-row justify-center"
onPress={() => {
if (toggleShowMore) {
Animated.timing(categoryListHeightAnimationValue, {
toValue: 170,
duration: 300,
useNativeDriver: false,
easing: Easing.ease,
}).start();
} else {
Animated.timing(categoryListHeightAnimationValue, {
toValue: 270,
duration: 300,
useNativeDriver: false,
easing: Easing.ease,
}).start();
}
setToggleShowMore(!toggleShowMore);
}}
>
<Text
className={"ease-in-out duration-500 font-bold text-lg"}
style={{ color: activeColors.color }}
>
{toggleShowMore ? "collapse" : "expand"}
</Text>
<Ionicons
name={toggleShowMore ? "ios-chevron-up" : "ios-chevron-down"}
color={activeColors.color}
className="ml-2 self-center"
size={icoSize}
/>
</TouchableOpacity> */}
</View>
);
}
function RenderIncomingExpenses() {
let allExpenses = selectedCategory ? selectedCategory.expenses : [];
let incomingExpenses = allExpenses.filter(
(a: any) => a.status == "pending"
);
const RenderItem = ({ item, index }: any) => {
return (
<View
className="w-80 mr-6 rounded-xl my-3"
style={{ marginLeft: index == 0 ? 4 : 0, ...styles.shadow }}
>
<View
className="flex flex-row items-center p-3"
style={{ backgroundColor: activeColors.warm }}
>
<View className="h-[50] w-[50] rounded-md items-center justify-center mr-2">
<Image
source={selectedCategory.icoName}
style={{
width: 30,
height: 30,
borderRadius: 10,
}}
/>
</View>
</View>
{/* <Text className="text-lg text-white bg-neon">{`${item.expenses[1].description}`}</Text> */}
<Text
className="text-lg text-white "
style={{ color: activeColors.color }}
>
{selectedCategory.name}
</Text>
{/* Expense Description */}
<View style={{ paddingHorizontal: 24 }}>
{/* Title and description */}
<Text
style={{
fontSize: 20,
color: activeColors.color,
fontWeight: "700",
}}
>
{item.title}
</Text>
<Text
className="text-base font-medium"
style={{
fontSize: 17,
flexWrap: "wrap",
color: COLORS.darkgray,
}}
>
{item.description}
</Text>
{/* Location */}
<Text
className="text-base font-medium"
style={{ marginTop: 24, color: activeColors.color }}
>
Location
</Text>
<View className="gap-y-3" style={{ flexDirection: "column" }}>
<Text
style={{
fontSize: 17,
color: activeColors.color,
}}
>
{item.location}
</Text>
<View className="flex flex-row">
<Text
className="text-base mr-1 -mb-3 font-medium"
style={{ color: activeColors.color }}
>
Price:
</Text>
<Text
style={{
fontSize: 17,
color: "red",
}}
>
{item.total}
</Text>
</View>
</View>
</View>
{/* Price */}
{/* Descripton */}
</View>
);
};
return (
<View className="">
<CategoryHeaders title="Items" shownOnlyTitle={true} />
{incomingExpenses.length > 0 && (
<FlatList
data={incomingExpenses}
renderItem={RenderItem}
keyExtractor={(item) => `${item.id}`}
// pagingEnabled
showsHorizontalScrollIndicator={false}
// horizontal
showsVerticalScrollIndicator={false}
/>
)}
{incomingExpenses.length == 0 && (
<View className="items-center justify-center h-[300]" style={{}}>
<Text style={{ color: activeColors.tint }}>No Records</Text>
</View>
)}
</View>
);
}
function processCategoryDataToDisplay() {
// Filter expenses with "Confirmed" status
let chartData = categories.map((item) => {
let confirmExpenses = item.expenses.filter(
(a) => a.status == "confirmed"
);
var total = confirmExpenses.reduce((a, b) => a + (b.total || 0), 0);
return {
name: item.name,
y: total,
expenseCount: confirmExpenses.length,
color: item.color,
id: item.id,
};
});
// filter out categories with no data/expenses
let filterChartData = chartData.filter((a) => a.y > 0);
// Calculate the total expenses
let totalExpense = filterChartData.reduce((a, b) => a + (b.y || 0), 0);
// Calculate percentage and repopulate chart data
let finalChartData = filterChartData.map((item) => {
let percentage = ((item.y / totalExpense) * 100).toFixed(0);
return {
label: `${percentage}%`,
y: Number(item.y),
expenseCount: item.expenseCount,
color: item.color,
name: item.name,
id: item.id,
};
});
return finalChartData;
}
function setSelectCategoryByName(name: any) {
let category = categories.filter((a) => a.name == name);
setselectedCategory(category[0]);
}
function RenderChart() {
let chartData = processCategoryDataToDisplay();
let colorScales = chartData.map((item) => item.color);
let totalExpenseCount = chartData.reduce(
(a, b) => a + (b.expenseCount || 0),
0
);
console.log("Check Chart");
console.log(chartData);
if (Platform.OS == "ios") {
return (
<View style={{ alignItems: "center", justifyContent: "center" }}>
<VictoryPie
data={chartData}
labels={(datum) => `${datum.y}`}
radius={({ datum }) =>
selectedCategory && selectedCategory.name == datum.name
? width * 0.4
: width * 0.4 - 10
}
innerRadius={70}
labelRadius={({ innerRadius }) => (width * 0.4 + innerRadius) / 2.5}
style={{
labels: { fill: "white" },
parent: {
...styles.shadow,
},
}}
width={width * 0.8}
height={width * 0.8}
colorScale={colorScales}
events={[
{
target: "data",
eventHandlers: {
onPress: () => {
return [
{
target: "labels",
mutation: (props) => {
let categoryName = chartData[props.index].name;
setSelectCategoryByName(categoryName);
},
},
];
},
},
},
]}
/>
<View style={{ position: "absolute", top: "42%", left: "42%" }}>
<Text style={{ textAlign: "center" }}>{totalExpenseCount}</Text>
<Text className="text-lg font-bold" style={{ textAlign: "center" }}>
Expenses
</Text>
</View>
</View>
);
} else {
// Android workaround by wrapping VictoryPie with SVG
return (
<View style={{ alignItems: "center", justifyContent: "center" }}>
<Svg
width={width}
height={width}
style={{ width: "100%", height: "auto" }}
>
<VictoryPie
standalone={false} // Android workaround
data={chartData}
labels={(datum) => `${datum.y}`}
radius={({ datum }) =>
selectedCategory && selectedCategory.name == datum.name
? width * 0.4
: width * 0.4 - 10
}
innerRadius={70}
labelRadius={({ innerRadius }) =>
(width * 0.4 + innerRadius) / 2.5
}
style={{
labels: { fill: "white" },
parent: {
...styles.shadow,
},
}}
width={width}
height={width}
colorScale={colorScales}
events={[
{
target: "data",
eventHandlers: {
onPress: () => {
return [
{
target: "labels",
mutation: (props) => {
let categoryName = chartData[props.index].name;
setSelectCategoryByName(categoryName);
},
},
];
},
},
},
]}
/>
</Svg>
<View style={{ position: "absolute", top: "42%", left: "42%" }}>
<Text className="text-xl font-bold" style={{ textAlign: "center" }}>
{totalExpenseCount}
</Text>
<Text className="text-xl font-bold" style={{ textAlign: "center" }}>
Expenses
</Text>
</View>
</View>
);
}
}
function RenderExpenseSummary() {
let data = processCategoryDataToDisplay();
const renderItem = ({ item }: any) => (
<TouchableOpacity
className="mb-4"
style={{
flexDirection: "row",
height: 40,
paddingHorizontal: 12,
borderRadius: 10,
backgroundColor:
selectedCategory && selectedCategory.name == item.name
? item.color
: COLORS.white,
}}
onPress={() => {
let categoryName = item.name;
setSelectCategoryByName(categoryName);
}}
>
{/* Name/Category */}
<View style={{ flex: 1, flexDirection: "row", alignItems: "center" }}>
<View
style={{
width: 20,
height: 20,
backgroundColor:
selectedCategory && selectedCategory.name == item.name
? COLORS.white
: item.color,
borderRadius: 5,
}}
/>
<Text
style={{
marginLeft: 8,
color:
selectedCategory && selectedCategory.name == item.name
? COLORS.white
: COLORS.primary,
}}
>
{item.name}
</Text>
</View>
{/* Expenses */}
<View style={{ justifyContent: "center" }}>
<Text
style={{
color:
selectedCategory && selectedCategory.name == item.name
? COLORS.white
: COLORS.primary,
}}
>
{item.y} USD - {item.label}
</Text>
</View>
</TouchableOpacity>
);
return (
<View style={{ padding: 24 }}>
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => `${item.id}`}
/>
</View>
);
}
const CategoryDetails = ({ category }) => {
const [isModalOpen, setModalOpen] = useState(false);
const [expenseTitle, setExpenseTitle] = useState("");
const [expenseDescription, setExpenseDescription] = useState("");
const [expenseLocation, setExpenseLocation] = useState("");
const [expenseTotal, setExpenseTotal] = useState("");
const handleAddExpense = async () => {
const newExpense = {
id: category.expenses.length + 1,
title: expenseTitle,
description: expenseDescription,
location: expenseLocation,
total: parseFloat(expenseTotal),
status: "pending",
};
// Get a reference to the category document
const categoryDocRef = doc(db, "categories", category.id.toString());
// Update the expenses array with the new expense
const updatedExpenses = [...category.expenses, newExpense];
// Update the category document in Firestore
try {
await updateDoc(categoryDocRef, {
expenses: updatedExpenses,
});
// Clear the input fields
setExpenseTitle("");
setExpenseDescription("");
setExpenseLocation("");
setExpenseTotal("");
// Close the modal
setModalOpen(false);
} catch (error) {
console.error("Error adding expense:", error);
}
};
return (
<View>
<FlatList
data={category.expenses}
renderItem={({ item }) => (
<View>
<Text>{item.title}</Text>
<Text>{item.description}</Text>
<Text>{item.location}</Text>
<Text>{item.total}</Text>
</View>
)}
keyExtractor={(item) => item.id.toString()}
/>
<Modal visible={isModalOpen} animationType="slide">
<View>
<Input
style={styles.input}
value={expenseTitle}
onChangeText={setExpenseTitle}
placeholder="Expense Title"
/>
<Input
style={styles.input}
value={expenseDescription}
onChangeText={setExpenseDescription}
placeholder="Expense Description"
/>
<Input
style={styles.input}
value={expenseLocation}
onChangeText={setExpenseLocation}
placeholder="Expense Location"
/>
<Input
style={styles.input}
value={expenseTotal}
onChangeText={setExpenseTotal}
placeholder="Expense Total"
keyboardType="numeric"
/>
<TouchableOpacity
style={styles.addButton}
onPress={handleAddExpense}
>
<Text>Add Expense</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.closeButton}
onPress={() => setModalOpen(false)}
>
<Text>Close</Text>
</TouchableOpacity>
</View>
</Modal>
<TouchableOpacity
style={styles.addButton}
onPress={() => setModalOpen(true)}
>
<Text>Add More</Text>
</TouchableOpacity>
</View>
);
};
const [selectedFirestoreCategory, setSelectedFirestoreCategory] = useState(
categories[0]
);
// const AddExpenseForm = ({ category }) => {
// const [title, setTitle] = useState('');
// const [description, setDescription] = useState('');
// const [location, setLocation] = useState('');
// const [total, setTotal] = useState('');
// const handleAddExpense = async () => {
// const expense = {
// id: Math.random().toString(),
// title,
// description,
// location,
// total: parseFloat(total),
// status: status.pending,
// };
// try {
// // Update the expenses array in Firestore
// await updateDoc(doc(firestore, 'categories', category.id), {
// expenses: [...category.expenses, expense],
// });
// // Clear the form inputs
// setTitle('');
// setDescription('');
// setLocation('');
// setTotal('');
// } catch (error) {
// console.log('Error adding expense:', error);
// }
// };
// return (
// <View>
// <TextInput value={title} onChangeText={setTitle} placeholder="Title" />
// <TextInput value={description} onChangeText={setDescription} placeholder="Description" />
// <TextInput value={location} onChangeText={setLocation} placeholder="Location" />
// <TextInput value={total} onChangeText={setTotal} placeholder="Total" keyboardType="numeric" />
// <Button onPress={handleAddExpense} title="Add Expense" />
// </View>
// );
// };
return (
<View
className="flex flex-1 mb-5 bg-neutral-950 overflow-y-scroll"
style={{ backgroundColor: activeColors.background }}
>
<RenderNavbar />
<RenderHeader />
<StatusBar backgroundColor={activeColors.background} />
<CategoryHeaders title="Categories" shownOnlyTitle={false} />
<ScrollView
contentContainerStyle={{ paddingBottom: 90 }}
className="pb-5 mx-2 "
>
{viewMode === "items" && (
<View>
<RenderCategoryList />
<RenderIncomingExpenses />
</View>
)}
{viewMode == "chart" && (
<View>
<RenderChart />
<RenderExpenseSummary />
</View>
)}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
shadow: {
shadowColor: "white",
shadowOffset: {
width: 2,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
paddingHorizontal: 5,
paddingVertical: 18,
alignItems: "center",
justifyContent: "space-between",
elevation: 3,
},
input: {
height: 40,
borderWidth: 1,
marginBottom: 10,
paddingHorizontal: 10,
},
addButton: {
backgroundColor: "green",
padding: 10,
borderRadius: 5,
marginBottom: 10,
},
closeButton: {
backgroundColor: "red",
padding: 10,
borderRadius: 5,
},
});
But the database structure looks different from mine. I would be very grateful if the code modified is to suit the expense categories collectionin my database structure which i shared the screenshot