diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fe2aab02e..d6c37b3c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -28,6 +28,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.16.8", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@types/chartjs-plugin-trendline": "1.0.1", "@types/damerau-levenshtein": "1.0.0", "@types/grecaptcha": "^3.0.3", "@types/howler": "^2.2.5", @@ -2500,6 +2501,15 @@ "@types/node": "*" } }, + "node_modules/@types/chartjs-plugin-trendline": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz", + "integrity": "sha512-QN9gWbksSFpM450wnFSfeH76zoHzHEIjVqhVg8hZdhXNp8xkgB07hdIZxVQVjmFl0vDxmXMJtea8jb3QRAtEQg==", + "dev": true, + "dependencies": { + "chart.js": "^3.7.1" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -17480,6 +17490,15 @@ "@types/node": "*" } }, + "@types/chartjs-plugin-trendline": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz", + "integrity": "sha512-QN9gWbksSFpM450wnFSfeH76zoHzHEIjVqhVg8hZdhXNp8xkgB07hdIZxVQVjmFl0vDxmXMJtea8jb3QRAtEQg==", + "dev": true, + "requires": { + "chart.js": "^3.7.1" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1555b6768..55965f5fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.16.8", "@babel/plugin-transform-runtime": "^7.17.0", "@babel/preset-env": "^7.16.11", + "@types/chartjs-plugin-trendline": "1.0.1", "@types/damerau-levenshtein": "1.0.0", "@types/grecaptcha": "^3.0.3", "@types/howler": "^2.2.5", diff --git a/frontend/src/scripts/account/mini-result-chart.ts b/frontend/src/scripts/account/mini-result-chart.ts index 4539503b0..1ddbff1b0 100644 --- a/frontend/src/scripts/account/mini-result-chart.ts +++ b/frontend/src/scripts/account/mini-result-chart.ts @@ -1,6 +1,12 @@ import * as ChartController from "../controllers/chart-controller"; import Config from "../config"; +import type { ScaleChartOptions } from "chart.js"; + +const miniResultScaleOptions = ( + ChartController.result.options as ScaleChartOptions<"line" | "scatter"> +).scales; + export function updatePosition(x: number, y: number): void { $(".pageAccount .miniResultChartWrapper").css({ top: y, left: x }); } @@ -21,10 +27,10 @@ export function updateData(data: MonkeyTypes.ChartData): void { for (let i = 1; i <= data.wpm.length; i++) { labels.push(i.toString()); } - (ChartController.miniResult.data.labels as string[]) = labels; - (ChartController.miniResult.data.datasets[0].data as number[]) = data.wpm; - (ChartController.miniResult.data.datasets[1].data as number[]) = data.raw; - (ChartController.miniResult.data.datasets[2].data as number[]) = data.err; + ChartController.miniResult.data.labels = labels; + ChartController.miniResult.data.datasets[0].data = data.wpm; + ChartController.miniResult.data.datasets[1].data = data.raw; + ChartController.miniResult.data.datasets[2].data = data.err; const maxChartVal = Math.max( ...[Math.max(...data.wpm), Math.max(...data.raw)] @@ -32,19 +38,15 @@ export function updateData(data: MonkeyTypes.ChartData): void { const minChartVal = Math.min( ...[Math.min(...data.wpm), Math.min(...data.raw)] ); - ChartController.miniResult.options.scales!["wpm"]!.max = - Math.round(maxChartVal); - ChartController.miniResult.options.scales!["raw"]!.max = - Math.round(maxChartVal); + miniResultScaleOptions["wpm"].max = Math.round(maxChartVal); + miniResultScaleOptions["raw"].max = Math.round(maxChartVal); if (!Config.startGraphsAtZero) { - ChartController.miniResult.options.scales!["wpm"]!.min = - Math.round(minChartVal); - ChartController.miniResult.options.scales!["raw"]!.min = - Math.round(minChartVal); + miniResultScaleOptions["wpm"].min = Math.round(minChartVal); + miniResultScaleOptions["raw"].min = Math.round(minChartVal); } else { - ChartController.miniResult.options.scales!["wpm"]!.min = 0; - ChartController.miniResult.options.scales!["raw"]!.min = 0; + miniResultScaleOptions["wpm"].min = 0; + miniResultScaleOptions["raw"].min = 0; } ChartController.miniResult.updateColors(); diff --git a/frontend/src/scripts/controllers/chart-controller.js b/frontend/src/scripts/controllers/chart-controller.js deleted file mode 100644 index fd42e62f3..000000000 --- a/frontend/src/scripts/controllers/chart-controller.js +++ /dev/null @@ -1,738 +0,0 @@ -import { - Chart, - BarController, - BarElement, - CategoryScale, - Filler, - Legend, - LinearScale, - LineController, - LineElement, - PointElement, - ScatterController, - TimeScale, - TimeSeriesScale, - Tooltip, -} from "chart.js"; - -import chartTrendline from "chartjs-plugin-trendline"; -import chartAnnotation from "chartjs-plugin-annotation"; - -Chart.register( - BarController, - BarElement, - CategoryScale, - Filler, - Legend, - LinearScale, - LineController, - LineElement, - PointElement, - ScatterController, - TimeScale, - TimeSeriesScale, - Tooltip, - chartTrendline, - chartAnnotation -); - -Chart.defaults.animation.duration = 0; -Chart.defaults.elements.line.tension = 0.3; -Chart.defaults.elements.line.fill = "origin"; - -import * as TestInput from "../test/test-input"; -import * as ThemeColors from "../elements/theme-colors"; -import * as Misc from "../utils/misc"; -import Config from "../config"; -import * as ConfigEvent from "../observables/config-event"; -import format from "date-fns/format"; -import "chartjs-adapter-date-fns"; - -class ChartWithUpdateColors extends Chart { - constructor(item, config) { - super(item, config); - } - - updateColors() { - return updateColors(this); - } -} - -export let result = new ChartWithUpdateColors($("#wpmChart"), { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "wpm", - data: [], - borderColor: "rgba(125, 125, 125, 1)", - borderWidth: 2, - yAxisID: "wpm", - order: 2, - radius: 2, - }, - { - label: "raw", - data: [], - borderColor: "rgba(125, 125, 125, 1)", - borderWidth: 2, - yAxisID: "raw", - order: 3, - radius: 2, - }, - { - label: "errors", - data: [], - borderColor: "rgba(255, 125, 125, 1)", - pointBackgroundColor: "rgba(255, 125, 125, 1)", - borderWidth: 2, - order: 1, - yAxisID: "error", - maxBarThickness: 10, - type: "scatter", - pointStyle: "crossRot", - radius: function (context) { - let index = context.dataIndex; - let value = context.dataset.data[index]; - return value <= 0 ? 0 : 3; - }, - pointHoverRadius: function (context) { - let index = context.dataIndex; - let value = context.dataset.data[index]; - return value <= 0 ? 0 : 5; - }, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - axis: "x", - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - display: true, - title: { - display: false, - text: "Seconds", - }, - }, - wpm: { - axis: "y", - display: true, - title: { - display: true, - text: "Words per Minute", - }, - beginAtZero: true, - min: 0, - ticks: { - precision: 0, - autoSkip: true, - autoSkipPadding: 30, - }, - grid: { - display: true, - }, - }, - raw: { - axis: "y", - display: false, - title: { - display: true, - text: "Raw Words per Minute", - }, - beginAtZero: true, - min: 0, - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - grid: { - display: false, - }, - }, - error: { - axis: "y", - display: true, - position: "right", - title: { - display: true, - text: "Errors", - }, - beginAtZero: true, - ticks: { - precision: 0, - autoSkip: true, - autoSkipPadding: 20, - }, - grid: { - display: false, - }, - }, - }, - plugins: { - annotation: { - annotations: [], - }, - tooltip: { - mode: "index", - intersect: false, - callbacks: { - afterLabel: function (ti) { - try { - $(".wordInputAfter").remove(); - - let wordsToHighlight = - TestInput.keypressPerSecond[parseInt(ti.label) - 1].words; - - let unique = [...new Set(wordsToHighlight)]; - unique.forEach((wordIndex) => { - let wordEl = $( - $("#resultWordsHistory .words .word")[wordIndex] - ); - let input = wordEl.attr("input"); - if (input != undefined) { - wordEl.append( - `
${input - .replace(/\t/g, "_") - .replace(/\n/g, "_") - .replace(//g, ">")}
` - ); - } - }); - } catch {} - }, - }, - }, - legend: { - display: false, - labels: {}, - }, - }, - }, -}); - -export let accountHistoryActiveIndex; - -export let accountHistory = new ChartWithUpdateColors( - $(".pageAccount #accountHistoryChart"), - { - type: "line", - data: { - datasets: [ - { - yAxisID: "wpm", - label: "wpm", - fill: false, - data: [], - borderColor: "#f44336", - borderWidth: 2, - trendlineLinear: { - style: "rgba(255,105,180, .8)", - lineStyle: "dotted", - width: 4, - }, - }, - { - yAxisID: "acc", - label: "acc", - fill: false, - data: [], - borderColor: "#cccccc", - borderWidth: 2, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - hover: { - mode: "nearest", - intersect: false, - }, - scales: { - x: { - axis: "x", - type: "timeseries", - bounds: "ticks", - display: false, - offset: true, - title: { - display: false, - text: "Date", - }, - }, - wpm: { - axis: "y", - beginAtZero: true, - min: 0, - ticks: { - stepSize: 10, - }, - display: true, - title: { - display: true, - text: "Words per Minute", - }, - }, - acc: { - axis: "y", - beginAtZero: true, - max: 100, - display: true, - position: "right", - title: { - display: true, - text: "Error rate (100 - accuracy)", - }, - grid: { - display: false, - }, - }, - }, - plugins: { - annotation: { - annotations: [], - }, - tooltip: { - // Disable the on-canvas tooltip - enabled: true, - intersect: false, - external: function (tooltip) { - if (!tooltip) return; - // disable displaying the color box; - tooltip.displayColors = false; - }, - callbacks: { - // HERE YOU CUSTOMIZE THE LABELS - title: function () { - return; - }, - beforeLabel: function (tooltipItem) { - let resultData = tooltipItem.dataset.data[tooltipItem.dataIndex]; - if (tooltipItem.datasetIndex !== 0) { - return `error rate: ${Misc.roundTo2( - resultData.errorRate - )}%\nacc: ${Misc.roundTo2(100 - resultData.errorRate)}%`; - } - let label = - `${Config.alwaysShowCPM ? "cpm" : "wpm"}: ${resultData.wpm}` + - "\n" + - `raw: ${resultData.raw}` + - "\n" + - `acc: ${resultData.acc}` + - "\n\n" + - `mode: ${resultData.mode} `; - - if (resultData.mode == "time") { - label += resultData.mode2; - } else if (resultData.mode == "words") { - label += resultData.mode2; - } - - let diff = resultData.difficulty; - if (diff == undefined) { - diff = "normal"; - } - label += "\n" + `difficulty: ${diff}`; - - label += - "\n" + - `punctuation: ${resultData.punctuation}` + - "\n" + - `language: ${resultData.language}` + - "\n\n" + - `date: ${format( - new Date(resultData.timestamp), - "dd MMM yyyy HH:mm" - )}`; - - return label; - }, - label: function () { - return; - }, - afterLabel: function (tooltip) { - accountHistoryActiveIndex = tooltip.dataIndex; - return; - }, - }, - }, - legend: { - display: false, - labels: { - color: "#ffffff", - }, - }, - }, - }, - } -); - -export let accountActivity = new ChartWithUpdateColors( - $(".pageAccount #accountActivityChart"), - { - type: "bar", - data: { - datasets: [ - { - yAxisID: "count", - label: "Seconds", - data: [], - trendlineLinear: { - style: "rgba(255,105,180, .8)", - lineStyle: "dotted", - width: 2, - }, - order: 3, - }, - { - yAxisID: "avgWpm", - label: "Average Wpm", - data: [], - type: "line", - order: 2, - tension: 0, - fill: false, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - hover: { - mode: "nearest", - intersect: false, - }, - scales: { - x: { - axis: "x", - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - type: "time", - time: { - unit: "day", - displayFormats: { - day: "d MMM", - }, - }, - bounds: "ticks", - display: true, - title: { - display: false, - text: "Date", - }, - offset: true, - }, - count: { - axis: "y", - beginAtZero: true, - min: 0, - ticks: { - autoSkip: true, - autoSkipPadding: 20, - stepSize: 10, - }, - display: true, - title: { - display: true, - text: "Time Typing", - }, - }, - avgWpm: { - axis: "y", - beginAtZero: true, - min: 0, - ticks: { - autoSkip: true, - autoSkipPadding: 20, - stepSize: 10, - }, - display: true, - position: "right", - title: { - display: true, - text: "Average Wpm", - }, - grid: { - display: false, - }, - }, - }, - plugins: { - annotation: { - annotations: [], - }, - tooltip: { - callbacks: { - // HERE YOU CUSTOMIZE THE LABELS - title: function (tooltipItem) { - let resultData = - tooltipItem[0].dataset.data[tooltipItem[0].dataIndex]; - return format(new Date(resultData.x), "dd MMM yyyy"); - }, - beforeLabel: function (tooltipItem) { - let resultData = tooltipItem.dataset.data[tooltipItem.dataIndex]; - if (tooltipItem.datasetIndex === 0) { - return `Time Typing: ${Misc.secondsToString( - Math.round(resultData.y), - true, - true - )}\nTests Completed: ${resultData.amount}`; - } else if (tooltipItem.datasetIndex === 1) { - return `Average ${ - Config.alwaysShowCPM ? "Cpm" : "Wpm" - }: ${Misc.roundTo2(resultData.y)}`; - } - }, - label: function () { - return; - }, - }, - }, - legend: { - display: false, - labels: { - color: "#ffffff", - }, - }, - }, - }, - } -); - -export let miniResult = new ChartWithUpdateColors( - $(".pageAccount #miniResultChart"), - { - type: "line", - data: { - labels: [], - datasets: [ - { - label: "wpm", - data: [], - borderColor: "rgba(125, 125, 125, 1)", - borderWidth: 2, - yAxisID: "wpm", - order: 2, - radius: 2, - }, - { - label: "raw", - data: [], - borderColor: "rgba(125, 125, 125, 1)", - borderWidth: 2, - yAxisID: "raw", - order: 3, - radius: 2, - }, - { - label: "errors", - data: [], - borderColor: "rgba(255, 125, 125, 1)", - pointBackgroundColor: "rgba(255, 125, 125, 1)", - borderWidth: 2, - order: 1, - yAxisID: "error", - maxBarThickness: 10, - type: "scatter", - pointStyle: "crossRot", - radius: function (context) { - let index = context.dataIndex; - let value = context.dataset.data[index]; - return value <= 0 ? 0 : 3; - }, - pointHoverRadius: function (context) { - let index = context.dataIndex; - let value = context.dataset.data[index]; - return value <= 0 ? 0 : 5; - }, - }, - ], - }, - options: { - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - axis: "x", - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - display: true, - title: { - display: false, - text: "Seconds", - }, - }, - wpm: { - axis: "y", - display: true, - title: { - display: true, - text: "Words per Minute", - }, - beginAtZero: true, - min: 0, - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - grid: { - display: true, - }, - }, - raw: { - axis: "y", - display: false, - title: { - display: true, - text: "Raw Words per Minute", - }, - beginAtZero: true, - min: 0, - ticks: { - autoSkip: true, - autoSkipPadding: 20, - }, - grid: { - display: false, - }, - }, - error: { - display: true, - position: "right", - title: { - display: true, - text: "Errors", - }, - beginAtZero: true, - ticks: { - precision: 0, - autoSkip: true, - autoSkipPadding: 20, - }, - grid: { - display: false, - }, - }, - }, - plugins: { - annotation: { - annotations: [], - }, - tooltip: { - mode: "index", - intersect: false, - }, - legend: { - display: false, - labels: {}, - }, - }, - }, - } -); - -function updateAccuracy() { - accountHistory.data.datasets[1].hidden = !Config.chartAccuracy; - accountHistory.options.scales["acc"].display = Config.chartAccuracy; - accountHistory.update(); -} - -function updateStyle() { - if (Config.chartStyle == "scatter") { - accountHistory.data.datasets[0].showLine = false; - accountHistory.data.datasets[1].showLine = false; - } else { - accountHistory.data.datasets[0].showLine = true; - accountHistory.data.datasets[1].showLine = true; - } - accountHistory.updateColors(); -} - -export async function updateColors(chart) { - let bgcolor = await ThemeColors.get("bg"); - let subcolor = await ThemeColors.get("sub"); - let maincolor = await ThemeColors.get("main"); - let errorcolor = await ThemeColors.get("error"); - - if (chart.data.datasets.every((dataset) => dataset.data.length === 0)) { - return; - } - - chart.data.datasets[0].borderColor = maincolor; - chart.data.datasets[1].borderColor = subcolor; - if (chart.data.datasets[2]) { - chart.data.datasets[2].borderColor = errorcolor; - } - - if (chart.data.datasets[0].type === undefined) { - if (chart.config.type === "line") { - chart.data.datasets[0].pointBackgroundColor = maincolor; - } else if (chart.config.type === "bar") { - chart.data.datasets[0].backgroundColor = maincolor; - } - } else if (chart.data.datasets[0].type === "bar") { - chart.data.datasets[0].backgroundColor = maincolor; - } else if (chart.data.datasets[0].type === "line") { - chart.data.datasets[0].pointBackgroundColor = maincolor; - } - - if (chart.data.datasets[1].type === undefined) { - if (chart.config.type === "line") { - chart.data.datasets[1].pointBackgroundColor = subcolor; - } else if (chart.config.type === "bar") { - chart.data.datasets[1].backgroundColor = subcolor; - } - } else if (chart.data.datasets[1].type === "bar") { - chart.data.datasets[1].backgroundColor = subcolor; - } else if (chart.data.datasets[1].type === "line") { - chart.data.datasets[1].pointBackgroundColor = subcolor; - } - - Object.keys(chart.options.scales).forEach((scaleID) => { - chart.options.scales[scaleID].ticks.color = subcolor; - chart.options.scales[scaleID].title.color = subcolor; - }); - - try { - chart.data.datasets[0].trendlineLinear.style = subcolor; - chart.data.datasets[1].trendlineLinear.style = subcolor; - } catch {} - - chart.options.plugins.annotation.annotations.forEach((annotation) => { - annotation.borderColor = subcolor; - annotation.label.backgroundColor = subcolor; - annotation.label.color = bgcolor; - }); - - chart.update("none"); -} - -export function setDefaultFontFamily(font) { - Chart.defaults.font.family = font.replace(/_/g, " "); -} - -export function updateAllChartColors() { - ThemeColors.update(); - accountHistory.updateColors(); - result.updateColors(); - accountActivity.updateColors(); - miniResult.updateColors(); -} - -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "chartAccuracy") updateAccuracy(); - if (eventKey === "chartStyle") updateStyle(); - if (eventKey === "fontFamily") setDefaultFontFamily(eventValue); -}); diff --git a/frontend/src/scripts/controllers/chart-controller.ts b/frontend/src/scripts/controllers/chart-controller.ts new file mode 100644 index 000000000..cc0847c6b --- /dev/null +++ b/frontend/src/scripts/controllers/chart-controller.ts @@ -0,0 +1,797 @@ +import { + Chart, + BarController, + BarElement, + CategoryScale, + Filler, + LinearScale, + LineController, + LineElement, + PointElement, + ScatterController, + TimeScale, + TimeSeriesScale, + Tooltip, +} from "chart.js"; + +import chartTrendline from "chartjs-plugin-trendline"; +import chartAnnotation from "chartjs-plugin-annotation"; + +Chart.register( + BarController, + BarElement, + CategoryScale, + Filler, + LinearScale, + LineController, + LineElement, + PointElement, + ScatterController, + TimeScale, + TimeSeriesScale, + Tooltip, + chartTrendline, + chartAnnotation +); + +( + Chart.defaults.animation as AnimationSpec<"line" | "bar" | "scatter"> +).duration = 0; +Chart.defaults.elements.line.tension = 0.3; +Chart.defaults.elements.line.fill = "origin"; + +import * as TestInput from "../test/test-input"; +import * as ThemeColors from "../elements/theme-colors"; +import * as Misc from "../utils/misc"; +import Config from "../config"; +import * as ConfigEvent from "../observables/config-event"; +import { format } from "date-fns"; +import "chartjs-adapter-date-fns"; + +import type { + AnimationSpec, + CartesianScaleOptions, + ChartConfiguration, + ChartDataset, + ChartType, + DefaultDataPoint, + PluginChartOptions, + ScaleChartOptions, +} from "chart.js"; + +import type { + AnnotationOptions, + LabelOptions, +} from "chartjs-plugin-annotation"; + +class ChartWithUpdateColors< + TType extends ChartType = ChartType, + TData = DefaultDataPoint, + TLabel = unknown +> extends Chart { + constructor(item: any, config: ChartConfiguration) { + super(item, config); + } + + updateColors(): void { + updateColors(this); + } +} + +export const result: ChartWithUpdateColors< + "line" | "scatter", + number[], + string +> = new ChartWithUpdateColors($("#wpmChart"), { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "wpm", + data: [], + borderColor: "rgba(125, 125, 125, 1)", + borderWidth: 2, + yAxisID: "wpm", + order: 2, + pointRadius: 2, + }, + { + label: "raw", + data: [], + borderColor: "rgba(125, 125, 125, 1)", + borderWidth: 2, + yAxisID: "raw", + order: 3, + pointRadius: 2, + }, + { + label: "errors", + data: [], + borderColor: "rgba(255, 125, 125, 1)", + pointBackgroundColor: "rgba(255, 125, 125, 1)", + borderWidth: 2, + order: 1, + yAxisID: "error", + type: "scatter", + pointStyle: "crossRot", + pointRadius: function (context): number { + const index = context.dataIndex; + const value = context.dataset.data[index]; + return (value ?? 0) <= 0 ? 0 : 3; + }, + pointHoverRadius: function (context): number { + const index = context.dataIndex; + const value = context.dataset.data[index]; + return (value ?? 0) <= 0 ? 0 : 5; + }, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + axis: "x", + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + display: true, + title: { + display: false, + text: "Seconds", + }, + }, + wpm: { + axis: "y", + display: true, + title: { + display: true, + text: "Words per Minute", + }, + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: true, + }, + }, + raw: { + axis: "y", + display: false, + title: { + display: true, + text: "Raw Words per Minute", + }, + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: false, + }, + }, + error: { + axis: "y", + display: true, + position: "right", + title: { + display: true, + text: "Errors", + }, + beginAtZero: true, + ticks: { + precision: 0, + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: false, + }, + }, + }, + plugins: { + annotation: { + annotations: [], + }, + tooltip: { + animation: { duration: 250 }, + mode: "index", + intersect: false, + callbacks: { + afterLabel: function (ti): string { + try { + $(".wordInputAfter").remove(); + + const wordsToHighlight = + TestInput.keypressPerSecond[parseInt(ti.label) - 1].words; + + const unique = [...new Set(wordsToHighlight)]; + unique.forEach((wordIndex) => { + const wordEl = $( + $("#resultWordsHistory .words .word")[wordIndex] + ); + const input = wordEl.attr("input"); + if (input != undefined) { + wordEl.append( + `
${input + .replace(/\t/g, "_") + .replace(/\n/g, "_") + .replace(//g, ">")}
` + ); + } + }); + } catch {} + return ""; + }, + }, + }, + }, + }, +}); + +export let accountHistoryActiveIndex: number; + +export const accountHistory: ChartWithUpdateColors< + "line", + MonkeyTypes.HistoryChartData[] | MonkeyTypes.AccChartData[], + string +> = new ChartWithUpdateColors($(".pageAccount #accountHistoryChart"), { + type: "line", + data: { + labels: [], + datasets: [ + { + yAxisID: "wpm", + label: "wpm", + fill: false, + data: [], + borderColor: "#f44336", + borderWidth: 2, + trendlineLinear: { + style: "rgba(255,105,180, .8)", + lineStyle: "dotted", + width: 4, + }, + }, + { + yAxisID: "acc", + label: "acc", + fill: false, + data: [], + borderColor: "#cccccc", + borderWidth: 2, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + hover: { + mode: "nearest", + intersect: false, + }, + scales: { + x: { + axis: "x", + type: "timeseries", + bounds: "ticks", + display: false, + offset: true, + title: { + display: false, + text: "Date", + }, + }, + wpm: { + axis: "y", + beginAtZero: true, + min: 0, + ticks: { + stepSize: 10, + }, + display: true, + title: { + display: true, + text: "Words per Minute", + }, + }, + acc: { + axis: "y", + beginAtZero: true, + max: 100, + display: true, + position: "right", + title: { + display: true, + text: "Error rate (100 - accuracy)", + }, + grid: { + display: false, + }, + }, + }, + plugins: { + annotation: { + annotations: [], + }, + tooltip: { + animation: { duration: 250 }, + // Disable the on-canvas tooltip + enabled: true, + intersect: false, + external: function (ctx): void { + if (!ctx) return; + ctx.tooltip.options.displayColors = false; + }, + callbacks: { + title: function (): string { + return ""; + }, + beforeLabel: function (tooltipItem): string { + if (tooltipItem.datasetIndex !== 0) { + const resultData = tooltipItem.dataset.data[ + tooltipItem.dataIndex + ] as MonkeyTypes.AccChartData; + return `error rate: ${Misc.roundTo2( + resultData.errorRate + )}%\nacc: ${Misc.roundTo2(100 - resultData.errorRate)}%`; + } + const resultData = tooltipItem.dataset.data[ + tooltipItem.dataIndex + ] as MonkeyTypes.HistoryChartData; + let label = + `${Config.alwaysShowCPM ? "cpm" : "wpm"}: ${resultData.wpm}` + + "\n" + + `raw: ${resultData.raw}` + + "\n" + + `acc: ${resultData.acc}` + + "\n\n" + + `mode: ${resultData.mode} `; + + if (resultData.mode == "time") { + label += resultData.mode2; + } else if (resultData.mode == "words") { + label += resultData.mode2; + } + + let diff = resultData.difficulty; + if (diff == undefined) { + diff = "normal"; + } + label += "\n" + `difficulty: ${diff}`; + + label += + "\n" + + `punctuation: ${resultData.punctuation}` + + "\n" + + `language: ${resultData.language}` + + "\n\n" + + `date: ${format( + new Date(resultData.timestamp), + "dd MMM yyyy HH:mm" + )}`; + + return label; + }, + label: function (): string { + return ""; + }, + afterLabel: function (tooltip): string { + accountHistoryActiveIndex = tooltip.dataIndex; + return ""; + }, + }, + }, + }, + }, +}); + +export const accountActivity: ChartWithUpdateColors< + "bar" | "line", + MonkeyTypes.ActivityChartDataPoint[], + string +> = new ChartWithUpdateColors($(".pageAccount #accountActivityChart"), { + type: "bar", + data: { + labels: [], + datasets: [ + { + yAxisID: "count", + label: "Seconds", + data: [], + trendlineLinear: { + style: "rgba(255,105,180, .8)", + lineStyle: "dotted", + width: 2, + }, + order: 3, + }, + { + yAxisID: "avgWpm", + label: "Average Wpm", + data: [], + type: "line", + order: 2, + tension: 0, + fill: false, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + hover: { + mode: "nearest", + intersect: false, + }, + scales: { + x: { + axis: "x", + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + type: "time", + time: { + unit: "day", + displayFormats: { + day: "d MMM", + }, + }, + bounds: "ticks", + display: true, + title: { + display: false, + text: "Date", + }, + offset: true, + }, + count: { + axis: "y", + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + stepSize: 10, + }, + display: true, + title: { + display: true, + text: "Time Typing", + }, + }, + avgWpm: { + axis: "y", + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + stepSize: 10, + }, + display: true, + position: "right", + title: { + display: true, + text: "Average Wpm", + }, + grid: { + display: false, + }, + }, + }, + plugins: { + annotation: { + annotations: [], + }, + tooltip: { + animation: { duration: 250 }, + callbacks: { + title: function (tooltipItem): string { + const resultData = tooltipItem[0].dataset.data[ + tooltipItem[0].dataIndex + ] as MonkeyTypes.ActivityChartDataPoint; + return format(new Date(resultData.x), "dd MMM yyyy"); + }, + beforeLabel: function (tooltipItem): string { + const resultData = tooltipItem.dataset.data[ + tooltipItem.dataIndex + ] as MonkeyTypes.ActivityChartDataPoint; + switch (tooltipItem.datasetIndex) { + case 0: + return `Time Typing: ${Misc.secondsToString( + Math.round(resultData.y), + true, + true + )}\nTests Completed: ${resultData.amount}`; + case 1: + return `Average ${ + Config.alwaysShowCPM ? "Cpm" : "Wpm" + }: ${Misc.roundTo2(resultData.y)}`; + default: + return ""; + } + }, + label: function (): string { + return ""; + }, + }, + }, + }, + }, +}); + +export const miniResult: ChartWithUpdateColors< + "line" | "scatter", + number[], + string +> = new ChartWithUpdateColors($(".pageAccount #miniResultChart"), { + type: "line", + data: { + labels: [], + datasets: [ + { + label: "wpm", + data: [], + borderColor: "rgba(125, 125, 125, 1)", + borderWidth: 2, + yAxisID: "wpm", + order: 2, + pointRadius: 2, + }, + { + label: "raw", + data: [], + borderColor: "rgba(125, 125, 125, 1)", + borderWidth: 2, + yAxisID: "raw", + order: 3, + pointRadius: 2, + }, + { + label: "errors", + data: [], + borderColor: "rgba(255, 125, 125, 1)", + pointBackgroundColor: "rgba(255, 125, 125, 1)", + borderWidth: 2, + order: 1, + yAxisID: "error", + type: "scatter", + pointStyle: "crossRot", + pointRadius: function (context): number { + const index = context.dataIndex; + const value = context.dataset.data[index]; + return (value ?? 0) <= 0 ? 0 : 3; + }, + pointHoverRadius: function (context): number { + const index = context.dataIndex; + const value = context.dataset.data[index]; + return (value ?? 0) <= 0 ? 0 : 5; + }, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + axis: "x", + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + display: true, + title: { + display: false, + text: "Seconds", + }, + }, + wpm: { + axis: "y", + display: true, + title: { + display: true, + text: "Words per Minute", + }, + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: true, + }, + }, + raw: { + axis: "y", + display: false, + title: { + display: true, + text: "Raw Words per Minute", + }, + beginAtZero: true, + min: 0, + ticks: { + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: false, + }, + }, + error: { + display: true, + position: "right", + title: { + display: true, + text: "Errors", + }, + beginAtZero: true, + ticks: { + precision: 0, + autoSkip: true, + autoSkipPadding: 20, + }, + grid: { + display: false, + }, + }, + }, + plugins: { + annotation: { + annotations: [], + }, + tooltip: { + animation: { duration: 250 }, + mode: "index", + intersect: false, + }, + }, + }, +}); + +function updateAccuracy(): void { + accountHistory.data.datasets[1].hidden = !Config.chartAccuracy; + (accountHistory.options as ScaleChartOptions<"line">).scales["acc"].display = + Config.chartAccuracy; + accountHistory.update(); +} + +function updateStyle(): void { + if (Config.chartStyle == "scatter") { + accountHistory.data.datasets[0].showLine = false; + accountHistory.data.datasets[1].showLine = false; + } else { + accountHistory.data.datasets[0].showLine = true; + accountHistory.data.datasets[1].showLine = true; + } + accountHistory.updateColors(); +} + +export async function updateColors< + TType extends ChartType = "bar" | "line" | "scatter", + TData = + | MonkeyTypes.HistoryChartData[] + | MonkeyTypes.AccChartData[] + | MonkeyTypes.ActivityChartDataPoint[] + | number[], + TLabel = string +>(chart: ChartWithUpdateColors): Promise { + const bgcolor = await ThemeColors.get("bg"); + const subcolor = await ThemeColors.get("sub"); + const maincolor = await ThemeColors.get("main"); + const errorcolor = await ThemeColors.get("error"); + + if ( + chart.data.datasets.every( + (dataset) => + ( + dataset.data as unknown as ( + | MonkeyTypes.HistoryChartData + | MonkeyTypes.AccChartData + | MonkeyTypes.ActivityChartDataPoint + | number + )[] + ).length === 0 + ) + ) { + return; + } + + chart.data.datasets[0].borderColor = maincolor; + chart.data.datasets[1].borderColor = subcolor; + if (chart.data.datasets[2]) { + chart.data.datasets[2].borderColor = errorcolor; + } + + if (chart.data.datasets[0].type === undefined) { + if (chart.config.type === "line") { + ( + chart.data.datasets as ChartDataset<"line", TData>[] + )[0].pointBackgroundColor = maincolor; + } else if (chart.config.type === "bar") { + chart.data.datasets[0].backgroundColor = maincolor; + } + } else if (chart.data.datasets[0].type === "bar") { + chart.data.datasets[0].backgroundColor = maincolor; + } else if (chart.data.datasets[0].type === "line") { + ( + chart.data.datasets as ChartDataset<"line", TData>[] + )[0].pointBackgroundColor = maincolor; + } + + if (chart.data.datasets[1].type === undefined) { + if (chart.config.type === "line") { + ( + chart.data.datasets as ChartDataset<"line", TData>[] + )[1].pointBackgroundColor = subcolor; + } else if (chart.config.type === "bar") { + chart.data.datasets[1].backgroundColor = subcolor; + } + } else if (chart.data.datasets[1].type === "bar") { + chart.data.datasets[1].backgroundColor = subcolor; + } else if (chart.data.datasets[1].type === "line") { + ( + chart.data.datasets as ChartDataset<"line", TData>[] + )[1].pointBackgroundColor = subcolor; + } + + const chartScaleOptions = chart.options as ScaleChartOptions; + Object.keys(chartScaleOptions.scales).forEach((scaleID) => { + const axis = chartScaleOptions.scales[scaleID] as CartesianScaleOptions; + axis.ticks.color = subcolor; + axis.title.color = subcolor; + }); + + try { + ( + chart.data.datasets[0] + .trendlineLinear as TrendlineLinearPlugin.TrendlineLinearOptions + ).style = subcolor; + ( + chart.data.datasets[1] + .trendlineLinear as TrendlineLinearPlugin.TrendlineLinearOptions + ).style = subcolor; + } catch {} + + ( + (chart.options as PluginChartOptions).plugins.annotation + .annotations as AnnotationOptions<"line">[] + ).forEach((annotation) => { + annotation.borderColor = subcolor; + (annotation.label as LabelOptions).backgroundColor = subcolor; + (annotation.label as LabelOptions).color = bgcolor; + }); + + chart.update("none"); +} + +export function setDefaultFontFamily(font: string): void { + Chart.defaults.font.family = font.replace(/_/g, " "); +} + +export function updateAllChartColors(): void { + ThemeColors.update(); + accountHistory.updateColors(); + result.updateColors(); + accountActivity.updateColors(); + miniResult.updateColors(); +} + +ConfigEvent.subscribe((eventKey, eventValue) => { + if (eventKey === "chartAccuracy") updateAccuracy(); + if (eventKey === "chartStyle") updateStyle(); + if (eventKey === "fontFamily") setDefaultFontFamily(eventValue as string); +}); diff --git a/frontend/src/scripts/pages/account.ts b/frontend/src/scripts/pages/account.ts index 96403c557..e2f72e612 100644 --- a/frontend/src/scripts/pages/account.ts +++ b/frontend/src/scripts/pages/account.ts @@ -15,7 +15,8 @@ import Page from "./page"; import * as Misc from "../utils/misc"; import * as ActivePage from "../states/active-page"; import format from "date-fns/format"; -import type { Chart } from "chart.js"; + +import type { ScaleChartOptions } from "chart.js"; let filterDebug = false; //toggle filterdebug @@ -186,29 +187,9 @@ export function reset(): void { ChartController.accountHistory.updateColors(); } -type ChartData = { - x: number; - y: number; - wpm: number; - acc: number; - mode: string; - mode2: string | number; - punctuation: boolean; - language: string; - timestamp: number; - difficulty: string; - raw: number; -}; - -type AccChartData = { - x: number; - y: number; - errorRate: number; -}; - let totalSecondsFiltered = 0; -let chartData: ChartData[] = []; -let accChartData: AccChartData[] = []; +let chartData: MonkeyTypes.HistoryChartData[] = []; +let accChartData: MonkeyTypes.AccChartData[] = []; export function smoothHistory(factor: number): void { const smoothedWpmData = Misc.smooth( @@ -232,10 +213,8 @@ export function smoothHistory(factor: number): void { return ret; }); - (ChartController.accountHistory.data.datasets[0].data as ChartData[]) = - chartData2; - (ChartController.accountHistory.data.datasets[1].data as AccChartData[]) = - accChartData2; + ChartController.accountHistory.data.datasets[0].data = chartData2; + ChartController.accountHistory.data.datasets[1].data = accChartData2; if (chartData2.length || accChartData2.length) { ChartController.accountHistory.update(); @@ -665,15 +644,9 @@ export function update(): void { loadMoreLines(); //////// - type ActivityChartDataPoint = { - x: number; - y: number; - amount?: number; - }; - - const activityChartData_amount: ActivityChartDataPoint[] = []; - const activityChartData_time: ActivityChartDataPoint[] = []; - const activityChartData_avgWpm: ActivityChartDataPoint[] = []; + const activityChartData_amount: MonkeyTypes.ActivityChartDataPoint[] = []; + const activityChartData_time: MonkeyTypes.ActivityChartDataPoint[] = []; + const activityChartData_avgWpm: MonkeyTypes.ActivityChartDataPoint[] = []; // let lastTimestamp = 0; Object.keys(activityChartData).forEach((date) => { const dateInt = parseInt(date); @@ -698,49 +671,48 @@ export function update(): void { // lastTimestamp = date; }); - if (Config.alwaysShowCPM) { - ( - ChartController.accountActivity as Chart<"bar" | "line"> - ).options.scales!["avgWpm"]!.title!.text = "Average Cpm"; - } else { - ( - ChartController.accountActivity as Chart<"bar" | "line"> - ).options.scales!["avgWpm"]!.title!.text = "Average Wpm"; - } - - (ChartController.accountActivity.data.datasets[0] - .data as ActivityChartDataPoint[]) = activityChartData_time; - (ChartController.accountActivity.data.datasets[1] - .data as ActivityChartDataPoint[]) = activityChartData_avgWpm; + const accountActivityScaleOptions = ( + ChartController.accountActivity.options as ScaleChartOptions< + "bar" | "line" + > + ).scales; if (Config.alwaysShowCPM) { - (ChartController.accountHistory as Chart<"line">).options.scales![ - "wpm" - ]!.title!.text = "Characters per Minute"; + accountActivityScaleOptions["avgWpm"].title.text = "Average Cpm"; } else { - (ChartController.accountHistory as Chart<"line">).options.scales![ - "wpm" - ]!.title!.text = "Words per Minute"; + accountActivityScaleOptions["avgWpm"].title.text = "Average Wpm"; } - (ChartController.accountHistory.data.datasets[0].data as ChartData[]) = - chartData; - (ChartController.accountHistory.data.datasets[1].data as AccChartData[]) = - accChartData; + ChartController.accountActivity.data.datasets[0].data = + activityChartData_time; + ChartController.accountActivity.data.datasets[1].data = + activityChartData_avgWpm; + + const accountHistoryScaleOptions = ( + ChartController.accountHistory.options as ScaleChartOptions<"line"> + ).scales; + + if (Config.alwaysShowCPM) { + accountHistoryScaleOptions["wpm"].title.text = "Characters per Minute"; + } else { + accountHistoryScaleOptions["wpm"].title.text = "Words per Minute"; + } + + ChartController.accountHistory.data.datasets[0].data = chartData; + ChartController.accountHistory.data.datasets[1].data = accChartData; const wpms = chartData.map((r) => r.y); const minWpmChartVal = Math.min(...wpms); const maxWpmChartVal = Math.max(...wpms); // let accuracies = accChartData.map((r) => r.y); - ChartController.accountHistory.options.scales!["wpm"]!.max = + accountHistoryScaleOptions["wpm"].max = Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10)); if (!Config.startGraphsAtZero) { - ChartController.accountHistory.options.scales!["wpm"]!.min = - Math.floor(minWpmChartVal); + accountHistoryScaleOptions["wpm"].min = Math.floor(minWpmChartVal); } else { - ChartController.accountHistory.options.scales!["wpm"]!.min = 0; + accountHistoryScaleOptions["wpm"].min = 0; } if (chartData == [] || chartData.length == 0) { diff --git a/frontend/src/scripts/test/result.ts b/frontend/src/scripts/test/result.ts index b13bdb5b7..f5afb70da 100644 --- a/frontend/src/scripts/test/result.ts +++ b/frontend/src/scripts/test/result.ts @@ -13,9 +13,11 @@ import * as GlarsesMode from "../states/glarses-mode"; import * as TestInput from "./test-input"; import * as Notifications from "../elements/notifications"; import { Chart } from "chart.js"; -import { AnnotationOptions } from "chartjs-plugin-annotation"; import { Auth } from "../firebase"; +import type { PluginChartOptions, ScaleChartOptions } from "chart.js"; +import type { AnnotationOptions } from "chartjs-plugin-annotation"; + let result: MonkeyTypes.Result; let maxChartVal: number; @@ -26,8 +28,15 @@ export function toggleUnsmoothedRaw(): void { Notifications.add(useUnsmoothedRaw ? "on" : "off", 1); } +const resultAnnotation = ( + ChartController.result.options as PluginChartOptions<"line" | "scatter"> +).plugins.annotation.annotations as AnnotationOptions<"line">[]; +const resultScaleOptions = ( + ChartController.result.options as ScaleChartOptions<"line" | "scatter"> +).scales; + async function updateGraph(): Promise { - ChartController.result.options.plugins!.annotation!.annotations = []; + resultAnnotation.length = 0; // Clear result annotation list to reset funbox label without reassigning to new array. const labels = []; for (let i = 1; i <= TestInput.wpmHistory.length; i++) { if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) { @@ -36,10 +45,8 @@ async function updateGraph(): Promise { labels.push(i.toString()); } } - (ChartController.result.data.labels as string[]) = labels; - (ChartController.result as Chart<"line" | "scatter">).options.scales![ - "wpm" - ]!.title!.text = Config.alwaysShowCPM + ChartController.result.data.labels = labels; + resultScaleOptions["wpm"].title.text = Config.alwaysShowCPM ? "Character per Minute" : "Words per Minute"; const chartData1 = Config.alwaysShowCPM @@ -61,8 +68,8 @@ async function updateGraph(): Promise { : result.chartData.raw; } - (ChartController.result.data.datasets[0].data as number[]) = chartData1; - (ChartController.result.data.datasets[1].data as number[]) = chartData2; + ChartController.result.data.datasets[0].data = chartData1; + ChartController.result.data.datasets[1].data = chartData2; ChartController.result.data.datasets[0].label = Config.alwaysShowCPM ? "cpm" @@ -73,15 +80,14 @@ async function updateGraph(): Promise { const minChartVal = Math.min( ...[Math.min(...chartData2), Math.min(...chartData1)] ); - ChartController.result.options.scales!["wpm"]!.min = minChartVal; - ChartController.result.options.scales!["raw"]!.min = minChartVal; + resultScaleOptions["wpm"].min = minChartVal; + resultScaleOptions["raw"].min = minChartVal; } else { - ChartController.result.options.scales!["wpm"]!.min = 0; - ChartController.result.options.scales!["raw"]!.min = 0; + resultScaleOptions["wpm"].min = 0; + resultScaleOptions["raw"].min = 0; } - (ChartController.result.data.datasets[2].data as number[]) = - result.chartData.err; + ChartController.result.data.datasets[2].data = result.chartData.err; const fc = await ThemeColors.get("sub"); if (Config.funbox !== "none") { @@ -89,15 +95,12 @@ async function updateGraph(): Promise { if (Config.funbox === "layoutfluid") { content += " " + Config.customLayoutfluid.replace(/#/g, " "); } - ( - ChartController.result.options.plugins!.annotation! - .annotations as AnnotationOptions[] - ).push({ + resultAnnotation.push({ display: true, id: "funbox-label", type: "line", scaleID: "wpm", - value: ChartController.result.options.scales!["wpm"]!.min, + value: resultScaleOptions["wpm"].min, borderColor: "transparent", borderWidth: 1, borderDash: [2, 2], @@ -107,8 +110,8 @@ async function updateGraph(): Promise { family: Config.fontFamily.replace(/_/g, " "), size: 11, style: "normal", - weight: Chart.defaults.font.weight!, - lineHeight: Chart.defaults.font.lineHeight!, + weight: Chart.defaults.font.weight as string, + lineHeight: Chart.defaults.font.lineHeight as number, }, color: fc, padding: 3, @@ -120,10 +123,9 @@ async function updateGraph(): Promise { }); } - ChartController.result.options.scales!["wpm"]!.max = maxChartVal; - ChartController.result.options.scales!["raw"]!.max = maxChartVal; - ChartController.result.options.scales!["error"]!.max = - Math.max(...result.chartData.err) + 1; + resultScaleOptions["wpm"].max = maxChartVal; + resultScaleOptions["raw"].max = maxChartVal; + resultScaleOptions["error"].max = Math.max(...result.chartData.err) + 1; } export async function updateGraphPBLine(): Promise { @@ -141,11 +143,7 @@ export async function updateGraphPBLine(): Promise { const chartlpb = Misc.roundTo2(Config.alwaysShowCPM ? lpb * 5 : lpb).toFixed( 2 ); - - ( - ChartController.result.options.plugins!.annotation! - .annotations as AnnotationOptions[] - ).push({ + resultAnnotation.push({ display: true, type: "line", id: "lpb", @@ -160,8 +158,8 @@ export async function updateGraphPBLine(): Promise { family: Config.fontFamily.replace(/_/g, " "), size: 11, style: "normal", - weight: Chart.defaults.font.weight!, - lineHeight: Chart.defaults.font.lineHeight!, + weight: Chart.defaults.font.weight as string, + lineHeight: Chart.defaults.font.lineHeight as number, }, color: themecolors["bg"], padding: 3, @@ -177,12 +175,8 @@ export async function updateGraphPBLine(): Promise { ) { maxChartVal = parseFloat(chartlpb) + 15; } - ChartController.result.options.scales!["wpm"]!.max = Math.round( - maxChartVal + 5 - ); - ChartController.result.options.scales!["raw"]!.max = Math.round( - maxChartVal + 5 - ); + resultScaleOptions["wpm"].max = Math.round(maxChartVal + 5); + resultScaleOptions["raw"].max = Math.round(maxChartVal + 5); } function updateWpmAndAcc(): void { @@ -424,10 +418,7 @@ function updateTags(dontSave: boolean): void { // console.log("new pb for tag " + tag.name); } else { const themecolors = await ThemeColors.getAll(); - ( - ChartController.result.options.plugins!.annotation! - .annotations as AnnotationOptions[] - ).push({ + resultAnnotation.push({ display: true, type: "line", id: "tpb", @@ -442,8 +433,8 @@ function updateTags(dontSave: boolean): void { family: Config.fontFamily.replace(/_/g, " "), size: 11, style: "normal", - weight: Chart.defaults.font.weight!, - lineHeight: Chart.defaults.font.lineHeight!, + weight: Chart.defaults.font.weight as string, + lineHeight: Chart.defaults.font.lineHeight as number, }, color: themecolors["bg"], padding: 3, diff --git a/frontend/src/scripts/types/types.d.ts b/frontend/src/scripts/types/types.d.ts index 1b2dd24d6..98ed443d4 100644 --- a/frontend/src/scripts/types/types.d.ts +++ b/frontend/src/scripts/types/types.d.ts @@ -130,6 +130,32 @@ declare namespace MonkeyTypes { | CustomLayoutFluid | `${string} ${string} ${string}`; + interface HistoryChartData { + x: number; + y: number; + wpm: number; + acc: number; + mode: string; + mode2: string | number; + punctuation: boolean; + language: string; + timestamp: number; + difficulty: string; + raw: number; + } + + interface AccChartData { + x: number; + y: number; + errorRate: number; + } + + interface ActivityChartDataPoint { + x: number; + y: number; + amount?: number; + } + interface FunboxObject { name: string; type: FunboxObjectType;