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
Some checks failed
Deploy / deploy (push) Failing after 2m46s
This commit is contained in:
parent
9097d254f1
commit
342dbefac7
@ -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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
labelFormatter: (value: number) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
Reference in New Issue
Block a user