From 912b38dfddc8f668d93dcfdfe9cc10a95c19ece6 Mon Sep 17 00:00:00 2001 From: Ghina Al Rashwani Date: Wed, 22 Oct 2025 20:29:53 +0300 Subject: [PATCH] ginar/ issue #665 fixing toggle --- components/home/MarketHistoryChart.tsx | 586 +++++++++++++++++++------ 1 file changed, 444 insertions(+), 142 deletions(-) diff --git a/components/home/MarketHistoryChart.tsx b/components/home/MarketHistoryChart.tsx index ace2a5f4f..4312928d6 100644 --- a/components/home/MarketHistoryChart.tsx +++ b/components/home/MarketHistoryChart.tsx @@ -1,174 +1,476 @@ -import { useEffect, useState } from "react"; +import React, { + useState, + useEffect, + useMemo, + useCallback, + SetStateAction, + Dispatch, +} from "react"; import { - LineChart, Line, + LineChart, XAxis, YAxis, Tooltip, - ResponsiveContainer, Legend, + ResponsiveContainer, Brush, } from "recharts"; -import Hive from "@/types/Hive"; +import { formatNumber } from "@/lib/utils"; +import { cn } from "@/lib/utils"; import moment from "moment"; +import { ArrowDown, ArrowUp, Minus } from "lucide-react"; +import { useI18n } from "@/i18n/i18n"; +import useDynamicGlobal from "@/hooks/api/homePage/useDynamicGlobal"; import { useHiveChainContext } from "@/contexts/HiveChainContext"; -import { colorMap } from "../balanceHistory/BalanceHistoryChart"; -import { useTheme } from "@/contexts/ThemeContext"; -import { useI18n } from "../../i18n/i18n"; - -const CustomTooltip = ({ - active, - payload, -}: { - active?: boolean; - payload?: any[]; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { useSettings } from "@/contexts/SettingsContext"; + +interface BalanceHistoryChartProps { + aggregatedAccountBalanceHistory?: { + timestamp: string; + balance_change: number; + balance: number; + savings_balance?: number; + savings_balance_change?: number; + hivePrice: string; + dollarValue?: number; + convertedHive?: number; + }[]; + className?: string; + quickView?: boolean; + showSavingsBalance?: string; + selectedCoinType: string; + setSelectedCoinType: Dispatch>; +} + +export const colorMap: Record = { + HIVE: "#8884d8", + VESTS: "#82ca9d", + HBD: "#ff7300", + SAVINGS: "#1E90FF", + DOLLAR: "#4be7f0", +}; + +const BalanceHistoryChart: React.FC = ({ + aggregatedAccountBalanceHistory, + className = "", + quickView = false, + showSavingsBalance = "yes", + selectedCoinType, + setSelectedCoinType, }) => { - const { t, locale } = useI18n(); + const { t, dir } = useI18n(); + const { hiveChain } = useHiveChainContext(); + const { dynamicGlobalData } = useDynamicGlobal(); + const { settings } = useSettings(); + const isRTL = dir === "rtl"; + + const [isMobile, setIsMobile] = useState(window.innerWidth < 480); + const [hiddenDataKeys, setHiddenDataKeys] = useState([]); + const [unit, setUnit] = useState<"vests" | "hp">( + settings.displayVestHpMode === "hp" ? "hp" : "vests" + ); + + useEffect(() => { + setUnit(settings.displayVestHpMode === "hp" ? "hp" : "vests"); + }, [settings.displayVestHpMode]); + + const availableCoins = ["HIVE", "VESTS", "HBD"]; + const [zoomedDomain, setZoomedDomain] = useState<[number, number] | null>(null); + + useEffect(() => { + const handleResize = () => { + setIsMobile(window.innerWidth < 480); + }; + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const processedData = useCallback( + (data: any, type: string) => { + if (!dynamicGlobalData || !hiveChain || !data) return []; + + return data.map((item: any) => { + const hivePrice = parseFloat(item.hivePrice || "0"); + + if (type === "VESTS") { + const vests = item.balance?.toString() || "0"; + + let convertedHPRaw = hiveChain.vestsToHp( + vests, + dynamicGlobalData.headBlockDetails.rawTotalVestingFundHive, + dynamicGlobalData.headBlockDetails.rawTotalVestingShares + ); + + let convertedValue: number; + if (typeof convertedHPRaw === "number") convertedValue = convertedHPRaw; + else if (convertedHPRaw && typeof convertedHPRaw === "object" && "amount" in convertedHPRaw) + convertedValue = parseFloat(convertedHPRaw.amount); + else if (typeof convertedHPRaw === "string") convertedValue = parseFloat(convertedHPRaw); + else convertedValue = 0; + + const dollarValueFull = + !isNaN(convertedValue) && !isNaN(hivePrice) ? (convertedValue * hivePrice) : 0; + + return { ...item, convertedHive: convertedValue, dollarValue: dollarValueFull }; + } + + if (type === "HIVE") { + const dollarValue = !isNaN(item.balance) && !isNaN(hivePrice) ? item.balance * hivePrice : 0; + return { ...item, dollarValue }; + } + + if (type === "HBD") { + return { ...item, dollarValue: !isNaN(item.balance) ? item.balance : 0 }; + } + + return item; + }); + }, + [dynamicGlobalData, hiveChain] + ); + + const dataMap = useMemo(() => { + return { + [selectedCoinType]: processedData(aggregatedAccountBalanceHistory, selectedCoinType) || [], + }; + }, [processedData, aggregatedAccountBalanceHistory, selectedCoinType]); + + const handleCoinTypeChange = (coinType: string) => { + setSelectedCoinType(coinType); + }; + + const displayData = useMemo(() => dataMap[selectedCoinType], [selectedCoinType, dataMap]); + + // ---------------- Tooltip ---------------- + const CustomTooltip = ({ active, payload, label }: { active?: boolean; payload?: any[]; label?: string }) => { + const { dir: tooltipDir } = useI18n(); + if (quickView || !active || !payload || payload.length === 0) return null; + const isTooltipRTL = tooltipDir === "rtl"; + const selectedData = displayData?.find((item: any) => item.timestamp === label); + if (!selectedData) return null; + + const actualBalance = selectedCoinType === "VESTS" && unit === "hp" + ? selectedData?.convertedHive ?? 0 + : selectedData?.balance ?? 0; + + const balanceChange = selectedData?.balance_change ?? 0; + const savingsBalance = selectedData?.savings_balance ?? undefined; + const savingsBalanceChange = selectedData?.savings_balance_change ?? 0; + const dollarValue = selectedData?.dollarValue ?? 0; + + const isPositiveChange = balanceChange > 0; + const isZeroChange = balanceChange === 0; + const isSavingsPositiveChange = savingsBalanceChange > 0; + const isSavingsZeroChange = savingsBalanceChange === 0; + + const currentCoinColor = colorMap[selectedCoinType]; - if (active && payload && payload.length) { return ( -
- {payload.map(({ payload: { tooltipDate, close, volume } }) => { - return ( -
-

- {t("marketHistoryChart.date")}: {tooltipDate} -

-

- {t("marketHistoryChart.closePrice")}: ${close} -

-

- {t("marketHistoryChart.volume")}:{" "} - {volume.toLocaleString("en-US")} HIVE -

+
+

{`${t("common.date")}: ${label}`}

+
+
+ {isPositiveChange ? ( + + ) : isZeroChange ? ( + + ) : ( + + )} + {` ${formatNumber(balanceChange, selectedCoinType === "VESTS" ? unit === "vests" : false)}`} +
+
+ {`${t("common.balance")}: ${formatNumber(actualBalance, selectedCoinType === "VESTS" && unit === "vests")}`} +
+ {dollarValue ? ( +
+ Dollar Value: ${formatNumber(dollarValue, false, selectedCoinType === "VESTS")}
- ); - })} + ) : null} +
+ {showSavingsBalance === "yes" && + savingsBalance !== undefined && + selectedCoinType !== "VESTS" && ( +
+
+ {isSavingsPositiveChange ? ( + + ) : isSavingsZeroChange ? ( + + ) : ( + + )} + {` ${formatNumber( + savingsBalanceChange, + selectedCoinType === "VESTS" ? unit === "vests" : false + )}`} +
+
+ {`${t("balanceHistoryChart.savingsBalance")}: ${formatNumber( + savingsBalance, + selectedCoinType === "VESTS" ? unit === "vests" : false + )}`} +
+
+ )}
); - } + }; - return null; -}; + // ---------------- Coin toggle buttons ---------------- + const renderCoinButtons = () => ( +
+ {availableCoins.map((coinType) => ( + + ))} +
+ ); -export const calculateCloseHivePrice = ( - hive: Hive.MarketData | undefined, - nonHive: Hive.MarketData | undefined -) => { - if (!hive || !nonHive) return; - const hiveClose = hive.close; - const nonHiveClose = nonHive.close; + // ---------------- Min/Max for Y-axis ---------------- + const getMinMax = (data: any[]): [number, number] => { + if (!data || data.length === 0) return [0, 1]; - return (nonHiveClose / hiveClose).toFixed(4); -}; + let allValues: number[] = []; -interface MarketChartProps { - data: Hive.MarketHistory | undefined; - isFullChart?: boolean; -} -interface ChartData { - date: string; - close: string | undefined; - volume: number; -} + if (selectedCoinType === "VESTS") { + allValues = data.map((item) => item.convertedHive || 0); + const dollarValues = data + .map((item) => item.dollarValue) + .filter((v): v is number => typeof v === "number" && !Number.isNaN(v)); + allValues = allValues.concat(dollarValues); + } else { + allValues = data.map((item) => item.balance); + const dollarValues = data + .map((item) => item.dollarValue) + .filter((v): v is number => typeof v === "number" && !Number.isNaN(v)); + allValues = allValues.concat(dollarValues); -const MarketHistoryChart: React.FC = ({ - data, - isFullChart = false, -}) => { - const { hiveChain } = useHiveChainContext(); - const { theme } = useTheme(); + if (showSavingsBalance === "yes") { + const savingsValues = data + .map((item) => item.savings_balance) + .filter((v): v is number => typeof v === "number"); + allValues = allValues.concat(savingsValues); + } + } - const { t, dir } = useI18n(); - const isRTL = dir === "rtl"; + return [Math.min(...allValues), Math.max(...allValues)]; + }; - const [chartData, setChartData] = useState( - undefined - ); - const [minValue, setMinValue] = useState(0); - const [maxValue, setMaxValue] = useState(0); + const [fullDataMin, fullDataMax] = getMinMax(displayData); + const [minValue, maxValue] = zoomedDomain || [fullDataMin, fullDataMax]; - useEffect(() => { - if (!data || !hiveChain) return; - - const filterData = data.buckets.map((bucket) => { - const { hive, non_hive } = bucket; - const hiveClosePrice = calculateCloseHivePrice(hive, non_hive); - - return { - date: moment(bucket.open).format("MMM D"), - tooltipDate: moment(bucket.open).format("YYYY MMM D"), - close: hiveClosePrice, - volume: bucket.hive.volume, - }; - }); - - const min = Math.min( - ...filterData?.map((d: ChartData) => parseFloat(d.close ?? "")) - ); - const max = Math.max( - ...filterData?.map((d: ChartData) => parseFloat(d.close ?? "")) - ); + const handleBrushAreaChange = (domain: { startIndex?: number; endIndex?: number }) => { + if (!domain || domain.startIndex === undefined || domain.endIndex === undefined) { + setZoomedDomain([fullDataMin, fullDataMax]); + return; + } + const visibleData = (displayData || []).slice(domain.startIndex, domain.endIndex + 1); + if (visibleData.length > 0) { + const [min, max] = getMinMax(visibleData); + setZoomedDomain([min, max]); + } + }; - setChartData(filterData); - setMinValue(min); - setMaxValue(max); - }, [data, hiveChain]); + if (!displayData || !displayData.length) return null; - const lastHivePrice = chartData?.[chartData.length - 1].close; - const strokeColor = theme === "dark" ? "#FFF" : "#000"; + const primaryAxisId = isRTL ? "right" : "left"; + const secondaryAxisId = isRTL ? "left" : "right"; return ( - - - - - } /> - - - {isFullChart && ( - + {renderCoinButtons()} + + + moment(value).format("MMM D")} + style={{ fontSize: "10px" }} + angle={isMobile ? -90 : 0} + dx={isMobile ? -8 : 0} + dy={isMobile ? 20 : 10} + reversed={isRTL} + /> + { + if (selectedCoinType === "VESTS") { + if (unit === "hp") return `${formatNumber(tick, false, false)}`; + const valueInK = tick / 1_000; + return `${formatNumber(valueInK, true, false).split(".")[0]} K`; + } + return formatNumber(tick, false, false); + }} + /> + + `$${formatNumber(tick, false, false)}`} + /> + } /> + + + {showSavingsBalance === "yes" && selectedCoinType !== "VESTS" && ( + + )} + {!quickView && ( + moment(value).format("MMM D")} + y={250} + x={50} + className="text-xs" + onChange={handleBrushAreaChange} + /> + )} + { + const dataKey = event.dataKey; + const actualDataKey = + (dataKey === selectedCoinType || dataKey === "HP" || dataKey === "VESTS") ? + (selectedCoinType === "VESTS" && unit === "hp" ? "convertedHive" : "balance") : + dataKey; + + const isHidden = hiddenDataKeys.includes(actualDataKey); + if (isHidden) + setHiddenDataKeys(hiddenDataKeys.filter((key) => key !== actualDataKey)); + else + setHiddenDataKeys([...hiddenDataKeys, actualDataKey]); + }} + /> + + + + + {/* Toggle next to legend (if VESTS is selected) */} + {selectedCoinType === "VESTS" && ( +
+ + setUnit(checked ? "hp" : "vests")} /> - )} - - + +
+ )} +
); }; -export default MarketHistoryChart; +export default BalanceHistoryChart; \ No newline at end of file -- GitLab