Added configuration for typing speed unit, removed alwaysShowCPM (#4492) fehmer

* Added configuration for typing speed unit, removed alwaysShowCPM

* review comments

* fix live-burst, activityChart and results-pb label

* Added support for typing speed unit in account histogram chart

* trigger build

* Update account.ts

* Fix chart scaling and wpm/rawWpm hovers on result page
fix chart scaling and bucket size on account page

* refactor histogramChartData to support 0.5 steps

* Revert dynamic scaling on accounts/result graph

* Refactor histogramChartData to an int[]

* Fix cutoff in account history

* Fix labels on result page

* Limit result chart label to two decimals

* renamed show average wpm to show average speed

* fix scaling on accounts history graph (again),   not adding an easteregg 🤫

* hiding by default

* fix scaling on accounts history graph episode three

* move typingSpeedUnit related functions out of Misc

* updating account page if typing speed unit changes

* updating result page if changing units on the result page

* missing buton change

---------

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Christian Fehmer 2023-08-04 13:22:27 +02:00 committed by GitHub
parent d4a061400b
commit 1f4df9199d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 411 additions and 293 deletions

View file

@ -106,7 +106,7 @@ const CONFIG_SCHEMA = joi.object({
minWpmCustomSpeed: joi.number().min(0),
highlightMode: joi.string().valid("off", "letter", "word"),
tapeMode: joi.string().valid("off", "letter", "word"),
alwaysShowCPM: joi.boolean(),
typingSpeedUnit: joi.string().valid("wpm", "cpm", "wps", "cps", "wph"),
enableAds: joi.string().valid("off", "on", "max"),
ads: joi.string().valid("off", "result", "on", "sellout"),
hideExtraLetters: joi.boolean(),
@ -128,7 +128,7 @@ const CONFIG_SCHEMA = joi.object({
burstHeatmap: joi.boolean(),
britishEnglish: joi.boolean(),
lazyMode: joi.boolean(),
showAverage: joi.string().valid("off", "wpm", "acc", "both"),
showAverage: joi.string().valid("off", "speed", "acc", "both"),
});
export default CONFIG_SCHEMA;

View file

@ -1,6 +1,7 @@
import Config from "../config";
import format from "date-fns/format";
import * as Misc from "../utils/misc";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
function clearTables(isProfile: boolean): void {
const source = isProfile ? "Profile" : "Account";
@ -127,9 +128,9 @@ function buildPbHtml(
): string {
let retval = "";
let dateText = "";
const multiplier = Config.alwaysShowCPM ? 5 : 1;
const modeString = `${mode2} ${mode === "time" ? "seconds" : "words"}`;
const wpmCpm = Config.alwaysShowCPM ? "cpm" : "wpm";
const speedUnit = Config.typingSpeedUnit;
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
try {
const pbData = (pbs[mode][mode2] ?? []).sort((a, b) => b.wpm - a.wpm)[0];
const date = new Date(pbData.timestamp);
@ -137,15 +138,15 @@ function buildPbHtml(
dateText = format(date, "dd MMM yyyy");
}
let wpmString: number | string = pbData.wpm * multiplier;
let speedString: number | string = typingSpeedUnit.convert(pbData.wpm);
if (Config.alwaysShowDecimalPlaces) {
wpmString = Misc.roundTo2(wpmString).toFixed(2);
speedString = Misc.roundTo2(speedString).toFixed(2);
} else {
wpmString = Math.round(wpmString);
speedString = Math.round(speedString);
}
wpmString += ` ${wpmCpm}`;
speedString += ` ${speedUnit}`;
let rawString: number | string = pbData.raw * multiplier;
let rawString: number | string = typingSpeedUnit.convert(pbData.raw);
if (Config.alwaysShowDecimalPlaces) {
rawString = Misc.roundTo2(rawString).toFixed(2);
} else {
@ -179,14 +180,14 @@ function buildPbHtml(
retval = `<div class="quick">
<div class="test">${modeString}</div>
<div class="wpm">${Math.round(pbData.wpm * multiplier)}</div>
<div class="wpm">${Math.round(typingSpeedUnit.convert(pbData.wpm))}</div>
<div class="acc">${
pbData.acc === undefined ? "-" : Math.floor(pbData.acc) + "%"
}</div>
</div>
<div class="fullTest">
<div>${modeString}</div>
<div>${wpmString}</div>
<div>${speedString}</div>
<div>${rawString}</div>
<div>${accString}</div>
<div>${conString}</div>

View file

@ -28,7 +28,7 @@ import SoundVolumeCommands from "./lists/sound-volume";
import FlipTestColorsCommands from "./lists/flip-test-colors";
import SmoothLineScrollCommands from "./lists/smooth-line-scroll";
import AlwaysShowDecimalCommands from "./lists/always-show-decimal";
import AlwaysShowCpmCommands from "./lists/always-show-cpm";
import TypingSpeedUnitCommands from "./lists/typing-speed-unit";
import StartGraphsAtZeroCommands from "./lists/start-graphs-at-zero";
import LazyModeCommands from "./lists/lazy-mode";
import ShowAllLinesCommands from "./lists/show-all-lines";
@ -275,7 +275,7 @@ export const commands: MonkeyTypes.CommandsSubgroup = {
...TapeModeCommands,
...SmoothLineScrollCommands,
...ShowAllLinesCommands,
...AlwaysShowCpmCommands,
...TypingSpeedUnitCommands,
...AlwaysShowDecimalCommands,
...StartGraphsAtZeroCommands,
...FontSizeCommands,

View file

@ -1,35 +0,0 @@
import * as UpdateConfig from "../../config";
const subgroup: MonkeyTypes.CommandsSubgroup = {
title: "Always show CPM...",
configKey: "alwaysShowCPM",
list: [
{
id: "setAlwaysShowCPMOff",
display: "off",
configValue: false,
exec: (): void => {
UpdateConfig.setAlwaysShowCPM(false);
},
},
{
id: "setAlwaysShowCPMOn",
display: "on",
configValue: true,
exec: (): void => {
UpdateConfig.setAlwaysShowCPM(true);
},
},
],
};
const commands: MonkeyTypes.Command[] = [
{
id: "changeAlwaysShowCPM",
display: "Always show CPM...",
icon: "fa-tachometer-alt",
subgroup,
},
];
export default commands;

View file

@ -14,10 +14,10 @@ const subgroup: MonkeyTypes.CommandsSubgroup = {
},
{
id: "setShowAverageSpeed",
display: "wpm",
configValue: "wpm",
display: "speed",
configValue: "speed",
exec: (): void => {
UpdateConfig.setShowAverage("wpm");
UpdateConfig.setShowAverage("speed");
},
},
{

View file

@ -0,0 +1,60 @@
import * as UpdateConfig from "../../config";
const subgroup: MonkeyTypes.CommandsSubgroup = {
title: "Typing speed unit...",
configKey: "typingSpeedUnit",
list: [
{
id: "setTypingSpeedUnitWpm",
display: "wpm",
configValue: "wpm",
exec: (): void => {
UpdateConfig.setTypingSpeedUnit("wpm");
},
},
{
id: "setTypingSpeedUnitCpm",
display: "cpm",
configValue: "cpm",
exec: (): void => {
UpdateConfig.setTypingSpeedUnit("cpm");
},
},
{
id: "setTypingSpeedUnitWps",
display: "wps",
configValue: "wps",
exec: (): void => {
UpdateConfig.setTypingSpeedUnit("wps");
},
},
{
id: "setTypingSpeedUnitCps",
display: "cps",
configValue: "cps",
exec: (): void => {
UpdateConfig.setTypingSpeedUnit("cps");
},
},
{
id: "setTypingSpeedUnitWph",
display: "wph",
configValue: "wph",
visible: false,
exec: (): void => {
UpdateConfig.setTypingSpeedUnit("wph");
},
},
],
};
const commands: MonkeyTypes.Command[] = [
{
id: "changeTypingSpeedUnit",
display: "Typing speed unit...",
icon: "fa-tachometer-alt",
subgroup,
},
];
export default commands;

View file

@ -409,12 +409,20 @@ export function setAlwaysShowDecimalPlaces(
return true;
}
export function setAlwaysShowCPM(val: boolean, nosave?: boolean): boolean {
if (!isConfigValueValid("always show CPM", val, ["boolean"])) return false;
config.alwaysShowCPM = val;
saveToLocalStorage("alwaysShowCPM", nosave);
ConfigEvent.dispatch("alwaysShowCPM", config.alwaysShowCPM);
export function setTypingSpeedUnit(
val: MonkeyTypes.TypingSpeedUnit,
nosave?: boolean
): boolean {
if (
!isConfigValueValid("typing speed unit", val, [
["wpm", "cpm", "wps", "cps", "wph"],
])
) {
return false;
}
config.typingSpeedUnit = val;
saveToLocalStorage("typingSpeedUnit", nosave);
ConfigEvent.dispatch("typingSpeedUnit", config.typingSpeedUnit, nosave);
return true;
}
@ -888,7 +896,9 @@ export function setShowAverage(
nosave?: boolean
): boolean {
if (
!isConfigValueValid("show average", value, [["off", "wpm", "acc", "both"]])
!isConfigValueValid("show average", value, [
["off", "speed", "acc", "both"],
])
) {
return false;
}
@ -1901,7 +1911,7 @@ export function apply(
setNumbers(configObj.numbers, true);
setPunctuation(configObj.punctuation, true);
setHighlightMode(configObj.highlightMode, true);
setAlwaysShowCPM(configObj.alwaysShowCPM, true);
setTypingSpeedUnit(configObj.typingSpeedUnit, true);
setHideExtraLetters(configObj.hideExtraLetters, true);
setStartGraphsAtZero(configObj.startGraphsAtZero, true);
setStrictSpace(configObj.strictSpace, true);
@ -1976,6 +1986,16 @@ function replaceLegacyValues(
configObj.quickRestart = "esc";
}
//@ts-ignore
if (configObj.alwaysShowCPM === true) {
configObj.typingSpeedUnit = "cpm";
}
//@ts-ignore
if (configObj.showAverage === "wpm") {
configObj.showAverage = "speed";
}
return configObj;
}

View file

@ -73,7 +73,7 @@ export default <MonkeyTypes.Config>{
minWpm: "off",
minWpmCustomSpeed: 100,
highlightMode: "letter",
alwaysShowCPM: false,
typingSpeedUnit: "wpm",
ads: "result",
hideExtraLetters: false,
strictSpace: false,

View file

@ -343,6 +343,7 @@ export const accountHistory: ChartWithUpdateColors<
},
wpm: {
axis: "y",
type: "linear",
beginAtZero: true,
min: 0,
ticks: {
@ -478,7 +479,7 @@ export const accountHistory: ChartWithUpdateColors<
tooltipItem.dataIndex
] as MonkeyTypes.HistoryChartData;
let label =
`${Config.alwaysShowCPM ? "cpm" : "wpm"}: ${resultData.wpm}` +
`${Config.typingSpeedUnit}: ${resultData.wpm}` +
"\n" +
`raw: ${resultData.raw}` +
"\n" +
@ -652,9 +653,9 @@ export const accountActivity: ChartWithUpdateColors<
true
)}\nTests Completed: ${resultData.amount}`;
case 1:
return `Average ${
Config.alwaysShowCPM ? "Cpm" : "Wpm"
}: ${Misc.roundTo2(resultData.y)}`;
return `Average ${Config.typingSpeedUnit}: ${Misc.roundTo2(
resultData.y
)}`;
default:
return "";
}
@ -755,7 +756,7 @@ export const accountHistogram: ChartWithUpdateColors<
// )}\nTests Completed: ${resultData.amount}`;
// case 1:
// return `Average ${
// Config.alwaysShowCPM ? "Cpm" : "Wpm"
// Config.typingSpeedUnit
// }: ${Misc.roundTo2(resultData.y)}`;
// default:
// return "";

View file

@ -2,6 +2,7 @@ import Ape from "../ape";
import * as DB from "../db";
import Config from "../config";
import * as Misc from "../utils/misc";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import * as Notifications from "./notifications";
import format from "date-fns/format";
import { Auth } from "../firebase";
@ -150,6 +151,7 @@ function updateFooter(lb: LbKey): void {
return;
}
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
if (DB.getSnapshot()?.lbOptOut === true) {
$(`#leaderboardsWrapper table.${side} tfoot`).html(`
<tr>
@ -185,14 +187,10 @@ function updateFooter(lb: LbKey): void {
<tr>
<td>${entry.rank}</td>
<td><span class="top">You</span>${toppercent ? toppercent : ""}</td>
<td class="alignRight">${(Config.alwaysShowCPM
? entry.wpm * 5
: entry.wpm
).toFixed(2)}<br><div class="sub">${entry.acc.toFixed(2)}%</div></td>
<td class="alignRight">${(Config.alwaysShowCPM
? entry.raw * 5
: entry.raw
).toFixed(2)}<br><div class="sub">${
<td class="alignRight">${typingSpeedUnit.convert(entry.wpm).toFixed(2)}<br>
<div class="sub">${entry.acc.toFixed(2)}%</div></td>
<td class="alignRight">${typingSpeedUnit.convert(entry.raw).toFixed(2)}<br>
<div class="sub">${
!entry.consistency || entry.consistency === "-"
? "-"
: entry.consistency.toFixed(2) + "%"
@ -264,6 +262,7 @@ async function fillTable(lb: LbKey, prepend?: number): Promise<void> {
);
}
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
const loggedInUserName = DB.getSnapshot()?.name;
const snap = DB.getSnapshot();
@ -342,14 +341,10 @@ async function fillTable(lb: LbKey, prepend?: number): Promise<void> {
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
</div>
</td>
<td class="alignRight">${(Config.alwaysShowCPM
? entry.wpm * 5
: entry.wpm
).toFixed(2)}<br><div class="sub">${entry.acc.toFixed(2)}%</div></td>
<td class="alignRight">${(Config.alwaysShowCPM
? entry.raw * 5
: entry.raw
).toFixed(2)}<br><div class="sub">${
<td class="alignRight">${typingSpeedUnit.convert(entry.wpm).toFixed(2)}<br>
<div class="sub">${entry.acc.toFixed(2)}%</div></td>
<td class="alignRight">${typingSpeedUnit.convert(entry.raw).toFixed(2)}<br>
<div class="sub">${
!entry.consistency || entry.consistency === "-"
? "-"
: entry.consistency.toFixed(2) + "%"
@ -608,15 +603,9 @@ export function show(): void {
"disabled"
);
}
if (Config.alwaysShowCPM) {
$("#leaderboards table thead tr td:nth-child(3)").html(
'cpm<br><div class="sub">accuracy</div>'
);
} else {
$("#leaderboards table thead tr td:nth-child(3)").html(
'wpm<br><div class="sub">accuracy</div>'
);
}
$("#leaderboards table thead tr td:nth-child(3)").html(
Config.typingSpeedUnit + '<br><div class="sub">accuracy</div>'
);
$("#leaderboardsWrapper")
.stop(true, true)
.css("opacity", 0)

View file

@ -7,6 +7,7 @@ import * as TestWords from "../test/test-words";
import * as ConfigEvent from "../observables/config-event";
import { Auth } from "../firebase";
import * as CustomTextState from "../states/custom-text-name";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
ConfigEvent.subscribe((eventKey) => {
if (
@ -21,7 +22,7 @@ ConfigEvent.subscribe((eventKey) => {
"confidenceMode",
"layout",
"showAverage",
"alwaysShowCPM",
"typingSpeedUnit",
].includes(eventKey)
) {
update();
@ -142,10 +143,10 @@ export async function update(): Promise<void> {
}
if (Auth?.currentUser && avgWPM > 0) {
const avgWPMText = ["wpm", "both"].includes(Config.showAverage)
? Config.alwaysShowCPM
? `${Math.round(avgWPM * 5)} cpm`
: `${avgWPM} wpm`
const avgWPMText = ["speed", "both"].includes(Config.showAverage)
? getTypingSpeedUnit(Config.typingSpeedUnit).convertWithUnitSuffix(
avgWPM
)
: "";
const avgAccText = ["acc", "both"].includes(Config.showAverage)

View file

@ -12,11 +12,14 @@ import * as TodayTracker from "../test/today-tracker";
import * as Notifications from "../elements/notifications";
import Page from "./page";
import * as Misc from "../utils/misc";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import * as Profile from "../elements/profile";
import format from "date-fns/format";
import * as ConnectionState from "../states/connection";
import * as Skeleton from "../popups/skeleton";
import type { ScaleChartOptions } from "chart.js";
import type { ScaleChartOptions, LinearScaleOptions } from "chart.js";
import * as ConfigEvent from "../observables/config-event";
import * as ActivePage from "../states/active-page";
import { Auth } from "../firebase";
let filterDebug = false;
@ -32,6 +35,7 @@ let filteredResults: MonkeyTypes.Result<MonkeyTypes.Mode>[] = [];
let visibleTableLines = 0;
function loadMoreLines(lineIndex?: number): void {
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
if (!filteredResults || filteredResults.length === 0) return;
let newVisibleLines;
if (lineIndex && lineIndex > visibleTableLines) {
@ -49,9 +53,7 @@ function loadMoreLines(lineIndex?: number): void {
let raw;
try {
raw = Config.alwaysShowCPM
? (result.rawWpm * 5).toFixed(2)
: result.rawWpm.toFixed(2);
raw = typingSpeedUnit.convert(result.rawWpm).toFixed(2);
if (raw === undefined) {
raw = "-";
}
@ -159,7 +161,7 @@ function loadMoreLines(lineIndex?: number): void {
$(".pageAccount .history table tbody").append(`
<tr class="resultRow" id="result-${i}">
<td>${pb}</td>
<td>${(Config.alwaysShowCPM ? result.wpm * 5 : result.wpm).toFixed(2)}</td>
<td>${typingSpeedUnit.convert(result.wpm).toFixed(2)}</td>
<td>${raw}</td>
<td>${result.acc.toFixed(2)}%</td>
<td>${consistency}</td>
@ -264,13 +266,9 @@ async function fillContent(): Promise<void> {
};
}
interface HistogramChartData {
[key: string]: number;
}
const activityChartData: ActivityChartData = {};
const histogramChartData: HistogramChartData = {};
const histogramChartData: number[] = [];
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
filteredResults = [];
$(".pageAccount .history table tbody").empty();
@ -538,13 +536,18 @@ async function fillContent(): Promise<void> {
};
}
const bucket = Math.floor(Math.round(result.wpm) / 10) * 10;
const bucketSize = typingSpeedUnit.histogramDataBucketSize;
const bucket = Math.floor(
typingSpeedUnit.convert(result.wpm) / bucketSize
);
if (Object.keys(histogramChartData).includes(String(bucket))) {
histogramChartData[bucket]++;
} else {
histogramChartData[bucket] = 1;
//grow array if needed
if (histogramChartData.length <= bucket) {
for (let i = histogramChartData.length; i <= bucket; i++) {
histogramChartData.push(0);
}
}
histogramChartData[bucket]++;
let tt = 0;
if (
@ -616,8 +619,8 @@ async function fillContent(): Promise<void> {
chartData.push({
x: filteredResults.length,
y: Config.alwaysShowCPM ? Misc.roundTo2(result.wpm * 5) : result.wpm,
wpm: Config.alwaysShowCPM ? Misc.roundTo2(result.wpm * 5) : result.wpm,
y: Misc.roundTo2(typingSpeedUnit.convert(result.wpm)),
wpm: Misc.roundTo2(typingSpeedUnit.convert(result.wpm)),
acc: result.acc,
mode: result.mode,
mode2: result.mode2,
@ -625,9 +628,7 @@ async function fillContent(): Promise<void> {
language: result.language,
timestamp: result.timestamp,
difficulty: result.difficulty,
raw: Config.alwaysShowCPM
? Misc.roundTo2(result.rawWpm * 5)
: result.rawWpm,
raw: Misc.roundTo2(typingSpeedUnit.convert(result.rawWpm)),
isPb: result.isPb ?? false,
});
@ -656,11 +657,9 @@ async function fillContent(): Promise<void> {
}
);
if (Config.alwaysShowCPM) {
$(".pageAccount .group.history table thead tr td:nth-child(2)").text("cpm");
} else {
$(".pageAccount .group.history table thead tr td:nth-child(2)").text("wpm");
}
$(".pageAccount .group.history table thead tr td:nth-child(2)").text(
Config.typingSpeedUnit
);
await Misc.sleep(0);
loadMoreLines();
@ -684,9 +683,7 @@ async function fillContent(): Promise<void> {
activityChartData_avgWpm.push({
x: dateInt,
y: Misc.roundTo2(
(Config.alwaysShowCPM
? activityChartData[dateInt].totalWpm * 5
: activityChartData[dateInt].totalWpm) /
typingSpeedUnit.convert(activityChartData[dateInt].totalWpm) /
activityChartData[dateInt].amount
),
});
@ -697,11 +694,8 @@ async function fillContent(): Promise<void> {
ChartController.accountActivity.options as ScaleChartOptions<"bar" | "line">
).scales;
if (Config.alwaysShowCPM) {
accountActivityScaleOptions["avgWpm"].title.text = "Average Cpm";
} else {
accountActivityScaleOptions["avgWpm"].title.text = "Average Wpm";
}
accountActivityScaleOptions["avgWpm"].title.text =
"Average " + Config.typingSpeedUnit;
ChartController.accountActivity.data.datasets[0].data =
activityChartData_time;
@ -711,21 +705,17 @@ async function fillContent(): Promise<void> {
const histogramChartDataBucketed: { x: number; y: number }[] = [];
const labels: string[] = [];
const keys = Object.keys(histogramChartData);
for (let i = 0; i < keys.length; i++) {
const bucket = parseInt(keys[i]);
labels.push(`${bucket} - ${bucket + 9}`);
const bucketSize = typingSpeedUnit.histogramDataBucketSize;
const bucketSizeUpperBound = bucketSize - (bucketSize <= 1 ? 0.01 : 1);
histogramChartData.forEach((amount: number, i: number) => {
const bucket = i * bucketSize;
labels.push(`${bucket} - ${bucket + bucketSizeUpperBound}`);
histogramChartDataBucketed.push({
x: bucket,
y: histogramChartData[bucket],
y: amount,
});
if (bucket + 10 !== parseInt(keys[i + 1])) {
for (let j = bucket + 10; j < parseInt(keys[i + 1]); j += 10) {
histogramChartDataBucketed.push({ x: j, y: 0 });
labels.push(`${j} - ${j + 9}`);
}
}
}
});
ChartController.accountHistogram.data.labels = labels;
ChartController.accountHistogram.data.datasets[0].data =
@ -735,11 +725,10 @@ async function fillContent(): Promise<void> {
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";
}
const accountHistoryWpmOptions = accountHistoryScaleOptions[
"wpm"
] as LinearScaleOptions;
accountHistoryWpmOptions.title.text = typingSpeedUnit.fullUnitString;
if (chartData.length > 0) {
// get pb points
@ -803,26 +792,29 @@ async function fillContent(): Promise<void> {
const wpms = chartData.map((r) => r.y);
const minWpmChartVal = Math.min(...wpms);
const maxWpmChartVal = Math.max(...wpms);
const wpmStepSize = typingSpeedUnit.historyStepSize;
const maxWpmChartValWithBuffer =
Math.floor(maxWpmChartVal) +
(wpmStepSize - (Math.floor(maxWpmChartVal) % wpmStepSize));
// let accuracies = accChartData.map((r) => r.y);
accountHistoryScaleOptions["wpm"].max =
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
accountHistoryScaleOptions["pb"].max =
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
accountHistoryScaleOptions["wpmAvgTen"].max =
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
accountHistoryScaleOptions["wpmAvgHundred"].max =
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
accountHistoryWpmOptions.max = maxWpmChartValWithBuffer;
accountHistoryWpmOptions.ticks.stepSize = wpmStepSize;
accountHistoryScaleOptions["pb"].max = maxWpmChartValWithBuffer;
accountHistoryScaleOptions["wpmAvgTen"].max = maxWpmChartValWithBuffer;
accountHistoryScaleOptions["wpmAvgHundred"].max = maxWpmChartValWithBuffer;
if (!Config.startGraphsAtZero) {
const minWpmChartValFloor = Math.floor(minWpmChartVal);
accountHistoryScaleOptions["wpm"].min = minWpmChartValFloor;
accountHistoryWpmOptions.min = minWpmChartValFloor;
accountHistoryScaleOptions["pb"].min = minWpmChartValFloor;
accountHistoryScaleOptions["wpmAvgTen"].min = minWpmChartValFloor;
accountHistoryScaleOptions["wpmAvgHundred"].min = minWpmChartValFloor;
} else {
accountHistoryScaleOptions["wpm"].min = 0;
accountHistoryWpmOptions.min = 0;
accountHistoryScaleOptions["pb"].min = 0;
accountHistoryScaleOptions["wpmAvgTen"].min = 0;
accountHistoryScaleOptions["wpmAvgHundred"].min = 0;
@ -852,38 +844,31 @@ async function fillContent(): Promise<void> {
Misc.secondsToString(Math.round(totalSecondsFiltered), true, true)
);
let highestSpeed: number | string = topWpm;
if (Config.alwaysShowCPM) {
highestSpeed = topWpm * 5;
}
let highestSpeed: number | string = typingSpeedUnit.convert(topWpm);
if (Config.alwaysShowDecimalPlaces) {
highestSpeed = Misc.roundTo2(highestSpeed).toFixed(2);
} else {
highestSpeed = Math.round(highestSpeed);
}
const wpmCpm = Config.alwaysShowCPM ? "cpm" : "wpm";
const speedUnit = Config.typingSpeedUnit;
$(".pageAccount .highestWpm .title").text(`highest ${wpmCpm}`);
$(".pageAccount .highestWpm .title").text(`highest ${speedUnit}`);
$(".pageAccount .highestWpm .val").text(highestSpeed);
let averageSpeed: number | string = totalWpm;
if (Config.alwaysShowCPM) {
averageSpeed = totalWpm * 5;
}
let averageSpeed: number | string = typingSpeedUnit.convert(totalWpm);
if (Config.alwaysShowDecimalPlaces) {
averageSpeed = Misc.roundTo2(averageSpeed / testCount).toFixed(2);
} else {
averageSpeed = Math.round(averageSpeed / testCount);
}
$(".pageAccount .averageWpm .title").text(`average ${wpmCpm}`);
$(".pageAccount .averageWpm .title").text(`average ${speedUnit}`);
$(".pageAccount .averageWpm .val").text(averageSpeed);
let averageSpeedLast10: number | string = wpmLast10total;
if (Config.alwaysShowCPM) {
averageSpeedLast10 = wpmLast10total * 5;
}
let averageSpeedLast10: number | string =
typingSpeedUnit.convert(wpmLast10total);
if (Config.alwaysShowDecimalPlaces) {
averageSpeedLast10 = Misc.roundTo2(averageSpeedLast10 / last10).toFixed(2);
} else {
@ -891,40 +876,33 @@ async function fillContent(): Promise<void> {
}
$(".pageAccount .averageWpm10 .title").text(
`average ${wpmCpm} (last 10 tests)`
`average ${speedUnit} (last 10 tests)`
);
$(".pageAccount .averageWpm10 .val").text(averageSpeedLast10);
let highestRawSpeed: number | string = rawWpm.max;
if (Config.alwaysShowCPM) {
highestRawSpeed = rawWpm.max * 5;
}
let highestRawSpeed: number | string = typingSpeedUnit.convert(rawWpm.max);
if (Config.alwaysShowDecimalPlaces) {
highestRawSpeed = Misc.roundTo2(highestRawSpeed).toFixed(2);
} else {
highestRawSpeed = Math.round(highestRawSpeed);
}
$(".pageAccount .highestRaw .title").text(`highest raw ${wpmCpm}`);
$(".pageAccount .highestRaw .title").text(`highest raw ${speedUnit}`);
$(".pageAccount .highestRaw .val").text(highestRawSpeed);
let averageRawSpeed: number | string = rawWpm.total;
if (Config.alwaysShowCPM) {
averageRawSpeed = rawWpm.total * 5;
}
let averageRawSpeed: number | string = typingSpeedUnit.convert(rawWpm.total);
if (Config.alwaysShowDecimalPlaces) {
averageRawSpeed = Misc.roundTo2(averageRawSpeed / rawWpm.count).toFixed(2);
} else {
averageRawSpeed = Math.round(averageRawSpeed / rawWpm.count);
}
$(".pageAccount .averageRaw .title").text(`average raw ${wpmCpm}`);
$(".pageAccount .averageRaw .title").text(`average raw ${speedUnit}`);
$(".pageAccount .averageRaw .val").text(averageRawSpeed);
let averageRawSpeedLast10: number | string = rawWpm.last10Total;
if (Config.alwaysShowCPM) {
averageRawSpeedLast10 = rawWpm.last10Total * 5;
}
let averageRawSpeedLast10: number | string = typingSpeedUnit.convert(
rawWpm.last10Total
);
if (Config.alwaysShowDecimalPlaces) {
averageRawSpeedLast10 = Misc.roundTo2(
averageRawSpeedLast10 / rawWpm.last10Count
@ -936,7 +914,7 @@ async function fillContent(): Promise<void> {
}
$(".pageAccount .averageRaw10 .title").text(
`average raw ${wpmCpm} (last 10 tests)`
`average raw ${speedUnit} (last 10 tests)`
);
$(".pageAccount .averageRaw10 .val").text(averageRawSpeedLast10);
@ -1029,11 +1007,8 @@ async function fillContent(): Promise<void> {
$(".pageAccount .group.chart .below .text").text(
`Speed change per hour spent typing: ${
plus +
Misc.roundTo2(
Config.alwaysShowCPM ? wpmChangePerHour * 5 : wpmChangePerHour
)
} ${Config.alwaysShowCPM ? "cpm" : "wpm"}`
plus + Misc.roundTo2(typingSpeedUnit.convert(wpmChangePerHour))
} ${Config.typingSpeedUnit}`
);
$(".pageAccount .estimatedWordsTyped .val").text(totalEstimatedWords);
@ -1312,6 +1287,12 @@ $(".pageAccount .profile").on("click", ".details .copyLink", () => {
);
});
ConfigEvent.subscribe((eventKey) => {
if (ActivePage.get() === "account" && eventKey === "typingSpeedUnit") {
update();
}
});
export const page = new Page(
"account",
$(".page.pageAccount"),

View file

@ -379,9 +379,9 @@ async function initGroups(): Promise<void> {
UpdateConfig.setAlwaysShowDecimalPlaces,
"button"
) as SettingsGroup<MonkeyTypes.ConfigValues>;
groups["alwaysShowCPM"] = new SettingsGroup(
"alwaysShowCPM",
UpdateConfig.setAlwaysShowCPM,
groups["typingSpeedUnit"] = new SettingsGroup(
"typingSpeedUnit",
UpdateConfig.setTypingSpeedUnit,
"button"
) as SettingsGroup<MonkeyTypes.ConfigValues>;
groups["customBackgroundSize"] = new SettingsGroup(

View file

@ -1,9 +1,11 @@
import Config from "../config";
import * as TestState from "../test/test-state";
import * as ConfigEvent from "../observables/config-event";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
export async function update(burst: number): Promise<void> {
if (!Config.showLiveBurst) return;
burst = Math.round(getTypingSpeedUnit(Config.typingSpeedUnit).convert(burst));
(document.querySelector("#miniTimerAndLiveWpm .burst") as Element).innerHTML =
burst.toString();
(document.querySelector("#liveBurst") as Element).innerHTML =

View file

@ -1,6 +1,7 @@
import Config from "../config";
import * as TestState from "../test/test-state";
import * as ConfigEvent from "../observables/config-event";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
const liveWpmElement = document.querySelector("#liveWpm") as Element;
const miniLiveWpmElement = document.querySelector(
@ -12,9 +13,9 @@ export function update(wpm: number, raw: number): void {
if (Config.blindMode) {
number = raw;
}
if (Config.alwaysShowCPM) {
number = Math.round(number * 5);
}
number = Math.round(
getTypingSpeedUnit(Config.typingSpeedUnit).convert(number)
);
miniLiveWpmElement.innerHTML = number.toString();
liveWpmElement.innerHTML = number.toString();
}

View file

@ -17,6 +17,7 @@ import * as QuoteRatePopup from "../popups/quote-rate-popup";
import * as GlarsesMode from "../states/glarses-mode";
import * as SlowTimer from "../states/slow-timer";
import * as Misc from "../utils/misc";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import * as FunboxList from "./funbox/funbox-list";
import * as PbCrown from "./pb-crown";
import * as TestConfig from "./test-config";
@ -24,6 +25,7 @@ import * as TestInput from "./test-input";
import * as TestStats from "./test-stats";
import * as TestUI from "./test-ui";
import * as TodayTracker from "./today-tracker";
import * as ConfigEvent from "../observables/config-event";
import confetti from "canvas-confetti";
import type { AnnotationOptions } from "chartjs-plugin-annotation";
@ -48,7 +50,9 @@ let resultScaleOptions = (
).scales;
async function updateGraph(): Promise<void> {
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
const labels = [];
for (let i = 1; i <= TestInput.wpmHistory.length; i++) {
if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) {
labels.push(Misc.roundTo2(result.testDuration).toString());
@ -56,21 +60,20 @@ async function updateGraph(): Promise<void> {
labels.push(i.toString());
}
}
resultScaleOptions["wpm"].title.text = Config.alwaysShowCPM
? "Characters per Minute"
: "Words per Minute";
resultScaleOptions["wpm"].title.text = typingSpeedUnit.fullUnitString;
const chartData1 = [
...(Config.alwaysShowCPM
? TestInput.wpmHistory.map((a) => a * 5)
: TestInput.wpmHistory),
...TestInput.wpmHistory.map((a) =>
Misc.roundTo2(typingSpeedUnit.convert(a))
),
];
if (result.chartData === "toolong") return;
const chartData2 = [
...(Config.alwaysShowCPM
? result.chartData.raw.map((a) => a * 5)
: result.chartData.raw),
...result.chartData.raw.map((a) =>
Misc.roundTo2(typingSpeedUnit.convert(a))
),
];
if (
@ -92,14 +95,12 @@ async function updateGraph(): Promise<void> {
ChartController.result.data.labels = labels;
ChartController.result.data.datasets[0].data = chartData1;
ChartController.result.data.datasets[1].data = smoothedRawData;
ChartController.result.data.datasets[0].label = Config.alwaysShowCPM
? "cpm"
: "wpm";
ChartController.result.data.datasets[0].label = Config.typingSpeedUnit;
maxChartVal = Math.max(
...[Math.max(...smoothedRawData), Math.max(...chartData1)]
);
if (!Config.startGraphsAtZero) {
const minChartVal = Math.min(
...[Math.min(...smoothedRawData), Math.min(...chartData1)]
@ -169,9 +170,8 @@ export async function updateGraphPBLine(): Promise<void> {
result.funbox ?? "none"
);
if (lpb === 0) return;
const chartlpb = Misc.roundTo2(Config.alwaysShowCPM ? lpb * 5 : lpb).toFixed(
2
);
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
const chartlpb = Misc.roundTo2(typingSpeedUnit.convert(lpb)).toFixed(2);
resultAnnotation.push({
display: true,
type: "line",
@ -198,54 +198,49 @@ export async function updateGraphPBLine(): Promise<void> {
content: `PB: ${chartlpb}`,
},
});
const lpbRange = typingSpeedUnit.convert(20);
if (
maxChartVal >= parseFloat(chartlpb) - 20 &&
maxChartVal <= parseFloat(chartlpb) + 20
maxChartVal >= parseFloat(chartlpb) - lpbRange &&
maxChartVal <= parseFloat(chartlpb) + lpbRange
) {
maxChartVal = parseFloat(chartlpb) + 15;
maxChartVal = Math.round(parseFloat(chartlpb) + lpbRange);
}
resultScaleOptions["wpm"].max = Math.round(maxChartVal + 5);
resultScaleOptions["raw"].max = Math.round(maxChartVal + 5);
resultScaleOptions["wpm"].max = maxChartVal;
resultScaleOptions["raw"].max = maxChartVal;
}
function updateWpmAndAcc(): void {
let inf = false;
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
if (result.wpm >= 1000) {
inf = true;
}
if (Config.alwaysShowDecimalPlaces) {
if (Config.alwaysShowCPM === false) {
$("#result .stats .wpm .top .text").text("wpm");
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(
Misc.roundTo2(result.wpm).toFixed(2)
);
}
$("#result .stats .raw .bottom").text(
Misc.roundTo2(result.rawWpm).toFixed(2)
$("#result .stats .wpm .top .text").text(Config.typingSpeedUnit);
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(
Misc.roundTo2(typingSpeedUnit.convert(result.wpm)).toFixed(2)
);
}
$("#result .stats .raw .bottom").text(
Misc.roundTo2(typingSpeedUnit.convert(result.rawWpm)).toFixed(2)
);
if (Config.typingSpeedUnit != "wpm") {
$("#result .stats .wpm .bottom").attr(
"aria-label",
Misc.roundTo2(result.wpm * 5).toFixed(2) + " cpm"
result.wpm.toFixed(2) + " wpm"
);
$("#result .stats .raw .bottom").attr(
"aria-label",
result.rawWpm.toFixed(2) + " wpm"
);
} else {
$("#result .stats .wpm .top .text").text("cpm");
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(
Misc.roundTo2(result.wpm * 5).toFixed(2)
);
}
$("#result .stats .raw .bottom").text(
Misc.roundTo2(result.rawWpm * 5).toFixed(2)
);
$("#result .stats .wpm .bottom").attr(
"aria-label",
Misc.roundTo2(result.wpm).toFixed(2) + " wpm"
);
$("#result .stats .wpm .bottom").removeAttr("aria-label");
$("#result .stats .raw .bottom").removeAttr("aria-label");
}
$("#result .stats .acc .bottom").text(
@ -256,7 +251,6 @@ function updateWpmAndAcc(): void {
time = Misc.secondsToString(Misc.roundTo2(result.testDuration));
}
$("#result .stats .time .bottom .text").text(time);
$("#result .stats .raw .bottom").removeAttr("aria-label");
// $("#result .stats .acc .bottom").removeAttr("aria-label");
$("#result .stats .acc .bottom").attr(
@ -265,34 +259,27 @@ function updateWpmAndAcc(): void {
);
} else {
//not showing decimal places
if (Config.alwaysShowCPM === false) {
$("#result .stats .wpm .top .text").text("wpm");
$("#result .stats .wpm .bottom").attr(
"aria-label",
result.wpm + ` (${Misc.roundTo2(result.wpm * 5)} cpm)`
);
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(Math.round(result.wpm));
}
$("#result .stats .raw .bottom").text(Math.round(result.rawWpm));
$("#result .stats .raw .bottom").attr("aria-label", result.rawWpm);
} else {
$("#result .stats .wpm .top .text").text("cpm");
$("#result .stats .wpm .bottom").attr(
"aria-label",
Misc.roundTo2(result.wpm * 5) + ` (${Misc.roundTo2(result.wpm)} wpm)`
);
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(Math.round(result.wpm * 5));
}
$("#result .stats .raw .bottom").text(Math.round(result.rawWpm * 5));
$("#result .stats .raw .bottom").attr("aria-label", result.rawWpm * 5);
let wpmHover = typingSpeedUnit.convertWithUnitSuffix(result.wpm);
let rawWpmHover = typingSpeedUnit.convertWithUnitSuffix(result.rawWpm);
if (Config.typingSpeedUnit != "wpm") {
wpmHover += " (" + result.wpm.toFixed(2) + " wpm)";
rawWpmHover += " (" + result.rawWpm.toFixed(2) + " wpm)";
}
$("#result .stats .wpm .top .text").text(Config.typingSpeedUnit);
$("#result .stats .wpm .bottom").attr("aria-label", wpmHover);
if (inf) {
$("#result .stats .wpm .bottom").text("Infinite");
} else {
$("#result .stats .wpm .bottom").text(
Math.round(typingSpeedUnit.convert(result.wpm))
);
}
$("#result .stats .raw .bottom").text(
Math.round(typingSpeedUnit.convert(result.rawWpm))
);
$("#result .stats .raw .bottom").attr("aria-label", rawWpmHover);
$("#result .stats .acc .bottom").text(Math.floor(result.acc) + "%");
$("#result .stats .acc .bottom").attr(
"aria-label",
@ -424,10 +411,11 @@ export async function updateCrown(): Promise<void> {
Config.lazyMode,
Config.funbox
);
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
pbDiff = Math.abs(result.wpm - lpb);
$("#result .stats .wpm .crown").attr(
"aria-label",
"+" + Misc.roundTo2(pbDiff)
"+" + Misc.roundTo2(typingSpeedUnit.convert(pbDiff))
);
}
@ -482,6 +470,7 @@ async function updateTags(dontSave: boolean): Promise<void> {
$("#result .stats .tags .bottom").append(`
<div tagid="${tag._id}" aria-label="PB: ${tpb}" data-balloon-pos="up">${tag.display}<i class="fas fa-crown hidden"></i></div>
`);
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
if (
Config.mode !== "quote" &&
!dontSave &&
@ -517,7 +506,7 @@ async function updateTags(dontSave: boolean): Promise<void> {
type: "line",
id: "tpb",
scaleID: "wpm",
value: Config.alwaysShowCPM ? tpb * 5 : tpb,
value: typingSpeedUnit.convert(tpb),
borderColor: themecolors["sub"],
borderWidth: 1,
borderDash: [2, 2],
@ -537,7 +526,7 @@ async function updateTags(dontSave: boolean): Promise<void> {
xAdjust: labelAdjust,
enabled: true,
content: `${tag.display} PB: ${Misc.roundTo2(
Config.alwaysShowCPM ? tpb * 5 : tpb
typingSpeedUnit.convert(tpb)
).toFixed(2)}`,
},
});
@ -888,3 +877,22 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => {
}
}
});
ConfigEvent.subscribe(async (eventKey) => {
if (eventKey === "typingSpeedUnit" && TestUI.resultVisible) {
resultScaleOptions = (
ChartController.result.options as ScaleChartOptions<"line" | "scatter">
).scales;
resultAnnotation = [];
updateWpmAndAcc();
await updateGraph();
await updateGraphPBLine();
((ChartController.result.options as PluginChartOptions<"line" | "scatter">)
.plugins.annotation.annotations as AnnotationOptions<"line">[]) =
resultAnnotation;
ChartController.result.updateColors();
ChartController.result.resize();
}
});

View file

@ -9,6 +9,7 @@ import * as Caret from "./caret";
import * as OutOfFocus from "./out-of-focus";
import * as Replay from "./replay";
import * as Misc from "../utils/misc";
import { get as getTypingSpeedUnit } from "../utils/typing-speed-units";
import * as SlowTimer from "../states/slow-timer";
import * as CompositionState from "../states/composition";
import * as ConfigEvent from "../observables/config-event";
@ -1244,9 +1245,9 @@ $(".pageTest #resultWordsHistory").on("mouseenter", ".words .word", (e) => {
.replace(/>/g, "&gt")}
</div>
<div class="speed">
${Math.round(Config.alwaysShowCPM ? burst * 5 : burst)}${
Config.alwaysShowCPM ? "cpm" : "wpm"
}
${Math.round(
getTypingSpeedUnit(Config.typingSpeedUnit).convert(burst)
)}${Config.typingSpeedUnit}
</div>
</div>`
);

View file

@ -89,7 +89,7 @@ declare namespace MonkeyTypes {
type KeymapShowTopRow = "always" | "layout" | "never";
type ShowAverage = "off" | "wpm" | "acc" | "both";
type ShowAverage = "off" | "speed" | "acc" | "both";
type SmoothCaretMode = "off" | "slow" | "medium" | "fast";
@ -469,7 +469,7 @@ declare namespace MonkeyTypes {
minWpm: MinimumWordsPerMinute;
minWpmCustomSpeed: number;
highlightMode: HighlightMode;
alwaysShowCPM: boolean;
typingSpeedUnit: TypingSpeedUnit;
ads: Ads;
hideExtraLetters: boolean;
strictSpace: boolean;
@ -881,4 +881,13 @@ declare namespace MonkeyTypes {
}
type AllRewards = XpReward | BadgeReward;
type TypingSpeedUnit = "wpm" | "cpm" | "wps" | "cps" | "wph";
interface TypingSpeedUnitSettings {
convert: (number) => number;
convertWithUnitSuffix: (number) => string;
fullUnitString: string;
histogramDataBucketSize: number;
historyStepSize: number;
}
}

View file

@ -0,0 +1,66 @@
import { roundTo2 } from "../utils/misc";
const typingSpeedUnits: Record<
MonkeyTypes.TypingSpeedUnit,
MonkeyTypes.TypingSpeedUnitSettings
> = {
wpm: {
convert: (wpm: number) => wpm,
convertWithUnitSuffix: (wpm: number) => {
return convertTypingSpeedWithUnitSuffix("wpm", wpm);
},
fullUnitString: "Words per Minute",
histogramDataBucketSize: 10,
historyStepSize: 10,
},
cpm: {
convert: (wpm: number) => wpm * 5,
convertWithUnitSuffix: (wpm: number) => {
return convertTypingSpeedWithUnitSuffix("cpm", wpm);
},
fullUnitString: "Characters per Minute",
histogramDataBucketSize: 50,
historyStepSize: 100,
},
wps: {
convert: (wpm: number) => wpm / 60,
convertWithUnitSuffix: (wpm: number) => {
return convertTypingSpeedWithUnitSuffix("wps", wpm);
},
fullUnitString: "Words per Second",
histogramDataBucketSize: 0.5,
historyStepSize: 2,
},
cps: {
convert: (wpm: number) => (wpm * 5) / 60,
convertWithUnitSuffix: (wpm: number) => {
return convertTypingSpeedWithUnitSuffix("cps", wpm);
},
fullUnitString: "Characters per Second",
histogramDataBucketSize: 5,
historyStepSize: 5,
},
wph: {
convert: (wpm: number) => wpm * 60,
convertWithUnitSuffix: (wpm: number) => {
return convertTypingSpeedWithUnitSuffix("wph", wpm);
},
fullUnitString: "Words per Hour",
histogramDataBucketSize: 60,
historyStepSize: 1000,
},
};
export function get(
unit: MonkeyTypes.TypingSpeedUnit
): MonkeyTypes.TypingSpeedUnitSettings {
return typingSpeedUnits[unit];
}
function convertTypingSpeedWithUnitSuffix(
unit: MonkeyTypes.TypingSpeedUnit,
wpm: number
): string {
return roundTo2(get(unit).convert(wpm)).toFixed(2) + " " + unit;
}

View file

@ -1584,31 +1584,44 @@
</div>
</div>
</div>
<div class="section alwaysShowCPM">
<div class="section typingSpeedUnit">
<div class="groupTitle">
<i class="fas fa-tachometer-alt"></i>
<span>always show cpm</span>
</div>
<div class="text">
Always shows characters per minute calculation instead of the default
words per minute calculation.
<span>typing speed unit</span>
</div>
<div class="text">Display typing speed in the specified unit.</div>
<div class="buttons">
<div
class="button"
alwaysShowCPM="false"
typingSpeedUnit="wpm"
tabindex="0"
onclick="this.blur();"
>
off
wpm
</div>
<div
class="button"
alwaysShowCPM="true"
typingSpeedUnit="cpm"
tabindex="0"
onclick="this.blur();"
>
on
cpm
</div>
<div
class="button"
typingSpeedUnit="wps"
tabindex="0"
onclick="this.blur();"
>
wps
</div>
<div
class="button"
typingSpeedUnit="cps"
tabindex="0"
onclick="this.blur();"
>
cps
</div>
</div>
</div>
@ -2584,11 +2597,11 @@
</div>
<div
class="button"
showAverage="wpm"
showAverage="speed"
tabindex="0"
onclick="this.blur();"
>
wpm
speed
</div>
<div
class="button"