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;