fix mobile support for chart and make the chart more flexible (easier to add more stats)
Some checks failed
Deploy / deploy (push) Failing after 2m46s

This commit is contained in:
Lee 2024-09-30 14:10:38 +01:00
parent 9097d254f1
commit 342dbefac7
2 changed files with 152 additions and 84 deletions

@ -1,16 +1,21 @@
export interface PlayerHistory { export interface PlayerHistory {
/**
* An object with the player's statistics
*/
[key: string]: number | null;
/** /**
* The player's rank. * The player's rank.
*/ */
rank?: number; rank: number;
/** /**
* The player's country rank. * The player's country rank.
*/ */
countryRank?: number; countryRank: number;
/** /**
* The pp of the player. * The pp of the player.
*/ */
pp?: number; pp: number;
} }

@ -15,6 +15,7 @@ import {
import { Line } from "react-chartjs-2"; import { Line } from "react-chartjs-2";
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player"; import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
import { getDaysAgo, parseDate } from "@/common/time-utils"; import { getDaysAgo, parseDate } from "@/common/time-utils";
import { useIsMobile } from "@/hooks/use-is-mobile";
Chart.register( Chart.register(
LinearScale, LinearScale,
@ -26,19 +27,21 @@ Chart.register(
Legend, Legend,
); );
type AxisPosition = "left" | "right";
/** /**
* A ChartJS axis * A ChartJS axis
*/ */
type Axis = { type Axis = {
id: string; id?: string;
position: "left" | "right"; 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;
}; };
reverse: boolean; reverse?: boolean;
}; };
/** /**
@ -67,7 +70,7 @@ const generateAxis = (
id: string, id: string,
reverse: boolean, reverse: boolean,
display: boolean, display: boolean,
position: "right" | "left", position: AxisPosition,
displayName: string, displayName: string,
): Axis => ({ ): Axis => ({
id, id,
@ -85,22 +88,7 @@ const generateAxis = (
ticks: { ticks: {
stepSize: 10, stepSize: 10,
}, },
reverse: reverse, reverse,
});
/**
* Create the axes
*/
const createAxes = () => ({
x: {
grid: {
color: "#252525", // gray grid lines
},
reverse: false,
},
y: generateAxis("y", true, true, "left", "Global Rank"), // Rank axis with display name
y1: generateAxis("y1", true, false, "left", "Country Rank"), // Country Rank axis with display name
y2: generateAxis("y2", false, true, "right", "PP"), // PP axis with display name
}); });
/** /**
@ -126,53 +114,73 @@ const generateDataset = (
yAxisID, yAxisID,
}); });
export const options: any = { type DatasetConfig = {
maintainAspectRatio: false, title: string;
aspectRatio: 1, field: string;
interaction: { color: string;
mode: "index", axisId: string;
intersect: false, axisConfig: {
}, reverse: boolean;
scales: createAxes(), // Use createAxes to configure axes display: boolean;
elements: { displayName: string;
point: { position: AxisPosition;
radius: 0, };
}, labelFormatter: (value: number) => string;
},
plugins: {
legend: {
position: "top" as const,
labels: {
color: "white",
},
},
tooltip: {
callbacks: {
label(context: any) {
switch (context.dataset.label) {
case "Rank": {
return `Rank #${formatNumberWithCommas(Number(context.parsed.y))}`;
}
case "Country Rank": {
return `Country Rank #${formatNumberWithCommas(Number(context.parsed.y))}`;
}
case "PP": {
return `PP ${formatNumberWithCommas(Number(context.parsed.y))}pp`;
}
}
},
},
},
},
}; };
// Configuration array for datasets and axes with label formatters
const datasetConfig: DatasetConfig[] = [
{
title: "Rank",
field: "rank",
color: "#3EC1D3",
axisId: "y",
axisConfig: {
reverse: true,
display: true,
displayName: "Global Rank",
position: "left",
},
labelFormatter: (value: number) => `Rank #${formatNumberWithCommas(value)}`,
},
{
title: "Country Rank",
field: "countryRank",
color: "#FFEA00",
axisId: "y1",
axisConfig: {
reverse: true,
display: false,
displayName: "Country Rank",
position: "left",
},
labelFormatter: (value: number) =>
`Country Rank #${formatNumberWithCommas(value)}`,
},
{
title: "PP",
field: "pp",
color: "#606fff",
axisId: "y2",
axisConfig: {
reverse: false,
display: true,
displayName: "PP",
position: "right",
},
labelFormatter: (value: number) => `PP ${formatNumberWithCommas(value)}pp`,
},
];
type Props = { type Props = {
player: ScoreSaberPlayer; player: ScoreSaberPlayer;
}; };
export default function PlayerRankChart({ player }: Props) { export default function PlayerRankChart({ player }: Props) {
const isMobile = useIsMobile();
if ( if (
player.statisticHistory === undefined || !player.statisticHistory ||
Object.keys(player.statisticHistory).length === 0 Object.keys(player.statisticHistory).length === 0
) { ) {
return ( return (
@ -183,7 +191,7 @@ export default function PlayerRankChart({ player }: Props) {
} }
const labels: string[] = []; const labels: string[] = [];
const histories: Record<"rank" | "countryRank" | "pp", (number | null)[]> = { const histories: Record<string, (number | null)[]> = {
rank: [], rank: [],
countryRank: [], countryRank: [],
pp: [], pp: [],
@ -208,35 +216,90 @@ export default function PlayerRankChart({ player }: Props) {
for (let i = 1; i < diffDays; i++) { for (let i = 1; i < diffDays; i++) {
labels.push( labels.push(
`${getDaysAgo(new Date(currentDate.getTime() - i * 24 * 60 * 60 * 1000))} days ago`, `${getDaysAgo(
new Date(currentDate.getTime() - i * 24 * 60 * 60 * 1000),
)} days ago`,
); );
histories.rank.push(null); datasetConfig.forEach((config) => {
histories.countryRank.push(null); histories[config.field].push(null);
histories.pp.push(null); });
} }
} }
const daysAgo = getDaysAgo(currentDate); const daysAgo = getDaysAgo(currentDate);
if (daysAgo === 0) { labels.push(daysAgo === 0 ? "Today" : `${daysAgo} days ago`);
labels.push("Today");
} else if (daysAgo === 1) {
labels.push("Yesterday");
} else {
labels.push(`${daysAgo} days ago`);
}
histories.rank.push(history.rank ?? null); datasetConfig.forEach((config) => {
histories.countryRank.push(history.countryRank ?? null); histories[config.field].push(history[config.field] ?? null);
histories.pp.push(history.pp ?? null); });
previousDate = currentDate; previousDate = currentDate;
} }
const datasets: Dataset[] = [ // Dynamically create axes and datasets based on datasetConfig
generateDataset("Rank", histories["rank"], "#3EC1D3", "y"), const axes: Record<string, Axis> = {
generateDataset("Country Rank", histories["countryRank"], "#FFEA00", "y1"), x: {
generateDataset("PP", histories["pp"], "#606fff", "y2"), grid: {
]; color: "#252525", // gray grid lines
},
reverse: false,
},
};
const datasets: Dataset[] = datasetConfig
.map((config) => {
if (histories[config.field].some((value) => value !== null)) {
axes[config.axisId] = generateAxis(
config.axisId,
config.axisConfig.reverse,
isMobile ? false : config.axisConfig.display,
config.axisConfig.position,
config.axisConfig.displayName,
);
return generateDataset(
config.title,
histories[config.field],
config.color,
config.axisId,
);
}
return null;
})
.filter(Boolean) as Dataset[];
const options: any = {
maintainAspectRatio: false,
aspectRatio: 1,
interaction: {
mode: "index",
intersect: false,
},
scales: axes,
elements: {
point: {
radius: 0,
},
},
plugins: {
legend: {
position: "top" as const,
labels: {
color: "white",
},
},
tooltip: {
callbacks: {
label(context: any) {
const value = Number(context.parsed.y);
const config = datasetConfig.find(
(cfg) => cfg.title === context.dataset.label,
);
return config?.labelFormatter(value) ?? "";
},
},
},
},
};
const data = { const data = {
labels, labels,