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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"clsx": "^2.1.1",
"ethers": "5.8.0",
"graphql-tag": "^2.12.6",
"lightweight-charts": "^5.0.8",
"next": "14.2.28",
"next-themes": "^0.4.6",
"pino-pretty": "^13.0.0",
Expand All @@ -28,7 +29,6 @@
"react-dom": "^18",
"react-jdenticon": "^1.4.0",
"react-use": "^17.6.0",
"recharts": "^2.15.3",
"viem": "^2.31.4",
"wagmi": "^2.15.6"
},
Expand Down
240 changes: 107 additions & 133 deletions src/app/(homepage)/components/Chart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
"use client";

import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";

import { useTheme } from "next-themes";

import { Card } from "@kleros/ui-components-library";
import clsx from "clsx";
import { format } from "date-fns";
import {
ResponsiveContainer,
LineChart,
Line,
XAxis,
YAxis,
Tooltip,
} from "recharts";
createChart,
LineSeries,
LineStyle,
UTCTimestamp,
} from "lightweight-charts";

import { type IChartData } from "@/hooks/useChartData";

import { shortenName } from "@/utils";

import { IMarket } from "@/consts/markets";

import Legend from "./Legend";
Expand Down Expand Up @@ -57,10 +54,21 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => {
});
};

const accentColor = useMemo(() => {
if (theme === "light") return "#999";
else return "#BECCE5";
}, [theme]);

const gridLinesColor = useMemo(() => {
if (theme === "light") return "#e5e5e5";
else return "#392C74";
}, [theme]);

