Skip to content

Commit

Permalink
improve percentage chart (#181)
Browse files Browse the repository at this point in the history
* improve percentage chart

* tweak

* fix layout

* hide card title

* support page

* rm commentor
  • Loading branch information
domechn committed Nov 20, 2023
1 parent e10316a commit 373ad85
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 60 deletions.
19 changes: 6 additions & 13 deletions src/components/historical-data/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,7 +92,7 @@ const App = ({
.catch((e) =>
toast({
description: e.message,
variant: "destructive"
variant: "destructive",
})
)
.finally(() => {
Expand Down Expand Up @@ -120,7 +120,7 @@ const App = ({
.catch((e) =>
toast({
description: e.message,
variant: "destructive"
variant: "destructive",
})
);
}
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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 (
<tr key={d.id}>
<td>
Expand Down Expand Up @@ -403,9 +398,7 @@ const App = ({
</table>
</div>
</Modal>
<div
className="flex justify-center items-center mb-5 text-gray-500 cursor-pointer"
>
<div className="flex justify-center items-center mb-5 text-gray-500 cursor-pointer">
<a
onClick={() => (pageNum > 1 ? setPageNum(pageNum - 1) : null)}
style={{
Expand Down
191 changes: 175 additions & 16 deletions src/components/latest-assets-percentage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,78 @@
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<string>("");
const [dataPage, setDataPage] = useState<number>(0);
const [maxDataPage, setMaxDataPage] = useState<number>(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,
responsive: false,
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: {
Expand All @@ -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 <Doughnut options={options as any} data={lineData()} />;
}

function renderTokenHoldingList() {
return (
<>
<div className="flex w-[100%] h-[50px] justify-between items-center">
<div className="font-bold text-muted-foreground ml-2">
Token holding
</div>
<div className="flex space-x-2 py-4 items-center">
<Button
variant="outline"
size="sm"
onClick={() => setDataPage(Math.max(dataPage - 1, 0))}
disabled={dataPage <= 0}
>
<ChevronLeftIcon />
</Button>
<div className="text-muted-foreground text-sm">
{dataPage + 1} {"/"} {maxDataPage + 1}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setDataPage(Math.min(dataPage + 1, maxDataPage))}
disabled={dataPage >= maxDataPage}
>
<ChevronRightIcon />
</Button>
</div>
</div>
<Separator />
<Table>
<TableBody>
{data
.slice(dataPage * pageSize, (dataPage + 1) * pageSize)
.map((d) => (
<TableRow key={d.coin} className="h-[55px]">
<TableCell>
<div className="flex flex-row items-center">
<img
className="inline-block w-[20px] h-[20px] mr-2 rounded-full"
src={getImageApiPath(appCacheDir, d.coin)}
alt={d.coin}
/>
<div className="mr-1 font-bold text-base">
{prettyPriceNumberToLocaleString(d.amount)}
</div>
<div className="text-gray-600">{d.coin}</div>
</div>
</TableCell>
<TableCell className="text-right">
<div className="text-gray-400">
{currency.symbol +
prettyNumberToLocaleString(
currencyWrapper(currency)(d.value)
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
);
}

return (
<div>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium font-bold">
Percentage of Assets
{/* Percentage of Assets */}
</CardTitle>
</CardHeader>
<CardContent>
<div
style={{
height: Math.max((size.height || 100) / 2, 400),
}}
>
<Doughnut options={options as any} data={lineData()} />
<div className="grid gap-4 grid-cols-2 md:grid-cols-5">
<div
className="col-span-2 md:col-span-3"
style={{
height: 300,
}}
>
{renderDoughnut()}
</div>
<div className="col-span-2 md:col-span-2 flex flex-col items-start justify-top">
{renderTokenHoldingList()}
</div>
</div>
</CardContent>
</Card>
Expand Down
28 changes: 16 additions & 12 deletions src/components/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const App = ({
topCoinsPercentageChangeData,
}: {
currency: CurrencyRateDetail;
pnlData: PNLData,
pnlData: PNLData;
totalValueData: TotalValueData;
latestAssetsPercentageData: LatestAssetsPercentageData;
assetChangeData: AssetChangeData;
Expand All @@ -37,18 +37,22 @@ const App = ({
}) => {
return (
<div className="space-y-2">
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-2">
<TotalValue
currency={currency}
assetChangeData={assetChangeData}
totalValueData={totalValueData}
></TotalValue>
<PNL
currency={currency}
pnlData={pnlData}
></PNL>
<div className="grid gap-4 grid-cols-2">
<div className="col-span-2 md:col-span-1">
<TotalValue
currency={currency}
assetChangeData={assetChangeData}
totalValueData={totalValueData}
></TotalValue>
</div>
<div className="col-span-2 md:col-span-1">
<PNL currency={currency} pnlData={pnlData}></PNL>
</div>
</div>
<LatestAssetsPercentage data={latestAssetsPercentageData} />
<LatestAssetsPercentage
currency={currency}
data={latestAssetsPercentageData}
/>
<CoinsAmountAndValueChange
currency={currency}
data={coinsAmountAndValueChangeData}
Expand Down
30 changes: 12 additions & 18 deletions src/middlelayers/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,35 +306,29 @@ export async function queryAssetChange(size = 10): Promise<AssetChangeData> {

export async function queryLatestAssetsPercentage(): Promise<LatestAssetsPercentageData> {
const size = 1
const backgroundColors = generateRandomColors(11) // top 10 and others

const assets = groupAssetModelsListBySymbol(await queryAssets(size) || [])
if (assets.length === 0) {
return []
}

const latest = assets[0]
const backgroundColors = generateRandomColors(_(latest).size())

const total = _(latest).sumBy("value") + 10 ** -21 // avoid total is 0
const sortedLatest = _(latest).sortBy('value').reverse().value()
const top10 = _(sortedLatest).take(10).value()
const others = _(sortedLatest).drop(10).value()

const res: { coin: string, percentage: number }[] = []

_(top10).forEach(t => {
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,
Expand Down
2 changes: 2 additions & 0 deletions src/middlelayers/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export type AssetChangeData = {

export type LatestAssetsPercentageData = {
coin: string
amount: number
value: number
percentage: number
chartColor: string
}[]
Expand Down
Loading

0 comments on commit 373ad85

Please sign in to comment.