Convert chart-controller to TypeScript (#2741) DanGonite57

* Begin converting chart-controller to TypeScript

* Clean up assertions

* Replace non-null assertion

* Add types for trendline plugin

* Remove unused legends

* Shorten tooltip animation

* Fix merge

* Upgrade to @types/chartjs-plugin-trendline 1.0.1

* Re-add mrmime to package-lock

* Corrected merge with result.ts

* More merge fixes

* Switch chart data types to interfaces
This commit is contained in:
DanGonite57 2022-03-24 18:39:17 +00:00 committed by GitHub
parent 8b419be011
commit b076559a57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 930 additions and 860 deletions

View file

@ -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",

View file

@ -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",

View file

@ -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();

View file

@ -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(
`<div class="wordInputAfter">${input
.replace(/\t/g, "_")
.replace(/\n/g, "_")
.replace(/</g, "&lt")
.replace(/>/g, "&gt")}</div>`
);
}
});
} 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);
});

View file

@ -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<TType>,
TLabel = unknown
> extends Chart<TType, TData, TLabel> {
constructor(item: any, config: ChartConfiguration<TType, TData, TLabel>) {
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(
`<div class="wordInputAfter">${input
.replace(/\t/g, "_")
.replace(/\n/g, "_")
.replace(/</g, "&lt")
.replace(/>/g, "&gt")}</div>`
);
}
});
} 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<TType, TData, TLabel>): Promise<void> {
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<TType>;
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<TType>).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);
});

View file

@ -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) {

View file

@ -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<MonkeyTypes.Mode>;
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<void> {
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<void> {
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<void> {
: 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<void> {
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<void> {
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<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: fc,
padding: 3,
@ -120,10 +123,9 @@ async function updateGraph(): Promise<void> {
});
}
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<void> {
@ -141,11 +143,7 @@ export async function updateGraphPBLine(): Promise<void> {
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<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,
@ -177,12 +175,8 @@ export async function updateGraphPBLine(): Promise<void> {
) {
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,

View file

@ -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;