expo react native and victory native pie chart

277 Views Asked by At

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

0

There are 0 best solutions below