Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve percentage chart #181

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading