Skip to content
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
190 changes: 190 additions & 0 deletions packages/next-common/components/dotusd/dailyStats.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import { Line } from "react-chartjs-2";
import "next-common/components/charts/globalConfig";
import hoverLinePlugin from "next-common/components/charts/plugins/hoverLine";
import { SecondaryCard } from "next-common/components/styled/containers/secondaryCard";
import Loading from "next-common/components/loading";
import NoData from "next-common/components/noData";
import ValueDisplay from "next-common/components/valueDisplay";
import { useThemeSetting } from "next-common/context/theme";
import { toPrecision, toPrecisionNumber } from "next-common/utils";
import { abbreviateBigNumber } from "next-common/utils/viewfuncs";
import RecordsLink from "./recordsLink";
import useHolderStats from "./useHolderStats";
import useIssuanceStats from "./useIssuanceStats";

dayjs.extend(utc);

// dotUSD is an Asset Hub asset with six decimals.
const DOT_USD_DECIMALS = 6;
const chartHeight = 240;

function buildChartData(points, color, valueOf) {
return {
labels: points.map(({ date }) => dayjs.utc(date).format("MMM D")),
datasets: [
{
data: points.map(valueOf),
borderColor: color,
backgroundColor: color,
borderWidth: 2,
// A long series would otherwise be covered by its own points.
pointRadius: points.length > 31 ? 0 : 2,
pointHoverRadius: 4,
pointBackgroundColor: color,
tension: 0.2,
},
],
};
}

function buildChartOptions(
points,
{ textTertiary, neutral100, neutral300, neutral400, tooltipBg },
{ formatTick, formatTooltip, ticksPrecision },
) {
return {
responsive: true,
maintainAspectRatio: false,
animation: false,
interaction: { mode: "index", intersect: false },
scales: {
x: {
border: { display: false },
grid: { display: false },
ticks: { color: textTertiary, maxRotation: 0, maxTicksLimit: 7 },
},
y: {
border: { display: false },
grid: { color: neutral300, drawTicks: false },
ticks: {
color: textTertiary,
maxTicksLimit: 5,
precision: ticksPrecision,
callback: formatTick,
},
},
},
plugins: {
legend: { display: false },
hoverLine: { lineColor: neutral400, lineWidth: 1 },
tooltip: {
displayColors: false,
backgroundColor: tooltipBg,
titleColor: neutral100,
bodyColor: neutral100,
callbacks: {
title: ([{ dataIndex }]) =>
dayjs.utc(points[dataIndex].date).format("MMM D, YYYY"),
label: ({ parsed }) => formatTooltip(parsed.y),
},
},
},
};
}

// One panel of the trend: the latest value, its description and the line of the metric.
function StatPanel({
points,
isLoading,
colorKey,
valueOf,
value,
label,
action,
formatTick,
formatTooltip,
ticksPrecision,
ariaLabel,
emptyText,
}) {
const theme = useThemeSetting();

return (
<SecondaryCard>
{isLoading ? (
<div className="flex h-[320px] items-center justify-center">
<Loading size={24} />
</div>
) : !points.length ? (
<NoData showIcon={false} text={emptyText} />
) : (
<>
<div className="mb-6">
<div className="text20Bold text-textPrimary">{value}</div>
<div className="flex items-center gap-x-2 text12Medium text-textTertiary">
<span>{label}</span>
{action}
</div>
</div>
<div style={{ height: chartHeight }}>
<Line
data={buildChartData(points, theme[colorKey], valueOf)}
options={buildChartOptions(points, theme, {
formatTick,
formatTooltip,
ticksPrecision,
})}
plugins={[hoverLinePlugin]}
role="img"
aria-label={ariaLabel}
/>
</div>
</>
)}
</SecondaryCard>
);
}

// The two metrics are read from their own apis, so they load and fail independently.
export default function DotUsdStats() {
const holder = useHolderStats();
const issuance = useIssuanceStats();
const latestHolder = holder.items[holder.items.length - 1]?.holderCount;
const latestIssuance =
issuance.items[issuance.items.length - 1]?.totalIssuance;

return (
<div className="grid grid-cols-2 gap-4 max-md:grid-cols-1">
<StatPanel
points={issuance.items}
isLoading={issuance.isLoading}
colorKey="green500"
valueOf={({ totalIssuance }) =>
toPrecisionNumber(totalIssuance, DOT_USD_DECIMALS)
}
value={
latestIssuance === undefined ? (
"—"
) : (
<ValueDisplay
value={toPrecision(latestIssuance, DOT_USD_DECIMALS, 2)}
symbol="dotUSD"
/>
)
}
label="Total issued dotUSD"
formatTick={(value) => abbreviateBigNumber(value, 2)}
formatTooltip={(value) => `${abbreviateBigNumber(value, 2)} dotUSD`}
ariaLabel="dotUSD total issuance since launch"
emptyText="No issuance data available."
/>

<StatPanel
points={holder.items}
isLoading={holder.isLoading}
colorKey="purple500"
valueOf={({ holderCount }) => holderCount}
value={latestHolder ?? "—"}
label="Accounts holding the asset"
action={<RecordsLink tab="holders" tooltip="View holders" />}
formatTick={(value) => value}
ticksPrecision={0}
formatTooltip={(value) => `${value} accounts`}
ariaLabel="dotUSD holders since launch"
emptyText="No holder data available."
/>
</div>
);
}
10 changes: 10 additions & 0 deletions packages/next-common/components/dotusd/holdersTab.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Section } from "./layout";
import DotUsdHolders from "./holders";

export default function DotUsdHoldersTab() {
return (
<Section title="Holders" hint="dotUSD holders ordered by balance">
<DotUsdHolders />
</Section>
);
}
13 changes: 9 additions & 4 deletions packages/next-common/components/dotusd/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import DotUsdAccount from "./account";
import DotUsdInstance from "./instance";
import DotUsdExternalAssets from "./externalAssets";
import DotUsdPool from "./pool";
import DotUsdHolders from "./holders";
import DotUsdStats from "./dailyStats";
import DotUsdHoldersTab from "./holdersTab";
import DotUsdMintRecords from "./mintRecords";
import DotUsdRedeemRecords from "./redeemRecords";

Expand Down Expand Up @@ -37,10 +38,10 @@ function DotUsdOverview() {
</Section>

<Section
title="Holders"
hint="dotUSD holders ordered by balance, as indexed by statescan"
title="Trend"
hint="dotUSD total issuance and holders since launch"
>
<DotUsdHolders />
<DotUsdStats />
</Section>
</div>
);
Expand All @@ -49,6 +50,10 @@ function DotUsdOverview() {
function DotUsdTabContent() {
const { activeTab } = useDotUsdTabs();

if (activeTab === "holders") {
return <DotUsdHoldersTab />;
}

if (activeTab === "mint") {
return <DotUsdMintRecords />;
}
Expand Down
21 changes: 21 additions & 0 deletions packages/next-common/components/dotusd/recordsLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ArrowExternalLink } from "@osn/icons/subsquare";
import Tooltip from "next-common/components/tooltip";
import { useDotUsdTabs } from "./tabs";

// Switches to the tab of the list the number is summed from.
export default function RecordsLink({ tab, tooltip }) {
const { setActiveTab } = useDotUsdTabs();

return (
<Tooltip className="inline-flex" content={tooltip}>
<button
type="button"
aria-label={tooltip}
onClick={() => setActiveTab(tab)}
className="inline-flex items-center text-textTertiary hover:text-textSecondary"
>
<ArrowExternalLink className="w-3.5 h-3.5" />
</button>
</Tooltip>
);
}
21 changes: 1 addition & 20 deletions packages/next-common/components/dotusd/summary.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ArrowExternalLink } from "@osn/icons/subsquare";
import BigNumber from "bignumber.js";
import { isNil } from "lodash-es";
import { toPrecision } from "next-common/utils";
Expand All @@ -10,7 +9,7 @@ import ValueDisplay from "next-common/components/valueDisplay";
import Tooltip from "next-common/components/tooltip";
import { useDotUsdContext } from "./context";
import usePsmStats from "./usePsmStats";
import { useDotUsdTabs } from "./tabs";
import RecordsLink from "./recordsLink";

function EmptyValue() {
return <span className="text-textTertiary">—</span>;
Expand Down Expand Up @@ -57,24 +56,6 @@ function SignedAmountValue({ value, symbol, decimals }) {
);
}

// Switches to the records list the flow number is summed from.
function RecordsLink({ tab, tooltip }) {
const { setActiveTab } = useDotUsdTabs();

return (
<Tooltip className="inline-flex" content={tooltip}>
<button
type="button"
aria-label={tooltip}
onClick={() => setActiveTab(tab)}
className="inline-flex items-center text-textTertiary hover:text-textSecondary"
>
<ArrowExternalLink className="w-3.5 h-3.5" />
</button>
</Tooltip>
);
}

function DebtUsageBar({ ratio, maxDebt }) {
const percent = BigNumber.min(BigNumber.max(ratio, 0), 100).toNumber();

Expand Down
1 change: 1 addition & 0 deletions packages/next-common/components/dotusd/tabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createContext, useContext, useState } from "react";

export const DOT_USD_TABS = [
{ value: "overview", label: "Overview" },
{ value: "holders", label: "Holders" },
{ value: "mint", label: "Mint records" },
{ value: "redeem", label: "Redeem records" },
];
Expand Down
50 changes: 50 additions & 0 deletions packages/next-common/components/dotusd/useDailyStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import nextApi from "next-common/services/nextApi";

/**
* The daily snapshots of one dotUSD metric served by the backend, oldest first. `days` is
* forwarded to the api when set, and the api decides how much history it returns otherwise.
*/
export default function useDailyStats(path, days) {
const [state, setState] = useState({ items: [], isLoading: true });

useEffect(() => {
let isMounted = true;
// Keep the previous points on screen while the next response loads.
setState((previous) => ({ ...previous, isLoading: true }));

const params = days === undefined ? {} : { days };

nextApi
.fetch(path, params)
.then(({ result, error }) => {
if (!isMounted) {
return;
}

if (error) {
console.error(`Failed to fetch ${path}`, error);
setState({ items: [], isLoading: false });
return;
}

setState({
items: Array.isArray(result) ? result : [],
isLoading: false,
});
})
.catch((error) => {
console.error(`Failed to fetch ${path}`, error);

if (isMounted) {
setState({ items: [], isLoading: false });
}
});

return () => {
isMounted = false;
};
}, [path, days]);

return state;
}
6 changes: 6 additions & 0 deletions packages/next-common/components/dotusd/useHolderStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import useDailyStats from "./useDailyStats";

// No days argument: the api returns the whole history since the launch.
export default function useHolderStats() {
return useDailyStats("dotusd/holder-stats");
}
6 changes: 6 additions & 0 deletions packages/next-common/components/dotusd/useIssuanceStats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import useDailyStats from "./useDailyStats";

// No days argument: the api returns the whole history since the launch.
export default function useIssuanceStats() {
return useDailyStats("dotusd/issuance-stats");
}
Loading