From c3d4d1fe1fecc8fcaac2c5af75b22257d270ceb8 Mon Sep 17 00:00:00 2001 From: Liam Date: Mon, 21 Oct 2024 06:47:53 +0100 Subject: [PATCH] add leaderboard pp chart --- .../src/components/chart/generic-chart.tsx | 59 ++++++++++--------- .../leaderboard/leaderboard-data.tsx | 9 ++- .../leaderboard/leaderboard-info.tsx | 2 +- .../leaderboard/leaderboard-pp-chart.tsx | 53 +++++++++++++++++ 4 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 projects/website/src/components/leaderboard/leaderboard-pp-chart.tsx diff --git a/projects/website/src/components/chart/generic-chart.tsx b/projects/website/src/components/chart/generic-chart.tsx index 2ec0816..cb6ca66 100644 --- a/projects/website/src/components/chart/generic-chart.tsx +++ b/projects/website/src/components/chart/generic-chart.tsx @@ -4,7 +4,7 @@ import { Chart, registerables } from "chart.js"; import { Line } from "react-chartjs-2"; import { useIsMobile } from "@/hooks/use-is-mobile"; -import { formatDateMinimal, getDaysAgo, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils"; +import { formatDateMinimal, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils"; Chart.register(...registerables); @@ -16,7 +16,7 @@ export type Axis = { position?: AxisPosition; display?: boolean; grid?: { color?: string; drawOnChartArea?: boolean }; - title?: { display: boolean; text: string; color?: string }; + title?: { display: boolean; text?: string; color?: string }; ticks?: { stepSize?: number; callback?: (value: number, index: number, values: any) => string; @@ -34,6 +34,7 @@ export type Dataset = { lineTension: number; spanGaps: boolean; yAxisID: string; + hidden?: boolean; type?: DatasetDisplayType; }; @@ -46,16 +47,17 @@ export type DatasetConfig = { reverse: boolean; display: boolean; hideOnMobile?: boolean; - displayName: string; + displayName?: string; position: AxisPosition; - valueFormatter?: (value: number) => string; // Added precision option here + valueFormatter?: (value: number) => string; }; type?: DatasetDisplayType; labelFormatter: (value: number) => string; + showLegend?: boolean; }; export type ChartProps = { - labels: Date[]; + labels: Date[] | string[]; datasetConfig: DatasetConfig[]; histories: Record; }; @@ -65,7 +67,7 @@ const generateAxis = ( reverse: boolean, display: boolean, position: AxisPosition, - displayName: string, + displayName?: string, valueFormatter?: (value: number) => string ): Axis => ({ id, @@ -88,6 +90,7 @@ const generateDataset = ( data: (number | null)[], borderColor: string, yAxisID: string, + showLegend: boolean = true, type?: DatasetDisplayType ): Dataset => ({ label, @@ -97,6 +100,7 @@ const generateDataset = ( lineTension: 0.5, spanGaps: false, yAxisID, + hidden: !showLegend, // Use hidden to disable legend type, ...(type === "bar" && { backgroundColor: borderColor, @@ -112,7 +116,6 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart reverse: false, ticks: { font: (context: any) => { - // Make the first of the month bold if (parseDate(context.tick.label).getDate() === 1) { return { weight: "bold", @@ -143,7 +146,14 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart config.axisConfig.valueFormatter ); - return generateDataset(config.title, historyArray, config.color, config.axisId, config.type || "line"); + return generateDataset( + config.title, + historyArray, + config.color, + config.axisId, + config.showLegend !== false, // Respect showLegend property + config.type || "line" + ); } return null; @@ -157,27 +167,15 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart scales: axes, elements: { point: { radius: 0 } }, plugins: { - legend: { position: "top", labels: { color: "white" } }, - tooltip: { - callbacks: { - title(context: any) { - const date = labels[context[0].dataIndex]; - const differenceInDays = getDaysAgo(date); - let formattedDate: string; - if (differenceInDays === 0) { - 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) ?? ""; + legend: { + position: "top", + labels: { + color: "white", + // Filter out datasets where showLegend is false + filter: (legendItem: any, chartData: any) => { + // Access showLegend from chartData.datasets + const dataset = chartData.datasets[legendItem.datasetIndex]; + return dataset.showLegend !== false; // Only show if showLegend is not false }, }, }, @@ -185,6 +183,9 @@ export default function GenericChart({ labels, datasetConfig, histories }: Chart }; const formattedLabels = labels.map(date => { + if (typeof date === "string") { + return date; + } const formattedDate = formatDateMinimal(date); if (formatDateMinimal(getDaysAgoDate(0)) === formattedDate) { return "Now"; diff --git a/projects/website/src/components/leaderboard/leaderboard-data.tsx b/projects/website/src/components/leaderboard/leaderboard-data.tsx index 152b8fc..360886b 100644 --- a/projects/website/src/components/leaderboard/leaderboard-data.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-data.tsx @@ -9,6 +9,7 @@ import { useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; +import LeaderboardPpChart from "@/components/leaderboard/leaderboard-pp-chart"; const REFRESH_INTERVAL = 1000 * 60 * 5; @@ -48,17 +49,21 @@ export function LeaderboardData({ initialLeaderboard, initialScores, initialPage } }, [data]); + const leaderboard = currentLeaderboard.leaderboard; return (
setCurrentLeaderboardId(newId)} showDifficulties isLeaderboardPage /> - +
+ + {leaderboard.stars > 0 && } +
); } diff --git a/projects/website/src/components/leaderboard/leaderboard-info.tsx b/projects/website/src/components/leaderboard/leaderboard-info.tsx index fe32e24..1c64948 100644 --- a/projects/website/src/components/leaderboard/leaderboard-info.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-info.tsx @@ -21,7 +21,7 @@ type LeaderboardInfoProps = { export function LeaderboardInfo({ leaderboard, beatSaverMap }: LeaderboardInfoProps) { return ( - +
{/* Song Info */} diff --git a/projects/website/src/components/leaderboard/leaderboard-pp-chart.tsx b/projects/website/src/components/leaderboard/leaderboard-pp-chart.tsx new file mode 100644 index 0000000..bfed290 --- /dev/null +++ b/projects/website/src/components/leaderboard/leaderboard-pp-chart.tsx @@ -0,0 +1,53 @@ +"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 = {}; + 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 ( + +

PP Curve

+ +
+ ); +}