mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-25 07:17:23 +08:00
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:
parent
8b419be011
commit
b076559a57
8 changed files with 930 additions and 860 deletions
19
frontend/package-lock.json
generated
19
frontend/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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, "<")
|
||||
.replace(/>/g, ">")}</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);
|
||||
});
|
||||
797
frontend/src/scripts/controllers/chart-controller.ts
Normal file
797
frontend/src/scripts/controllers/chart-controller.ts
Normal 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, "<")
|
||||
.replace(/>/g, ">")}</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);
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
26
frontend/src/scripts/types/types.d.ts
vendored
26
frontend/src/scripts/types/types.d.ts
vendored
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue