From 373ad85a31bcd834ad31831916bfaa6105514417 Mon Sep 17 00:00:00 2001 From: domchan <31119455+domechn@users.noreply.github.com> Date: Mon, 20 Nov 2023 14:46:27 -0600 Subject: [PATCH] improve percentage chart (#181) * improve percentage chart * tweak * fix layout * hide card title * support page * rm commentor --- src/components/historical-data/index.tsx | 19 +- src/components/latest-assets-percentage.tsx | 191 ++++++++++++++++++-- src/components/overview.tsx | 28 +-- src/middlelayers/charts.ts | 30 ++- src/middlelayers/types.d.ts | 2 + src/utils/app.ts | 9 +- 6 files changed, 219 insertions(+), 60 deletions(-) diff --git a/src/components/historical-data/index.tsx b/src/components/historical-data/index.tsx index 05406ea..51ac7a8 100644 --- a/src/components/historical-data/index.tsx +++ b/src/components/historical-data/index.tsx @@ -19,9 +19,9 @@ import { import Modal from "../common/modal"; import { downloadCoinLogos } from "@/middlelayers/data"; import { appCacheDir as getAppCacheDir } from "@tauri-apps/api/path"; -import { convertFileSrc } from "@tauri-apps/api/tauri"; import { useWindowSize } from "@/utils/hook"; import ImageStack from "../common/image-stack"; +import { getImageApiPath } from "@/utils/app"; type RankData = { id: number; @@ -92,7 +92,7 @@ const App = ({ .catch((e) => toast({ description: e.message, - variant: "destructive" + variant: "destructive", }) ) .finally(() => { @@ -120,7 +120,7 @@ const App = ({ .catch((e) => toast({ description: e.message, - variant: "destructive" + variant: "destructive", }) ); } @@ -208,7 +208,7 @@ const App = ({ .sortBy("value") .reverse() .take(7) - .map((a) => getImageApiPath(a.symbol)) + .map((a) => getImageApiPath(appCacheDir, a.symbol)) .value()} imageWidth={25} imageHeight={25} @@ -260,15 +260,10 @@ const App = ({ .value(); } - function getImageApiPath(symbol: string) { - const filePath = `${appCacheDir}assets/coins/${symbol.toLowerCase()}.png`; - return convertFileSrc(filePath); - } - function renderDetailPage(data: RankData[]) { return _(data) .map((d) => { - const apiPath = getImageApiPath(d.symbol); + const apiPath = getImageApiPath(appCacheDir, d.symbol); return ( @@ -403,9 +398,7 @@ const App = ({ -
+
(pageNum > 1 ? setPageNum(pageNum - 1) : null)} style={{ diff --git a/src/components/latest-assets-percentage.tsx b/src/components/latest-assets-percentage.tsx index 511feec..1d396fc 100644 --- a/src/components/latest-assets-percentage.tsx +++ b/src/components/latest-assets-percentage.tsx @@ -1,10 +1,63 @@ import { Doughnut } from "react-chartjs-2"; -import { useWindowSize } from "@/utils/hook"; -import { LatestAssetsPercentageData } from "@/middlelayers/types"; +import { + CurrencyRateDetail, + LatestAssetsPercentageData, +} from "@/middlelayers/types"; import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"; +import _ from "lodash"; +import { useEffect, useState } from "react"; +import { appCacheDir as getAppCacheDir } from "@tauri-apps/api/path"; +import { Table, TableBody, TableCell, TableRow } from "./ui/table"; +import { getImageApiPath } from "@/utils/app"; +import { + currencyWrapper, + prettyNumberToLocaleString, + prettyPriceNumberToLocaleString, +} from "@/utils/currency"; +import { downloadCoinLogos } from "@/middlelayers/data"; +import { Button } from "./ui/button"; +import { Separator } from "./ui/separator"; +import { + ArrowLeftIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "@radix-ui/react-icons"; -const App = ({ data }: { data: LatestAssetsPercentageData }) => { - const size = useWindowSize(); +const App = ({ + currency, + data, +}: { + currency: CurrencyRateDetail; + data: LatestAssetsPercentageData; +}) => { + const [appCacheDir, setAppCacheDir] = useState(""); + const [dataPage, setDataPage] = useState(0); + const [maxDataPage, setMaxDataPage] = useState(0); + const pageSize = 5; + + useEffect(() => { + getAppCacheDir().then((dir) => { + setAppCacheDir(dir); + }); + }, []); + + const [percentageData, setPercentageData] = useState< + { + coin: string; + percentage: number; + chartColor: string; + }[] + >(data); + + useEffect(() => { + setPercentageData(splitTopAndOtherData(data)); + + // download coin logos + downloadCoinLogos(_(data).map("coin").value()); + + // set max data page + setMaxDataPage(Math.floor(data.length / pageSize)); + }, [data]); const options = { maintainAspectRatio: false, @@ -12,7 +65,14 @@ const App = ({ data }: { data: LatestAssetsPercentageData }) => { plugins: { // text is set for resizing title: { display: false, text: "Percentage of Assets" }, - legend: { labels: { font: {} } }, + legend: { + display: true, + position: "right", + font: { + size: 14, + }, + labels: { font: {} }, + }, datalabels: { color: "white", font: { @@ -33,35 +93,134 @@ const App = ({ data }: { data: LatestAssetsPercentageData }) => { }, }; + function splitTopAndOtherData(d: LatestAssetsPercentageData) { + const count = 5; + if (d.length <= count) { + return d; + } + const top = _(d).sortBy("percentage").reverse().take(count).value(); + console.log(top); + const other = _(d).sortBy("percentage").reverse().drop(count).value(); + + return _([ + ...top, + other + ? { + coin: "Other", + percentage: _(other).map("percentage").sum(), + chartColor: other[0]?.chartColor ?? "#4B5563", + } + : null, + ]) + .compact() + .value(); + } + function lineData() { + const d = percentageData; return { - labels: data.map((coin) => coin.coin), + labels: d.map((coin) => coin.coin), datasets: [ { - data: data.map((coin) => coin.percentage), - borderColor: data.map((coin) => coin.chartColor), - backgroundColor: data.map((coin) => coin.chartColor), + data: d.map((coin) => coin.percentage), + borderColor: d.map((coin) => coin.chartColor), + backgroundColor: d.map((coin) => coin.chartColor), borderWidth: 1, }, ], }; } + function renderDoughnut() { + return ; + } + + function renderTokenHoldingList() { + return ( + <> +
+
+ Token holding +
+
+ +
+ {dataPage + 1} {"/"} {maxDataPage + 1} +
+ +
+
+ + + + {data + .slice(dataPage * pageSize, (dataPage + 1) * pageSize) + .map((d) => ( + + +
+ {d.coin} +
+ {prettyPriceNumberToLocaleString(d.amount)} +
+
{d.coin}
+
+
+ +
+ {currency.symbol + + prettyNumberToLocaleString( + currencyWrapper(currency)(d.value) + )} +
+
+
+ ))} +
+
+ + ); + } + return (
- Percentage of Assets + {/* Percentage of Assets */} -
- +
+
+ {renderDoughnut()} +
+
+ {renderTokenHoldingList()} +
diff --git a/src/components/overview.tsx b/src/components/overview.tsx index 6d4483b..b922eee 100644 --- a/src/components/overview.tsx +++ b/src/components/overview.tsx @@ -27,7 +27,7 @@ const App = ({ topCoinsPercentageChangeData, }: { currency: CurrencyRateDetail; - pnlData: PNLData, + pnlData: PNLData; totalValueData: TotalValueData; latestAssetsPercentageData: LatestAssetsPercentageData; assetChangeData: AssetChangeData; @@ -37,18 +37,22 @@ const App = ({ }) => { return (
-
- - +
+
+ +
+
+ +
- + { export async function queryLatestAssetsPercentage(): Promise { const size = 1 - const backgroundColors = generateRandomColors(11) // top 10 and others const assets = groupAssetModelsListBySymbol(await queryAssets(size) || []) if (assets.length === 0) { @@ -314,27 +313,22 @@ export async function queryLatestAssetsPercentage(): Promise { - res.push({ - coin: t.symbol, - percentage: t.value / total * 100, - }) - }) + const res: { + coin: string, + percentage: number, + amount: number, + value: number, + }[] = _(latest).map(t => ({ + coin: t.symbol, + amount: t.amount, + value: t.value, + percentage: t.value / total * 100, - if (others.length > 0) { - res.push({ - coin: 'Others', - percentage: _(others).sumBy('value') / total * 100, - }) - } + })).value() return _(res).sortBy('percentage').reverse().map((v, idx) => ({ ...v, diff --git a/src/middlelayers/types.d.ts b/src/middlelayers/types.d.ts index 4602da1..40bed98 100644 --- a/src/middlelayers/types.d.ts +++ b/src/middlelayers/types.d.ts @@ -99,6 +99,8 @@ export type AssetChangeData = { export type LatestAssetsPercentageData = { coin: string + amount: number + value: number percentage: number chartColor: string }[] diff --git a/src/utils/app.ts b/src/utils/app.ts index fcab9f8..976a05e 100644 --- a/src/utils/app.ts +++ b/src/utils/app.ts @@ -1,6 +1,8 @@ import * as api from '@tauri-apps/api' import { getClientIDConfiguration } from '../middlelayers/configuration' import { trackEvent } from '@aptabase/tauri' +import { appCacheDir } from "@tauri-apps/api/path" +import { convertFileSrc } from "@tauri-apps/api/tauri" export async function getVersion() { return api.app.getVersion() @@ -21,4 +23,9 @@ export async function trackEventWithClientID(event: string, props?: { [k: string } catch (e) { console.error("track event failed", e) } -} \ No newline at end of file +} + +export function getImageApiPath(cacheDir: string, symbol: string) { + const filePath = `${cacheDir}assets/coins/${symbol.toLowerCase()}.png` + return convertFileSrc(filePath) +}