const chartContainerRef = useRef<HTMLDivElement>(null);

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [series, maxYDomain, marketsData] = useMemo(() => {
if (!data.length) return [[], 0];
let maxYAxis = 0;
const [series, marketsData] = useMemo(() => {
if (!data.length) return [[], {}];
// Combine all market data
const marketsData: MarketsData = {};
data.forEach((marketData) => {
Expand All @@ -69,26 +77,26 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => {
});
});

// Find the earliest timestamp across all markets
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const earliestTimestamp = Math.min(
...Object.values(marketsData).map((market) =>
market.data.length > 0 ? market.data[0].timestamp : Infinity,
),
);

// Find the latest timestamp across all markets
const latestTimestamp = Date.now() / 1000;

// Generate common timestamps for all markets
const timestamps = getTimestamps(1754407213, latestTimestamp);

// Process data for each market
const processedData = timestamps.map((timestamp) => {
const dataPoint: Record<string, number> = { timestamp };
const seriesData: Record<
string,
{ info: IMarket; data: Array<{ time: UTCTimestamp; value: number }> }
> = {};

Object.entries(marketsData).forEach(([marketName, marketInfo]) => {
seriesData[marketName] = { info: marketInfo.market, data: [] };
});

// Process data for each market
timestamps.forEach((timestamp) => {
Object.entries(marketsData).forEach(([marketName, marketInfo]) => {
const { data, market } = marketInfo;

const maxValue = market.maxValue;

// Find valid start index (skip extreme values)
Expand All @@ -104,7 +112,10 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => {
const normalizedData = data.slice(validStartIndex);

if (normalizedData.length === 0) {
dataPoint[marketName] = 0;
seriesData[marketName].data.push({
time: timestamp as UTCTimestamp,
value: 0,
});
return;
}

Expand All @@ -117,22 +128,80 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => {
break;
}
}
if (closestDataPoint.value > maxYAxis)
maxYAxis = closestDataPoint.value;
dataPoint[marketName] = closestDataPoint.value;
});

return dataPoint;
seriesData[marketName].data.push({
time: timestamp as UTCTimestamp,
value: closestDataPoint.value,
});
});
});

maxYAxis = Math.ceil(maxYAxis);
return [processedData, maxYAxis, marketsData];
return [seriesData, marketsData];
}, [data]);

const accentColor = useMemo(() => {
if (theme === "light") return "#999";
else return "#BECCE5";
}, [theme]);
useEffect(() => {
if (!chartContainerRef.current) return;

const handleResize = () => {
chart.applyOptions({ width: chartContainerRef?.current?.clientWidth });
};

const chart = createChart(chartContainerRef?.current, {
layout: {
background: {
color: "transparent",
},
textColor: accentColor,
},
width: chartContainerRef?.current?.clientWidth,
height: 300,
autoSize: true,
rightPriceScale: {
borderVisible: false,
visible: true,
},
leftPriceScale: {
borderVisible: false,
visible: false,
},
timeScale: {
borderVisible: false,
timeVisible: true,
},
grid: {
vertLines: {
color: gridLinesColor,
style: LineStyle.SparseDotted,
},
horzLines: {
color: gridLinesColor,
style: LineStyle.SparseDotted,
},
},
});
chart.timeScale().fitContent();

Object.entries(series).forEach(([marketName, marketData]) => {
if (visibleMarkets.has(marketData.info.name)) {
const series = chart.addSeries(LineSeries, {
color: marketData.info.color,
lineWidth: 2,
title: shortenName(marketName),
});
series.setData(
marketData.data as Array<{ time: UTCTimestamp; value: number }>,
);
}
});

window.addEventListener("resize", handleResize);

return () => {
window.removeEventListener("resize", handleResize);

chart.remove();
};
}, [series, accentColor, visibleMarkets, gridLinesColor]);

return (
<div className="mt-6 flex size-full flex-col">
Expand All @@ -144,102 +213,7 @@ const Chart: React.FC<{ data: IChartData[] }> = ({ data }) => {
<h2 className="text-klerosUIComponentsPrimaryText mt-6 mb-4 text-base font-semibold">
Market Estimate Scores
</h2>
<ResponsiveContainer width="100%" height={200}>
<LineChart
data={series}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
{marketNames
.filter((marketName) => visibleMarkets.has(marketName))
.map((marketName) => (
<Line
key={marketName}
dataKey={marketName}
type="monotone"
stroke={marketsData?.[marketName].market.color ?? "#009AFF"}
// shown when tooltip is visible
activeDot={{ strokeWidth: 0 }}
dot={false}
strokeWidth={1.5}
/>
))}
<XAxis
dataKey="timestamp"
type="number"
scale="time"
tickFormatter={(timestamp) =>
format(new Date(timestamp * 1000), "dd LLL")
}
tick={{ fill: accentColor, fontSize: "12px" }}
domain={["dataMin", "dataMax"]}
axisLine={false}
tickSize={0}
tickMargin={16}
/>
<YAxis
domain={["auto", "auto"]}
axisLine={false}
tickSize={0}
tickMargin={16}
width={40}
tick={{ fill: accentColor, fontSize: "12px" }}
/>
<Tooltip
wrapperStyle={{ zIndex: 10 }}
cursor={{
stroke: accentColor,
strokeDasharray: "4",
strokeWidth: 1,
opacity: 0.5,
}}
content={({ active, payload: payloads, viewBox, label }) => {
const isVisible =
active && payloads && payloads?.length && viewBox?.height;

return isVisible ? (
<Card
className={clsx(
"h-fit w-fit p-2 px-3 shadow-md",
"flex flex-col gap-4",
)}
>
<h3 className="text-klerosUIComponentsPrimaryText text-sm font-semibold">
{new Date(Number(label) * 1000).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
hour: "numeric",
minute: "numeric",
timeZone: "GMT",
},
)}
</h3>
<ul className="flex flex-col gap-2">
{payloads.map(({ color, value, name }, index) => (
<li
className="flex items-center gap-2"
key={`${index}-${name}`}
>
<div
className="size-2 rounded-full"
style={{ backgroundColor: color }}
/>
<p className="text-klerosUIComponentsPrimaryText flex-1 grow text-sm">
{name}
</p>
<p className="text-klerosUIComponentsPrimaryText text-sm font-semibold">
{Number(value).toFixed(2)}
</p>
</li>
))}
</ul>
</Card>
) : null;
}}
/>
</LineChart>
</ResponsiveContainer>
<div ref={chartContainerRef} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from "react";
import { BigNumberField, Tooltip } from "@kleros/ui-components-library";
import { formatUnits } from "viem";

import { formatValue } from "@/utils";
import { formatValue, shortenName } from "@/utils";

interface IProjectAmount {
amount: bigint;
Expand All @@ -12,9 +12,6 @@ interface IProjectAmount {
color: string;
}

const shortenName = (name: string) =>
name.length > 16 ? `${name.slice(0, 12)}...` : name;

const ProjectAmount: React.FC<IProjectAmount> = ({
amount,
balance,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ export function commify(value: string | number): string {

return negative + formatted.join(",") + suffix;
}

export const shortenName = (name: string) =>
name.length > 16 ? `${name.slice(0, 12)}...` : name;
Loading