Compare commits

..

No commits in common. "577fcb0e0d7323ef1257f52cfe8e53104e266362" and "a15893ea5632383b62e0006e5a977651f1733d60" have entirely different histories.

4 changed files with 32 additions and 91 deletions

@ -4,7 +4,7 @@
import { Chart, registerables } from "chart.js"; import { Chart, registerables } from "chart.js";
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import { useIsMobile } from "@/hooks/use-is-mobile"; import { useIsMobile } from "@/hooks/use-is-mobile";
import { formatDateMinimal, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils"; import { formatDateMinimal, getDaysAgo, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils";
Chart.register(...registerables); Chart.register(...registerables);
@ -16,7 +16,7 @@ export type Axis = {
position?: AxisPosition; position?: AxisPosition;
display?: boolean; display?: boolean;
grid?: { color?: string; drawOnChartArea?: boolean }; grid?: { color?: string; drawOnChartArea?: boolean };
title?: { display: boolean; text?: string; color?: string }; title?: { display: boolean; text: string; color?: string };
ticks?: { ticks?: {
stepSize?: number; stepSize?: number;
callback?: (value: number, index: number, values: any) => string; callback?: (value: number, index: number, values: any) => string;
@ -34,7 +34,6 @@ export type Dataset = {
lineTension: number; lineTension: number;
spanGaps: boolean; spanGaps: boolean;
yAxisID: string; yAxisID: string;
hidden?: boolean;
type?: DatasetDisplayType; type?: DatasetDisplayType;
}; };
@ -47,17 +46,16 @@ export type DatasetConfig = {
reverse: boolean; reverse: boolean;
display: boolean; display: boolean;
hideOnMobile?: boolean; hideOnMobile?: boolean;
displayName?: string; displayName: string;
position: AxisPosition; position: AxisPosition;
valueFormatter?: (value: number) => string; valueFormatter?: (value: number) => string; // Added precision option here
}; };
type?: DatasetDisplayType; type?: DatasetDisplayType;
labelFormatter: (value: number) => string; labelFormatter: (value: number) => string;
showLegend?: boolean;
}; };
export type ChartProps = { export type ChartProps = {
labels: Date[] | string[]; labels: Date[];
datasetConfig: DatasetConfig[]; datasetConfig: DatasetConfig[];
histories: Record<string, (number | null)[]>; histories: Record<string, (number | null)[]>;
}; };
@ -67,7 +65,7 @@ const generateAxis = (
reverse: boolean, reverse: boolean,
display: boolean, display: boolean,
position: AxisPosition, position: AxisPosition,
displayName?: string, displayName: string,
valueFormatter?: (value: number) => string valueFormatter?: (value: number) => string
): Axis => ({ ): Axis => ({
id, id,
@ -90,7 +88,6 @@ const generateDataset = (
data: (number | null)[], data: (number | null)[],
borderColor: string, borderColor: string,
yAxisID: string, yAxisID: string,
showLegend: boolean = true,
type?: DatasetDisplayType type?: DatasetDisplayType
): Dataset => ({ ): Dataset => ({
label, label,
@ -100,7 +97,6 @@ const generateDataset = (
lineTension: 0.5, lineTension: 0.5,
spanGaps: false, spanGaps: false,
yAxisID, yAxisID,
hidden: !showLegend, // Use hidden to disable legend
type, type,
...(type === "bar" && { ...(type === "bar" && {
backgroundColor: borderColor, backgroundColor: borderColor,
@ -116,6 +112,7 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart
reverse: false, reverse: false,
ticks: { ticks: {
font: (context: any) => { font: (context: any) => {
// Make the first of the month bold
if (parseDate(context.tick.label).getDate() === 1) { if (parseDate(context.tick.label).getDate() === 1) {
return { return {
weight: "bold", weight: "bold",
@ -146,14 +143,7 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart
config.axisConfig.valueFormatter config.axisConfig.valueFormatter
); );
return generateDataset( return generateDataset(config.title, historyArray, config.color, config.axisId, config.type || "line");
config.title,
historyArray,
config.color,
config.axisId,
config.showLegend !== false, // Respect showLegend property
config.type || "line"
);
} }
return null; return null;
@ -167,15 +157,27 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart
scales: axes, scales: axes,
elements: { point: { radius: 0 } }, elements: { point: { radius: 0 } },
plugins: { plugins: {
legend: { legend: { position: "top", labels: { color: "white" } },
position: "top", tooltip: {
labels: { callbacks: {
color: "white", title(context: any) {
// Filter out datasets where showLegend is false const date = labels[context[0].dataIndex];
filter: (legendItem: any, chartData: any) => { const differenceInDays = getDaysAgo(date);
// Access showLegend from chartData.datasets let formattedDate: string;
const dataset = chartData.datasets[legendItem.datasetIndex]; if (differenceInDays === 0) {
return dataset.showLegend !== false; // Only show if showLegend is not false formattedDate = "Now";
} else if (differenceInDays === 1) {
formattedDate = "Yesterday";
} else {
formattedDate = formatDateMinimal(date);
}
return `${formattedDate} ${differenceInDays > 0 ? `(${differenceInDays} day${differenceInDays > 1 ? "s" : ""} ago)` : ""}`;
},
label(context: any) {
const value = Number(context.parsed.y);
const config = datasetConfig.find(cfg => cfg.title === context.dataset.label);
return config?.labelFormatter(value) ?? "";
}, },
}, },
}, },
@ -183,9 +185,6 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart
}; };
const formattedLabels = labels.map(date => { const formattedLabels = labels.map(date => {
if (typeof date === "string") {
return date;
}
const formattedDate = formatDateMinimal(date); const formattedDate = formatDateMinimal(date);
if (formatDateMinimal(getDaysAgoDate(0)) === formattedDate) { if (formatDateMinimal(getDaysAgoDate(0)) === formattedDate) {
return "Now"; return "Now";

@ -9,7 +9,6 @@ import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util";
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response";
import LeaderboardPpChart from "@/components/leaderboard/leaderboard-pp-chart";
const REFRESH_INTERVAL = 1000 * 60 * 5; const REFRESH_INTERVAL = 1000 * 60 * 5;
@ -49,21 +48,17 @@ export function LeaderboardData({ initialLeaderboard, initialScores, initialPage
} }
}, [data]); }, [data]);
const leaderboard = currentLeaderboard.leaderboard;
return ( return (
<main className="flex flex-col-reverse xl:flex-row w-full gap-2"> <main className="flex flex-col-reverse xl:flex-row w-full gap-2">
<LeaderboardScores <LeaderboardScores
leaderboard={leaderboard} leaderboard={currentLeaderboard.leaderboard}
initialScores={initialScores} initialScores={initialScores}
initialPage={initialPage} initialPage={initialPage}
leaderboardChanged={newId => setCurrentLeaderboardId(newId)} leaderboardChanged={newId => setCurrentLeaderboardId(newId)}
showDifficulties showDifficulties
isLeaderboardPage isLeaderboardPage
/> />
<div className="flex flex-col gap-2 w-full xl:w-[500px]"> <LeaderboardInfo leaderboard={currentLeaderboard.leaderboard} beatSaverMap={currentLeaderboard.beatsaver} />
<LeaderboardInfo leaderboard={leaderboard} beatSaverMap={currentLeaderboard.beatsaver} />
{leaderboard.stars > 0 && <LeaderboardPpChart leaderboard={leaderboard} />}
</div>
</main> </main>
); );
} }

@ -21,7 +21,7 @@ type LeaderboardInfoProps = {
export function LeaderboardInfo({ leaderboard, beatSaverMap }: LeaderboardInfoProps) { export function LeaderboardInfo({ leaderboard, beatSaverMap }: LeaderboardInfoProps) {
return ( return (
<Card className="w-full h-fit"> <Card className="xl:w-[500px] h-fit w-full">
<div className="flex flex-row justify-between w-full"> <div className="flex flex-row justify-between w-full">
<div className="flex flex-col justify-between w-full min-h-[160px]"> <div className="flex flex-col justify-between w-full min-h-[160px]">
{/* Song Info */} {/* Song Info */}

@ -1,53 +0,0 @@
"use client";
import React from "react";
import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import Card from "@/components/card";
type Props = {
/**
* The player the chart is for
*/
leaderboard: ScoreSaberLeaderboard;
};
export default function LeaderboardPpChart({ leaderboard }: Props) {
const histories: Record<string, (number | null)[]> = {};
const labels: string[] = [];
for (let accuracy = 60; accuracy <= 100; accuracy += 0.2) {
const label = accuracy.toFixed(2) + "%";
labels.push(label);
const history = histories["pp"];
if (!history) {
histories["pp"] = [];
}
histories["pp"].push(scoresaberService.getPp(leaderboard.stars, accuracy));
}
const datasetConfig: DatasetConfig[] = [
{
title: "PP",
field: "pp",
color: "#3EC1D3",
axisId: "y",
axisConfig: {
reverse: false,
display: true,
displayName: "PP",
position: "left",
},
labelFormatter: (value: number) => `${value.toFixed(2)}pp`,
},
];
return (
<Card className="h-64 w-full">
<p className="font-semibold">PP Curve</p>
<GenericChart labels={labels} datasetConfig={datasetConfig} histories={histories} />
</Card>
);
}