From 0fcf7950b87605c0144faca168a8176489f2af73 Mon Sep 17 00:00:00 2001 From: nairobi Date: Tue, 10 Dec 2024 00:46:51 +0100 Subject: [PATCH 1/3] feat: implement graphs --- .../components/charts/OngekiScoreChart.tsx | 139 ++++++++++++++++++ .../tables/dropdowns/GPTDropdownSettings.tsx | 6 + .../components/OngekiScoreDropdownParts.tsx | 75 ++++++++++ common/src/config/game-support/ongeki.ts | 16 ++ 4 files changed, 236 insertions(+) create mode 100644 client/src/components/charts/OngekiScoreChart.tsx create mode 100644 client/src/components/tables/dropdowns/components/OngekiScoreDropdownParts.tsx diff --git a/client/src/components/charts/OngekiScoreChart.tsx b/client/src/components/charts/OngekiScoreChart.tsx new file mode 100644 index 000000000..98e44ee2f --- /dev/null +++ b/client/src/components/charts/OngekiScoreChart.tsx @@ -0,0 +1,139 @@ +import { TACHI_LINE_THEME } from "util/constants/chart-theme"; +import React from "react"; +import { ResponsiveLine, Serie } from "@nivo/line"; +import { COLOUR_SET } from "tachi-common"; +import ChartTooltip from "./ChartTooltip"; + +const formatTime = (s: number) => + `${Math.floor(s / 60) + .toString() + .padStart(2, "0")}:${Math.floor(s % 60) + .toString() + .padStart(2, "0")}`; + +const scoreToLamp = (s: number) => { + switch (s) { + case 970000: + return "S"; + case 990000: + return "SS"; + case 1000000: + return "SSS"; + case 1007500: + return "SSS+"; + } + return ""; +}; + +const typeSpecificParams = (t: "Score" | "Bells" | "Life", maxBells: number) => { + const minBells = Math.min(-maxBells, -1); + switch (t) { + case "Score": + return { + yScale: { type: "linear", min: 970000, max: 1010000 }, + yFormat: ">-,.0f", + axisLeft: { + tickValues: [970000, 990000, 1000000, 1007500, 1010000], + format: scoreToLamp, + }, + gridYValues: [970000, 980000, 990000, 1000000, 1007500, 1010000], + colors: COLOUR_SET.blue, + areaBaselineValue: 970000, + tooltip: (d) => ( + + {d.point.data.y === 970000 ? "≤ " : ""} + {d.point.data.yFormatted} @ {formatTime(d.point.data.x)} + + ), + }; + case "Bells": + return { + yScale: { type: "linear", min: minBells, max: 0, stacked: false }, + enableGridY: false, + axisLeft: { format: (e) => Math.floor(e) === e && e }, + colors: COLOUR_SET.vibrantYellow, + areaBaselineValue: minBells, + tooltip: (d) => ( + + MAX{d.point.data.y === 0 ? "" : d.point.data.y} @{" "} + {formatTime(d.point.data.x)} + + ), + }; + case "Life": + return { + colors: COLOUR_SET.green, + enableGridY: false, + yScale: { type: "linear", min: 0, max: 100 }, + axisLeft: { format: (d) => `${d}%` }, + areaBaselineValue: 0, + tooltip: (d) => ( + + {d.point.data.y}% @ {formatTime(d.point.data.x)} + + ), + }; + } +}; + +export default function OngekiScoreChart({ + width = "100%", + height = "100%", + mobileHeight = "100%", + mobileWidth = width, + type, + maxBells, + chartSize, + data, +}: { + mobileHeight?: number | string; + mobileWidth?: number | string; + width?: number | string; + height?: number | string; + type: "Score" | "Bells" | "Life"; + maxBells: number; + chartSize: number; + data: Serie[]; +} & ResponsiveLine["props"]) { + const realData = + type === "Score" + ? [ + { + id: "Score", + data: data[0].data.map(({ x, y }) => ({ + x, + y: y && y < 970000 ? 970000 : y, + })), + }, + ] + : data; + const component = ( + formatTime(d) }} + curve="linear" + legends={[]} + enableArea + {...typeSpecificParams(type, maxBells)} + /> + ); + + return ( + <> +
+ {component} +
+
+ {component} +
+ + ); +} diff --git a/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx b/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx index 9d15ec5ce..2bad20de5 100644 --- a/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx +++ b/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx @@ -3,6 +3,7 @@ import { BMSGraphsComponent } from "./components/BMSScoreDropdownParts"; import { IIDXGraphsComponent } from "./components/IIDXScoreDropdownParts"; import { ITGGraphsComponent } from "./components/ITGScoreDropdownParts"; import { JubeatGraphsComponent } from "./components/JubeatScoreDropdownParts"; +import { OngekiGraphsComponent } from "./components/OngekiScoreDropdownParts"; export function GPTDropdownSettings(game: Game, playtype: Playtype): any { if (game === "iidx") { @@ -27,6 +28,11 @@ export function GPTDropdownSettings(game: Game, playtype: Playtype): any { renderScoreInfo: true, GraphComponent: JubeatGraphsComponent as any, }; + } else if (game === "ongeki") { + return { + renderScoreInfo: true, + GraphComponent: OngekiGraphsComponent as any, + }; } return {}; diff --git a/client/src/components/tables/dropdowns/components/OngekiScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/OngekiScoreDropdownParts.tsx new file mode 100644 index 000000000..09f4b1edf --- /dev/null +++ b/client/src/components/tables/dropdowns/components/OngekiScoreDropdownParts.tsx @@ -0,0 +1,75 @@ +import SelectNav from "components/util/SelectNav"; +import React, { useState } from "react"; +import { Nav } from "react-bootstrap"; +import { PBScoreDocument, ScoreData, ScoreDocument } from "tachi-common"; +import OngekiScoreChart from "components/charts/OngekiScoreChart"; + +type ChartTypes = "Score" | "Bells" | "Life"; + +export function OngekiGraphsComponent({ + score, +}: { + score: ScoreDocument<"ongeki:Single"> | PBScoreDocument<"ongeki:Single">; +}) { + const [chart, setChart] = useState("Score"); + const available = + score.scoreData.optional.scoreGraph && + score.scoreData.optional.bellGraph && + score.scoreData.optional.lifeGraph && + score.scoreData.optional.totalBellCount !== null && + score.scoreData.optional.totalBellCount !== undefined; + + return ( + <> +
+ +
+
+ {available ? ( + + ) : ( + "No charts available" + )} +
+ + ); +} + +function GraphComponent({ + type, + scoreData, +}: { + type: ChartTypes; + scoreData: ScoreData<"ongeki:Single">; +}) { + const values = + type === "Score" + ? scoreData.optional.scoreGraph! + : type === "Bells" + ? scoreData.optional.bellGraph! + : scoreData.optional.lifeGraph!; + return ( + ({ x: i, y: e })), + }, + ]} + /> + ); +} diff --git a/common/src/config/game-support/ongeki.ts b/common/src/config/game-support/ongeki.ts index ea545e2d7..0f945b20a 100644 --- a/common/src/config/game-support/ongeki.ts +++ b/common/src/config/game-support/ongeki.ts @@ -90,6 +90,22 @@ export const ONGEKI_SINGLE_CONF = { description: "The Platinum Score value. Only exists in MASTER and LUNATIC charts.", partOfScoreID: true, }, + scoreGraph: { + type: "NULLABLE_GRAPH", + validate: p.isBetween(0, 1010000), + description: "The history of the projected score, queried in one-second intervals.", + }, + bellGraph: { + type: "NULLABLE_GRAPH", + validate: p.isBetween(-10000, 0), + description: + "The history of the number of bells missed, queried in one-second intervals.", + }, + lifeGraph: { + type: "NULLABLE_GRAPH", + validate: p.isBetween(0, 100), + description: "The life gauge history, queried in one-second intervals.", + }, }, scoreRatingAlgs: { From 3a8ad3bb8341c43006b780e402c7e67f18166ae2 Mon Sep 17 00:00:00 2001 From: nairobi Date: Tue, 10 Dec 2024 00:54:17 +0100 Subject: [PATCH 2/3] fix: remove a dbg field --- client/src/components/charts/OngekiScoreChart.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/components/charts/OngekiScoreChart.tsx b/client/src/components/charts/OngekiScoreChart.tsx index 98e44ee2f..072ccbf9c 100644 --- a/client/src/components/charts/OngekiScoreChart.tsx +++ b/client/src/components/charts/OngekiScoreChart.tsx @@ -83,7 +83,6 @@ export default function OngekiScoreChart({ mobileWidth = width, type, maxBells, - chartSize, data, }: { mobileHeight?: number | string; @@ -92,7 +91,6 @@ export default function OngekiScoreChart({ height?: number | string; type: "Score" | "Bells" | "Life"; maxBells: number; - chartSize: number; data: Serie[]; } & ResponsiveLine["props"]) { const realData = From f8d00c8069e4246ec79642b167e3800c22aee605 Mon Sep 17 00:00:00 2001 From: nairobi Date: Tue, 10 Dec 2024 02:01:53 +0100 Subject: [PATCH 3/3] fix: implicit anys I don't know why it doesn't complain about other files. I don't know why the server check failed. Works on my machine. --- .../src/components/charts/OngekiScoreChart.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/src/components/charts/OngekiScoreChart.tsx b/client/src/components/charts/OngekiScoreChart.tsx index 072ccbf9c..6d68b450b 100644 --- a/client/src/components/charts/OngekiScoreChart.tsx +++ b/client/src/components/charts/OngekiScoreChart.tsx @@ -39,7 +39,7 @@ const typeSpecificParams = (t: "Score" | "Bells" | "Life", maxBells: number) => gridYValues: [970000, 980000, 990000, 1000000, 1007500, 1010000], colors: COLOUR_SET.blue, areaBaselineValue: 970000, - tooltip: (d) => ( + tooltip: (d: any) => ( {d.point.data.y === 970000 ? "≤ " : ""} {d.point.data.yFormatted} @ {formatTime(d.point.data.x)} @@ -50,10 +50,10 @@ const typeSpecificParams = (t: "Score" | "Bells" | "Life", maxBells: number) => return { yScale: { type: "linear", min: minBells, max: 0, stacked: false }, enableGridY: false, - axisLeft: { format: (e) => Math.floor(e) === e && e }, + axisLeft: { format: (e: number) => Math.floor(e) === e && e }, colors: COLOUR_SET.vibrantYellow, areaBaselineValue: minBells, - tooltip: (d) => ( + tooltip: (d: any) => ( MAX{d.point.data.y === 0 ? "" : d.point.data.y} @{" "} {formatTime(d.point.data.x)} @@ -65,14 +65,16 @@ const typeSpecificParams = (t: "Score" | "Bells" | "Life", maxBells: number) => colors: COLOUR_SET.green, enableGridY: false, yScale: { type: "linear", min: 0, max: 100 }, - axisLeft: { format: (d) => `${d}%` }, + axisLeft: { format: (d: number) => `${d}%` }, areaBaselineValue: 0, - tooltip: (d) => ( + tooltip: (d: any) => ( {d.point.data.y}% @ {formatTime(d.point.data.x)} ), }; + default: + return {}; } }; @@ -116,11 +118,11 @@ export default function OngekiScoreChart({ useMesh={true} enableGridX={false} theme={TACHI_LINE_THEME} - axisBottom={{ format: (d) => formatTime(d) }} + axisBottom={{ format: (d: number) => formatTime(d) }} curve="linear" legends={[]} enableArea - {...typeSpecificParams(type, maxBells)} + {...(typeSpecificParams(type, maxBells) as any)} /> );