mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-04 05:38:55 +08:00
feat(result): rename raw to burst, add raw line to result graph, add ability to hide chart data (@miodec) (#6907)
This commit is contained in:
parent
7487e53c67
commit
8627235bef
19 changed files with 676 additions and 120 deletions
|
@ -622,7 +622,7 @@ describe("result controller test", () => {
|
|||
bailedOut: false,
|
||||
blindMode: false,
|
||||
charStats: [100, 2, 3, 5],
|
||||
chartData: { wpm: [1, 2, 3], raw: [50, 55, 56], err: [0, 2, 0] },
|
||||
chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] },
|
||||
consistency: 23.5,
|
||||
difficulty: "normal",
|
||||
funbox: [],
|
||||
|
@ -675,7 +675,7 @@ describe("result controller test", () => {
|
|||
charStats: [100, 2, 3, 5],
|
||||
chartData: {
|
||||
err: [0, 2, 0],
|
||||
raw: [50, 55, 56],
|
||||
burst: [50, 55, 56],
|
||||
wpm: [1, 2, 3],
|
||||
},
|
||||
consistency: 23.5,
|
||||
|
@ -757,7 +757,7 @@ describe("result controller test", () => {
|
|||
bailedOut: false,
|
||||
blindMode: false,
|
||||
charStats: [100, 2, 3, 5],
|
||||
chartData: { wpm: [1, 2, 3], raw: [50, 55, 56], err: [0, 2, 0] },
|
||||
chartData: { wpm: [1, 2, 3], burst: [50, 55, 56], err: [0, 2, 0] },
|
||||
consistency: 23.5,
|
||||
difficulty: "normal",
|
||||
funbox: [],
|
||||
|
|
|
@ -123,19 +123,85 @@ describe("Result Utils", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("legacy chartData conversion", () => {
|
||||
it("should convert chartData with 'raw' property to 'burst'", () => {
|
||||
const resultWithLegacyChartData: DBResult = {
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
raw: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithLegacyChartData);
|
||||
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not convert chartData when it's 'toolong'", () => {
|
||||
const resultWithToolongChartData: DBResult = {
|
||||
chartData: "toolong",
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithToolongChartData);
|
||||
|
||||
expect(result.chartData).toBe("toolong");
|
||||
});
|
||||
|
||||
it("should not convert chartData when it doesn't have 'raw' property", () => {
|
||||
const resultWithModernChartData: DBResult = {
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithModernChartData);
|
||||
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
it("should not convert chartData when it's undefined", () => {
|
||||
const resultWithoutChartData: DBResult = {} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithoutChartData);
|
||||
|
||||
expect(result.chartData).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should convert all legacy data at once", () => {
|
||||
const resultWithBothLegacy: DBResult = {
|
||||
const resultWithAllLegacy: DBResult = {
|
||||
correctChars: 100,
|
||||
incorrectChars: 8,
|
||||
funbox: "memory#mirror" as any,
|
||||
chartData: {
|
||||
wpm: [50, 55, 60],
|
||||
raw: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
} as any,
|
||||
} as any;
|
||||
|
||||
const result = replaceLegacyValues(resultWithBothLegacy);
|
||||
const result = replaceLegacyValues(resultWithAllLegacy);
|
||||
|
||||
expect(result.charStats).toEqual([100, 8, 0, 0]);
|
||||
expect(result.correctChars).toBeUndefined();
|
||||
expect(result.incorrectChars).toBeUndefined();
|
||||
expect(result.funbox).toEqual(["memory", "mirror"]);
|
||||
expect(result.chartData).toEqual({
|
||||
wpm: [50, 55, 60],
|
||||
burst: [52, 57, 62],
|
||||
err: [1, 0, 2],
|
||||
});
|
||||
});
|
||||
|
||||
describe("no legacy values", () => {
|
||||
|
|
|
@ -137,7 +137,7 @@ function createResult(
|
|||
keyConsistency: 33.18,
|
||||
chartData: {
|
||||
wpm: createArray(testDuration, () => random(80, 120)),
|
||||
raw: createArray(testDuration, () => random(80, 120)),
|
||||
burst: createArray(testDuration, () => random(80, 120)),
|
||||
err: createArray(testDuration, () => (Math.random() < 0.1 ? 1 : 0)),
|
||||
},
|
||||
keySpacingStats: {
|
||||
|
|
|
@ -63,6 +63,7 @@ export async function getResult(uid: string, id: string): Promise<DBResult> {
|
|||
_id: new ObjectId(id),
|
||||
uid,
|
||||
});
|
||||
|
||||
if (!result) throw new MonkeyError(404, "Result not found");
|
||||
return replaceLegacyValues(result);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import { CompletedEvent, Result } from "@monkeytype/schemas/results";
|
||||
import {
|
||||
ChartData,
|
||||
CompletedEvent,
|
||||
OldChartData,
|
||||
Result,
|
||||
} from "@monkeytype/schemas/results";
|
||||
import { Mode } from "@monkeytype/schemas/shared";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { WithObjectId } from "./misc";
|
||||
|
@ -8,6 +13,7 @@ export type DBResult = WithObjectId<Result<Mode>> & {
|
|||
//legacy values
|
||||
correctChars?: number;
|
||||
incorrectChars?: number;
|
||||
chartData: ChartData | OldChartData | "toolong";
|
||||
};
|
||||
|
||||
export function buildDbResult(
|
||||
|
@ -103,5 +109,18 @@ export function replaceLegacyValues(result: DBResult): DBResult {
|
|||
}
|
||||
}
|
||||
|
||||
if (
|
||||
result.chartData !== undefined &&
|
||||
result.chartData !== "toolong" &&
|
||||
"raw" in result.chartData
|
||||
) {
|
||||
const temp = result.chartData;
|
||||
result.chartData = {
|
||||
wpm: temp.wpm,
|
||||
burst: temp.raw,
|
||||
err: temp.err,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -331,6 +331,25 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="chart">
|
||||
<div class="chartLegend">
|
||||
<button class="text" tabindex="-1" data-id="pbLine">
|
||||
<i class="fas fa-crown"></i>
|
||||
<div class="text">pb</div>
|
||||
</button>
|
||||
<button class="text" tabindex="-1" data-id="raw">
|
||||
<div class="line dashed"></div>
|
||||
<div class="text">raw</div>
|
||||
</button>
|
||||
<button class="text" tabindex="-1" data-id="burst">
|
||||
<div class="line"></div>
|
||||
<div class="text">burst</div>
|
||||
</button>
|
||||
<button class="text" tabindex="-1" data-id="errors">
|
||||
<!-- <div class="line"></div> -->
|
||||
<i class="fas fa-times"></i>
|
||||
<div class="text">errors</div>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <div class="title">wpm over time</div> -->
|
||||
<canvas id="wpmChart"></canvas>
|
||||
</div>
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
<button class="showTestNotifications">show test notifications</button>
|
||||
<button class="showRealWordsInput">show real words input</button>
|
||||
<button class="xpBarTest">xp bar test</button>
|
||||
<button class="toggleFakeChartData">toggle fake chart data</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
|
|
@ -779,6 +779,109 @@
|
|||
.chart {
|
||||
grid-area: chart;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.chartLegend {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.chartLegend {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
// top: -2.25em;
|
||||
bottom: -0.75em;
|
||||
padding: 0.25em;
|
||||
right: 0;
|
||||
font-size: 0.75em;
|
||||
transition: opacity 0.125s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
// button {
|
||||
// border-radius: 0;
|
||||
// background: var(--sub-alt-color);
|
||||
// }
|
||||
|
||||
// button:first-child {
|
||||
// border-top-left-radius: var(--roundness);
|
||||
// border-bottom-left-radius: var(--roundness);
|
||||
// }
|
||||
|
||||
// button:last-child {
|
||||
// border-top-right-radius: var(--roundness);
|
||||
// border-bottom-right-radius: var(--roundness);
|
||||
// }
|
||||
|
||||
button {
|
||||
padding: 0.5em 1em;
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
align-items: center;
|
||||
--color: var(--sub-color);
|
||||
|
||||
text-decoration: line-through;
|
||||
.text {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line {
|
||||
height: 0.25em;
|
||||
width: 1.5em;
|
||||
border-radius: calc(var(--roundness) / 2);
|
||||
transition: background 0.125s;
|
||||
background: var(--color);
|
||||
pointer-events: none;
|
||||
|
||||
&.dashed {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color) 0%,
|
||||
var(--color) 40%,
|
||||
transparent 40%,
|
||||
transparent 60%,
|
||||
var(--color) 60%,
|
||||
var(--color) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.fas {
|
||||
color: var(--color);
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
text-decoration: none;
|
||||
// .text {
|
||||
// }
|
||||
color: var(--sub-color);
|
||||
&[data-id="raw"] {
|
||||
--color: var(--main-color);
|
||||
}
|
||||
&[data-id="burst"] {
|
||||
--color: var(--sub-color);
|
||||
}
|
||||
&[data-id="errors"] {
|
||||
--color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
background: var(--sub-alt-color);
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--sub-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { get as getTypingSpeedUnit } from "../../utils/typing-speed-units";
|
|||
import { Command, CommandsSubgroup } from "../types";
|
||||
|
||||
const subgroup: CommandsSubgroup = {
|
||||
title: "Minimum burst...",
|
||||
title: "Minimum word burst...",
|
||||
configKey: "minBurst",
|
||||
list: [
|
||||
{
|
||||
|
@ -48,7 +48,7 @@ const subgroup: CommandsSubgroup = {
|
|||
const commands: Command[] = [
|
||||
{
|
||||
id: "changeMinBurst",
|
||||
display: "Minimum burst...",
|
||||
display: "Minimum word burst...",
|
||||
alias: "minimum",
|
||||
icon: "fa-bomb",
|
||||
subgroup,
|
||||
|
|
|
@ -174,7 +174,7 @@ export const configMetadata: ConfigMetadataObject = {
|
|||
},
|
||||
burstHeatmap: {
|
||||
icon: "fa-fire",
|
||||
displayString: "burst heatmap",
|
||||
displayString: "word burst heatmap",
|
||||
changeRequiresRestart: false,
|
||||
},
|
||||
|
||||
|
@ -246,12 +246,12 @@ export const configMetadata: ConfigMetadataObject = {
|
|||
},
|
||||
minBurst: {
|
||||
icon: "fa-bomb",
|
||||
displayString: "min burst",
|
||||
displayString: "min word burst",
|
||||
changeRequiresRestart: true,
|
||||
},
|
||||
minBurstCustomSpeed: {
|
||||
icon: "fa-bomb",
|
||||
displayString: "min burst custom speed",
|
||||
displayString: "min word burst custom speed",
|
||||
changeRequiresRestart: true,
|
||||
},
|
||||
britishEnglish: {
|
||||
|
@ -477,7 +477,7 @@ export const configMetadata: ConfigMetadataObject = {
|
|||
},
|
||||
liveBurstStyle: {
|
||||
icon: "fa-tachometer-alt",
|
||||
displayString: "live burst style",
|
||||
displayString: "live word burst style",
|
||||
changeRequiresRestart: false,
|
||||
},
|
||||
timerColor: {
|
||||
|
|
|
@ -54,7 +54,7 @@ Chart.register(
|
|||
(
|
||||
Chart.defaults.animation as AnimationSpec<"line" | "bar" | "scatter">
|
||||
).duration = 0;
|
||||
Chart.defaults.elements.line.tension = 0.3;
|
||||
Chart.defaults.elements.line.tension = 0.5;
|
||||
Chart.defaults.elements.line.fill = "origin";
|
||||
|
||||
import "chartjs-adapter-date-fns";
|
||||
|
@ -84,6 +84,7 @@ class ChartWithUpdateColors<
|
|||
}
|
||||
|
||||
async updateColors(): Promise<void> {
|
||||
//@ts-expect-error its too difficult to figure out these types, but this works
|
||||
await updateColors(this);
|
||||
}
|
||||
|
||||
|
@ -92,6 +93,11 @@ class ChartWithUpdateColors<
|
|||
return this.data.datasets?.find((x) => x.yAxisID === id);
|
||||
}
|
||||
|
||||
getScaleIds(): DatasetIds[] {
|
||||
//@ts-expect-error its too difficult to figure out these types, but this works
|
||||
return typedKeys(this.options?.scales ?? {}) as DatasetIds[];
|
||||
}
|
||||
|
||||
getScale(
|
||||
id: DatasetIds extends never ? never : "x" | DatasetIds
|
||||
): DatasetIds extends never ? never : CartesianScaleOptions {
|
||||
|
@ -106,7 +112,7 @@ export const result = new ChartWithUpdateColors<
|
|||
"line" | "scatter",
|
||||
number[],
|
||||
string,
|
||||
"wpm" | "raw" | "error"
|
||||
"wpm" | "raw" | "error" | "burst"
|
||||
>(document.querySelector("#wpmChart") as HTMLCanvasElement, {
|
||||
type: "line",
|
||||
data: {
|
||||
|
@ -129,10 +135,11 @@ export const result = new ChartWithUpdateColors<
|
|||
label: "raw",
|
||||
data: [],
|
||||
borderColor: "rgba(125, 125, 125, 1)",
|
||||
borderWidth: 3,
|
||||
borderWidth: 2,
|
||||
yAxisID: "raw",
|
||||
borderDash: [8, 8],
|
||||
order: 3,
|
||||
pointRadius: 1,
|
||||
pointRadius: 0,
|
||||
},
|
||||
{
|
||||
//@ts-expect-error the type is defined incorrectly, have to ingore the error
|
||||
|
@ -157,6 +164,17 @@ export const result = new ChartWithUpdateColors<
|
|||
return (value ?? 0) <= 0 ? 0 : 5;
|
||||
},
|
||||
},
|
||||
{
|
||||
//@ts-expect-error the type is defined incorrectly, have to ingore the error
|
||||
clip: false,
|
||||
label: "burst",
|
||||
data: [],
|
||||
borderColor: "rgba(125, 125, 125, 1)",
|
||||
borderWidth: 3,
|
||||
yAxisID: "burst",
|
||||
order: 4,
|
||||
pointRadius: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
|
@ -209,6 +227,23 @@ export const result = new ChartWithUpdateColors<
|
|||
display: false,
|
||||
},
|
||||
},
|
||||
burst: {
|
||||
axis: "y",
|
||||
display: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Burst Words per Minute",
|
||||
},
|
||||
beginAtZero: true,
|
||||
min: 0,
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
autoSkipPadding: 20,
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
error: {
|
||||
axis: "y",
|
||||
display: true,
|
||||
|
@ -923,7 +958,7 @@ export const miniResult = new ChartWithUpdateColors<
|
|||
"line" | "scatter",
|
||||
number[],
|
||||
string,
|
||||
"wpm" | "raw" | "error"
|
||||
"wpm" | "burst" | "error"
|
||||
>(document.querySelector("#miniResultChartModal canvas") as HTMLCanvasElement, {
|
||||
type: "line",
|
||||
data: {
|
||||
|
@ -933,19 +968,19 @@ export const miniResult = new ChartWithUpdateColors<
|
|||
label: "wpm",
|
||||
data: [],
|
||||
borderColor: "rgba(125, 125, 125, 1)",
|
||||
borderWidth: 2,
|
||||
borderWidth: 3,
|
||||
yAxisID: "wpm",
|
||||
order: 2,
|
||||
pointRadius: 2,
|
||||
pointRadius: 1,
|
||||
},
|
||||
{
|
||||
label: "raw",
|
||||
label: "burst",
|
||||
data: [],
|
||||
borderColor: "rgba(125, 125, 125, 1)",
|
||||
borderWidth: 2,
|
||||
yAxisID: "raw",
|
||||
borderWidth: 3,
|
||||
yAxisID: "burst",
|
||||
order: 3,
|
||||
pointRadius: 2,
|
||||
pointRadius: 1,
|
||||
},
|
||||
{
|
||||
label: "errors",
|
||||
|
@ -1003,12 +1038,12 @@ export const miniResult = new ChartWithUpdateColors<
|
|||
display: true,
|
||||
},
|
||||
},
|
||||
raw: {
|
||||
burst: {
|
||||
axis: "y",
|
||||
display: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: "Raw Words per Minute",
|
||||
text: "Burst Words per Minute",
|
||||
},
|
||||
beginAtZero: true,
|
||||
min: 0,
|
||||
|
@ -1171,6 +1206,70 @@ async function updateColors<
|
|||
scale.title.color = subcolor;
|
||||
}
|
||||
|
||||
if (chart.id === result.id) {
|
||||
const c = chart as unknown as typeof result;
|
||||
|
||||
const wpm = c.getDataset("wpm");
|
||||
wpm.backgroundColor = "transparent";
|
||||
wpm.borderColor = maincolor;
|
||||
wpm.pointBackgroundColor = maincolor;
|
||||
wpm.pointBorderColor = maincolor;
|
||||
|
||||
const raw = c.getDataset("raw");
|
||||
raw.backgroundColor = "transparent";
|
||||
raw.borderColor = maincolor + "99";
|
||||
raw.pointBackgroundColor = maincolor + "99";
|
||||
raw.pointBorderColor = maincolor + "99";
|
||||
|
||||
const error = c.getDataset("error");
|
||||
error.backgroundColor = errorcolor;
|
||||
error.borderColor = errorcolor;
|
||||
error.pointBackgroundColor = errorcolor;
|
||||
error.pointBorderColor = errorcolor;
|
||||
|
||||
const burst = c.getDataset("burst");
|
||||
burst.backgroundColor = blendTwoHexColors(
|
||||
subaltcolor,
|
||||
subaltcolor + "00",
|
||||
0.5
|
||||
);
|
||||
burst.borderColor = subcolor;
|
||||
burst.pointBackgroundColor = subcolor;
|
||||
burst.pointBorderColor = subcolor;
|
||||
|
||||
chart.update("resize");
|
||||
return;
|
||||
}
|
||||
|
||||
if (chart.id === miniResult.id) {
|
||||
const c = chart as unknown as typeof miniResult;
|
||||
|
||||
const wpm = c.getDataset("wpm");
|
||||
wpm.backgroundColor = "transparent";
|
||||
wpm.borderColor = maincolor;
|
||||
wpm.pointBackgroundColor = maincolor;
|
||||
wpm.pointBorderColor = maincolor;
|
||||
|
||||
const error = c.getDataset("error");
|
||||
error.backgroundColor = errorcolor;
|
||||
error.borderColor = errorcolor;
|
||||
error.pointBackgroundColor = errorcolor;
|
||||
error.pointBorderColor = errorcolor;
|
||||
|
||||
const burst = c.getDataset("burst");
|
||||
burst.backgroundColor = blendTwoHexColors(
|
||||
subaltcolor,
|
||||
subaltcolor + "00",
|
||||
0.75
|
||||
);
|
||||
burst.borderColor = subcolor;
|
||||
burst.pointBackgroundColor = subcolor;
|
||||
burst.pointBorderColor = subcolor;
|
||||
|
||||
chart.update("resize");
|
||||
return;
|
||||
}
|
||||
|
||||
//@ts-expect-error its too difficult to figure out these types, but this works
|
||||
chart.data.datasets[0].borderColor = (ctx): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
|
@ -1326,10 +1425,10 @@ function setDefaultFontFamily(font: string): void {
|
|||
|
||||
export function updateAllChartColors(): void {
|
||||
ThemeColors.update();
|
||||
void result.updateColors();
|
||||
void accountHistory.updateColors();
|
||||
void accountHistogram.updateColors();
|
||||
void globalSpeedHistogram.updateColors();
|
||||
void result.updateColors();
|
||||
void accountActivity.updateColors();
|
||||
void miniResult.updateColors();
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ addToGlobal({
|
|||
replay: Replay.getReplayExport,
|
||||
enableTimerDebug: TestTimer.enableTimerDebug,
|
||||
getTimerStats: TestTimer.getTimerStats,
|
||||
toggleUnsmoothedRaw: Result.toggleUnsmoothedRaw,
|
||||
toggleSmoothedBurst: Result.toggleSmoothedBurst,
|
||||
egVideoListener: egVideoListener,
|
||||
toggleDebugLogs: Logger.toggleDebugLogs,
|
||||
toggleSentryDebug: Sentry.toggleDebug,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { setMediaQueryDebugLevel } from "../ui";
|
|||
import { signIn } from "../auth";
|
||||
import * as Loader from "../elements/loader";
|
||||
import { update } from "../elements/xp-bar";
|
||||
import { toggleUserFakeChartData } from "../test/result";
|
||||
|
||||
let mediaQueryDebugLevel = 0;
|
||||
|
||||
|
@ -83,6 +84,11 @@ async function setup(modalEl: HTMLElement): Promise<void> {
|
|||
}, 500);
|
||||
void modal.hide();
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".toggleFakeChartData")
|
||||
?.addEventListener("click", () => {
|
||||
toggleUserFakeChartData();
|
||||
});
|
||||
}
|
||||
|
||||
const modal = new AnimatedModal({
|
||||
|
|
|
@ -2,7 +2,6 @@ import { ChartData } from "@monkeytype/schemas/results";
|
|||
import AnimatedModal from "../utils/animated-modal";
|
||||
import * as ChartController from "../controllers/chart-controller";
|
||||
import Config from "../config";
|
||||
import * as Arrays from "../utils/arrays";
|
||||
|
||||
function updateData(data: ChartData): void {
|
||||
// let data = filteredResults[filteredId].chartData;
|
||||
|
@ -11,35 +10,33 @@ function updateData(data: ChartData): void {
|
|||
labels.push(i.toString());
|
||||
}
|
||||
|
||||
//make sure data.wpm and data.err are the same length as data.raw using slice
|
||||
data.wpm = data.wpm.slice(0, data.raw.length);
|
||||
data.err = data.err.slice(0, data.raw.length);
|
||||
labels = labels.slice(0, data.raw.length);
|
||||
|
||||
const smoothedRawData = Arrays.smooth(data.raw, 1);
|
||||
//make sure data.wpm and data.err are the same length as data.burst using slice
|
||||
data.wpm = data.wpm.slice(0, data.burst.length);
|
||||
data.err = data.err.slice(0, data.burst.length);
|
||||
labels = labels.slice(0, data.burst.length);
|
||||
|
||||
ChartController.miniResult.data.labels = labels;
|
||||
|
||||
ChartController.miniResult.getDataset("wpm").data = data.wpm;
|
||||
ChartController.miniResult.getDataset("raw").data = smoothedRawData;
|
||||
ChartController.miniResult.getDataset("burst").data = data.burst;
|
||||
ChartController.miniResult.getDataset("error").data = data.err;
|
||||
|
||||
const maxChartVal = Math.max(
|
||||
...[Math.max(...data.wpm), Math.max(...data.raw)]
|
||||
...[Math.max(...data.wpm), Math.max(...data.burst)]
|
||||
);
|
||||
const minChartVal = Math.min(
|
||||
...[Math.min(...data.wpm), Math.min(...data.raw)]
|
||||
...[Math.min(...data.wpm), Math.min(...data.burst)]
|
||||
);
|
||||
|
||||
ChartController.miniResult.getScale("wpm").max = Math.round(maxChartVal);
|
||||
ChartController.miniResult.getScale("raw").max = Math.round(maxChartVal);
|
||||
ChartController.miniResult.getScale("burst").max = Math.round(maxChartVal);
|
||||
|
||||
if (!Config.startGraphsAtZero) {
|
||||
ChartController.miniResult.getScale("wpm").min = Math.round(minChartVal);
|
||||
ChartController.miniResult.getScale("raw").min = Math.round(minChartVal);
|
||||
ChartController.miniResult.getScale("burst").min = Math.round(minChartVal);
|
||||
} else {
|
||||
ChartController.miniResult.getScale("wpm").min = 0;
|
||||
ChartController.miniResult.getScale("raw").min = 0;
|
||||
ChartController.miniResult.getScale("burst").min = 0;
|
||||
}
|
||||
|
||||
void ChartController.miniResult.updateColors();
|
||||
|
|
|
@ -42,25 +42,48 @@ import { getFunbox } from "@monkeytype/funbox";
|
|||
import { SnapshotUserTag } from "../constants/default-snapshot";
|
||||
import { Language } from "@monkeytype/schemas/languages";
|
||||
import { canQuickRestart as canQuickRestartFn } from "../utils/quick-restart";
|
||||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { z } from "zod";
|
||||
|
||||
let result: CompletedEvent;
|
||||
let maxChartVal: number;
|
||||
|
||||
let useUnsmoothedRaw = false;
|
||||
let useSmoothedBurst = true;
|
||||
let useFakeChartData = false;
|
||||
|
||||
let quoteLang: Language | undefined;
|
||||
let quoteId = "";
|
||||
|
||||
export function toggleUnsmoothedRaw(): void {
|
||||
useUnsmoothedRaw = !useUnsmoothedRaw;
|
||||
Notifications.add(useUnsmoothedRaw ? "on" : "off", 1);
|
||||
export function toggleSmoothedBurst(): void {
|
||||
useSmoothedBurst = !useSmoothedBurst;
|
||||
Notifications.add(useSmoothedBurst ? "on" : "off", 1);
|
||||
if (TestUI.resultVisible) {
|
||||
void updateGraph().then(() => {
|
||||
ChartController.result.update("resize");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleUserFakeChartData(): void {
|
||||
useFakeChartData = !useFakeChartData;
|
||||
Notifications.add(useFakeChartData ? "on" : "off", 1);
|
||||
if (TestUI.resultVisible) {
|
||||
void updateGraph().then(() => {
|
||||
ChartController.result.update("resize");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let resultAnnotation: AnnotationOptions<"line">[] = [];
|
||||
|
||||
async function updateGraph(): Promise<void> {
|
||||
if (result.chartData === "toolong") return;
|
||||
|
||||
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
|
||||
const labels = [];
|
||||
ChartController.result.getScale("wpm").title.text =
|
||||
typingSpeedUnit.fullUnitString;
|
||||
|
||||
let labels = [];
|
||||
|
||||
for (let i = 1; i <= TestInput.wpmHistory.length; i++) {
|
||||
if (TestStats.lastSecondNotRound && i === TestInput.wpmHistory.length) {
|
||||
|
@ -70,22 +93,29 @@ async function updateGraph(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
ChartController.result.getScale("wpm").title.text =
|
||||
typingSpeedUnit.fullUnitString;
|
||||
|
||||
const chartData1 = [
|
||||
...TestInput.wpmHistory.map((a) =>
|
||||
...result.chartData["wpm"].map((a) =>
|
||||
Numbers.roundTo2(typingSpeedUnit.fromWpm(a))
|
||||
),
|
||||
];
|
||||
if (result.chartData === "toolong") return;
|
||||
|
||||
const chartData2 = [
|
||||
...result.chartData.raw.map((a) =>
|
||||
...TestInput.rawHistory.map((a) =>
|
||||
Numbers.roundTo2(typingSpeedUnit.fromWpm(a))
|
||||
),
|
||||
];
|
||||
|
||||
const valueWindow = Math.max(...result.chartData["burst"]) * 0.25;
|
||||
let smoothedBurst = Arrays.smoothWithValueWindow(
|
||||
result.chartData["burst"],
|
||||
1,
|
||||
useSmoothedBurst ? valueWindow : 0
|
||||
);
|
||||
|
||||
const chartData3 = [
|
||||
...smoothedBurst.map((a) => Numbers.roundTo2(typingSpeedUnit.fromWpm(a))),
|
||||
];
|
||||
|
||||
if (
|
||||
Config.mode !== "time" &&
|
||||
TestStats.lastSecondNotRound &&
|
||||
|
@ -96,36 +126,31 @@ async function updateGraph(): Promise<void> {
|
|||
chartData2.pop();
|
||||
}
|
||||
|
||||
let smoothedRawData = chartData2;
|
||||
if (!useUnsmoothedRaw) {
|
||||
smoothedRawData = Arrays.smooth(smoothedRawData, 1);
|
||||
smoothedRawData = smoothedRawData.map((a) => Math.round(a));
|
||||
}
|
||||
|
||||
ChartController.result.data.labels = labels;
|
||||
ChartController.result.getDataset("wpm").data = chartData1;
|
||||
ChartController.result.getDataset("wpm").label = Config.typingSpeedUnit;
|
||||
ChartController.result.getDataset("raw").data = smoothedRawData;
|
||||
|
||||
maxChartVal = Math.max(
|
||||
...[Math.max(...smoothedRawData), Math.max(...chartData1)]
|
||||
...[
|
||||
Math.max(...chartData1),
|
||||
Math.max(...chartData2),
|
||||
Math.max(...chartData3),
|
||||
]
|
||||
);
|
||||
|
||||
let minChartVal = 0;
|
||||
|
||||
if (!Config.startGraphsAtZero) {
|
||||
const minChartVal = Math.min(
|
||||
...[Math.min(...smoothedRawData), Math.min(...chartData1)]
|
||||
minChartVal = Math.min(
|
||||
...[
|
||||
Math.min(...chartData1),
|
||||
Math.min(...chartData2),
|
||||
Math.min(...chartData3),
|
||||
]
|
||||
);
|
||||
|
||||
ChartController.result.getScale("wpm").min = minChartVal;
|
||||
ChartController.result.getScale("raw").min = minChartVal;
|
||||
} else {
|
||||
ChartController.result.getScale("wpm").min = 0;
|
||||
ChartController.result.getScale("raw").min = 0;
|
||||
// Round down to nearest multiple of 10
|
||||
minChartVal = Math.floor(minChartVal / 10) * 10;
|
||||
}
|
||||
|
||||
ChartController.result.getDataset("error").data = result.chartData.err;
|
||||
const subcolor = await ThemeColors.get("sub");
|
||||
|
||||
const fc = await ThemeColors.get("sub");
|
||||
if (Config.funbox.length > 0) {
|
||||
let content = "";
|
||||
for (const fb of getActiveFunboxes()) {
|
||||
|
@ -154,7 +179,7 @@ async function updateGraph(): Promise<void> {
|
|||
weight: Chart.defaults.font.weight as string,
|
||||
lineHeight: Chart.defaults.font.lineHeight as number,
|
||||
},
|
||||
color: fc,
|
||||
color: subcolor,
|
||||
padding: 3,
|
||||
borderRadius: 3,
|
||||
position: "start",
|
||||
|
@ -164,11 +189,113 @@ async function updateGraph(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
ChartController.result.data.labels = labels;
|
||||
|
||||
ChartController.result.getDataset("wpm").data = chartData1;
|
||||
ChartController.result.getDataset("wpm").label = Config.typingSpeedUnit;
|
||||
ChartController.result.getScale("wpm").min = minChartVal;
|
||||
ChartController.result.getScale("wpm").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("raw").data = chartData2;
|
||||
ChartController.result.getScale("raw").min = minChartVal;
|
||||
ChartController.result.getScale("raw").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("burst").data = chartData3;
|
||||
ChartController.result.getScale("burst").min = minChartVal;
|
||||
ChartController.result.getScale("burst").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("error").data = result.chartData.err;
|
||||
ChartController.result.getScale("error").max = Math.max(
|
||||
...result.chartData.err
|
||||
);
|
||||
|
||||
if (useFakeChartData) {
|
||||
applyFakeChartData();
|
||||
}
|
||||
}
|
||||
|
||||
function applyFakeChartData(): void {
|
||||
const fakeChartData = {
|
||||
wpm: [
|
||||
108, 120, 116, 114, 113, 120, 118, 121, 119, 120, 116, 118, 113, 110, 108,
|
||||
110, 107, 107, 108, 109, 110, 112, 114, 112, 111, 109, 110, 108, 108, 109,
|
||||
],
|
||||
raw: [
|
||||
108, 120, 116, 114, 113, 120, 123, 127, 131, 131, 131, 132, 130, 133, 134,
|
||||
134, 131, 129, 129, 128, 129, 130, 131, 129, 129, 127, 127, 128, 127, 127,
|
||||
],
|
||||
burst: [
|
||||
108, 132, 108, 108, 108, 156, 144, 156, 156, 132, 132, 144, 108, 168, 156,
|
||||
132, 96, 108, 120, 120, 144, 156, 144, 84, 132, 84, 132, 156, 108, 120,
|
||||
],
|
||||
err: [
|
||||
0, 0, 0, 0, 0, 0, 3, 1, 3, 0, 5, 0, 3, 5, 4, 0, 2, 0, 0, 0, 0, 0, 0, 1, 2,
|
||||
1, 0, 4, 0, 0,
|
||||
],
|
||||
};
|
||||
|
||||
const labels = fakeChartData.wpm.map((_, i) => (i + 1).toString());
|
||||
|
||||
const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit);
|
||||
|
||||
const chartData1 = [
|
||||
...fakeChartData["wpm"].map((a) =>
|
||||
Numbers.roundTo2(typingSpeedUnit.fromWpm(a))
|
||||
),
|
||||
];
|
||||
|
||||
const chartData2 = [
|
||||
...fakeChartData["raw"].map((a) =>
|
||||
Numbers.roundTo2(typingSpeedUnit.fromWpm(a))
|
||||
),
|
||||
];
|
||||
|
||||
const chartData3 = [
|
||||
...fakeChartData["burst"].map((a) =>
|
||||
Numbers.roundTo2(typingSpeedUnit.fromWpm(a))
|
||||
),
|
||||
];
|
||||
|
||||
maxChartVal = Math.max(
|
||||
...[
|
||||
Math.max(...chartData1),
|
||||
Math.max(...chartData2),
|
||||
Math.max(...chartData3),
|
||||
]
|
||||
);
|
||||
|
||||
let minChartVal = 0;
|
||||
|
||||
if (!Config.startGraphsAtZero) {
|
||||
minChartVal = Math.min(
|
||||
...[
|
||||
Math.min(...chartData1),
|
||||
Math.min(...chartData2),
|
||||
Math.min(...chartData3),
|
||||
]
|
||||
);
|
||||
|
||||
// Round down to nearest multiple of 10
|
||||
minChartVal = Math.floor(minChartVal / 10) * 10;
|
||||
}
|
||||
|
||||
ChartController.result.data.labels = labels;
|
||||
|
||||
ChartController.result.getDataset("wpm").data = chartData1;
|
||||
ChartController.result.getDataset("wpm").label = Config.typingSpeedUnit;
|
||||
ChartController.result.getScale("wpm").min = minChartVal;
|
||||
ChartController.result.getScale("wpm").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("raw").data = chartData2;
|
||||
ChartController.result.getScale("raw").min = minChartVal;
|
||||
ChartController.result.getScale("raw").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("burst").data = chartData3;
|
||||
ChartController.result.getScale("burst").min = minChartVal;
|
||||
ChartController.result.getScale("burst").max = maxChartVal;
|
||||
|
||||
ChartController.result.getDataset("error").data = fakeChartData.err;
|
||||
ChartController.result.getScale("error").max = Math.max(...fakeChartData.err);
|
||||
}
|
||||
|
||||
export async function updateGraphPBLine(): Promise<void> {
|
||||
|
@ -195,9 +322,9 @@ export async function updateGraphPBLine(): Promise<void> {
|
|||
id: "lpb",
|
||||
scaleID: "wpm",
|
||||
value: chartlpb,
|
||||
borderColor: themecolors.sub,
|
||||
borderColor: themecolors.sub + "55",
|
||||
borderWidth: 1,
|
||||
borderDash: [2, 2],
|
||||
// borderDash: [4, 16],
|
||||
label: {
|
||||
backgroundColor: themecolors.sub,
|
||||
font: {
|
||||
|
@ -211,7 +338,7 @@ export async function updateGraphPBLine(): Promise<void> {
|
|||
padding: 3,
|
||||
borderRadius: 3,
|
||||
position: "center",
|
||||
content: `PB: ${chartlpb}`,
|
||||
content: ` PB: ${chartlpb} `,
|
||||
display: true,
|
||||
},
|
||||
});
|
||||
|
@ -632,9 +759,9 @@ async function updateTags(dontSave: boolean): Promise<void> {
|
|||
id: "tpb",
|
||||
scaleID: "wpm",
|
||||
value: typingSpeedUnit.fromWpm(tpb),
|
||||
borderColor: themecolors.sub,
|
||||
borderColor: themecolors.sub + "55",
|
||||
borderWidth: 1,
|
||||
borderDash: [2, 2],
|
||||
// borderDash: [4, 16],
|
||||
label: {
|
||||
backgroundColor: themecolors.sub,
|
||||
font: {
|
||||
|
@ -883,6 +1010,7 @@ export async function update(
|
|||
await updateCrown(dontSave);
|
||||
await updateGraph();
|
||||
await updateGraphPBLine();
|
||||
updateResultChartDataVisibility();
|
||||
await updateTags(dontSave);
|
||||
updateOther(difficultyFailed, failReason, afkDetected, isRepeated, tooShort);
|
||||
|
||||
|
@ -982,6 +1110,54 @@ export async function update(
|
|||
);
|
||||
}
|
||||
|
||||
const resultChartDataVisibility = new LocalStorageWithSchema({
|
||||
key: "resultChartDataVisibility",
|
||||
schema: z
|
||||
.object({
|
||||
raw: z.boolean(),
|
||||
burst: z.boolean(),
|
||||
errors: z.boolean(),
|
||||
pbLine: z.boolean(),
|
||||
})
|
||||
.strict(),
|
||||
fallback: {
|
||||
raw: true,
|
||||
burst: true,
|
||||
errors: true,
|
||||
pbLine: true,
|
||||
},
|
||||
});
|
||||
|
||||
function updateResultChartDataVisibility(update = false): void {
|
||||
const vis = resultChartDataVisibility.get();
|
||||
ChartController.result.getDataset("raw").hidden = !vis.raw;
|
||||
ChartController.result.getDataset("burst").hidden = !vis.burst;
|
||||
ChartController.result.getDataset("error").hidden = !vis.errors;
|
||||
|
||||
for (const annotation of resultAnnotation) {
|
||||
if (annotation.id !== "lpb") continue;
|
||||
annotation.display = vis.pbLine;
|
||||
}
|
||||
|
||||
if (update) ChartController.result.update();
|
||||
|
||||
const buttons = $(".pageTest #result .chart .chartLegend button");
|
||||
|
||||
for (const button of buttons) {
|
||||
const id = $(button).data("id") as string;
|
||||
|
||||
if (id !== "raw" && id !== "burst" && id !== "errors" && id !== "pbLine") {
|
||||
return;
|
||||
}
|
||||
|
||||
$(button).toggleClass("active", vis[id]);
|
||||
|
||||
if (id === "pbLine") {
|
||||
$(button).toggleClass("hidden", !isAuthenticated());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTagsAfterEdit(
|
||||
tagIds: string[],
|
||||
tagPbIds: string[]
|
||||
|
@ -1036,6 +1212,21 @@ export function updateTagsAfterEdit(
|
|||
}
|
||||
}
|
||||
|
||||
$(".pageTest #result .chart .chartLegend button").on("click", (event) => {
|
||||
const $target = $(event.target);
|
||||
const id = $target.data("id") as string;
|
||||
|
||||
if (id !== "raw" && id !== "burst" && id !== "errors" && id !== "pbLine") {
|
||||
return;
|
||||
}
|
||||
|
||||
const vis = resultChartDataVisibility.get();
|
||||
vis[id] = !vis[id];
|
||||
resultChartDataVisibility.set(vis);
|
||||
|
||||
updateResultChartDataVisibility(true);
|
||||
});
|
||||
|
||||
$(".pageTest #favoriteQuoteButton").on("click", async () => {
|
||||
if (quoteLang === undefined || quoteId === "") {
|
||||
Notifications.add("Could not get quote stats!", -1);
|
||||
|
|
|
@ -722,7 +722,8 @@ export async function retrySavingResult(): Promise<void> {
|
|||
}
|
||||
|
||||
function buildCompletedEvent(
|
||||
difficultyFailed: boolean
|
||||
stats: TestStats.Stats,
|
||||
rawPerSecond: number[]
|
||||
): Omit<CompletedEvent, "hash" | "uid"> {
|
||||
//build completed event object
|
||||
let stfk = Numbers.roundTo2(
|
||||
|
@ -738,44 +739,8 @@ function buildCompletedEvent(
|
|||
if (lkte < 0 || Config.mode === "zen") {
|
||||
lkte = 0;
|
||||
}
|
||||
// stats
|
||||
const stats = TestStats.calculateStats();
|
||||
if (stats.time % 1 !== 0 && Config.mode !== "time") {
|
||||
TestStats.setLastSecondNotRound();
|
||||
}
|
||||
|
||||
PaceCaret.setLastTestWpm(stats.wpm); //todo why is this in here?
|
||||
|
||||
// if the last second was not rounded, add another data point to the history
|
||||
if (TestStats.lastSecondNotRound && !difficultyFailed) {
|
||||
const wpmAndRaw = TestStats.calculateWpmAndRaw();
|
||||
TestInput.pushToWpmHistory(wpmAndRaw.wpm);
|
||||
TestInput.pushToRawHistory(wpmAndRaw.raw);
|
||||
TestInput.pushKeypressesToHistory();
|
||||
TestInput.pushErrorToHistory();
|
||||
TestInput.pushAfkToHistory();
|
||||
}
|
||||
|
||||
//consistency
|
||||
const rawPerSecond = TestInput.keypressCountHistory.map((count) =>
|
||||
Math.round((count / 5) * 60)
|
||||
);
|
||||
|
||||
//adjust last second if last second is not round
|
||||
// if (TestStats.lastSecondNotRound && stats.time % 1 >= 0.1) {
|
||||
if (
|
||||
Config.mode !== "time" &&
|
||||
TestStats.lastSecondNotRound &&
|
||||
stats.time % 1 >= 0.5
|
||||
) {
|
||||
const timescale = 1 / (stats.time % 1);
|
||||
|
||||
//multiply last element of rawBefore by scale, and round it
|
||||
rawPerSecond[rawPerSecond.length - 1] = Math.round(
|
||||
(rawPerSecond[rawPerSecond.length - 1] as number) * timescale
|
||||
);
|
||||
}
|
||||
|
||||
const stddev = Numbers.stdDev(rawPerSecond);
|
||||
const avg = Numbers.mean(rawPerSecond);
|
||||
let consistency = Numbers.roundTo2(Numbers.kogasa(stddev / avg));
|
||||
|
@ -805,7 +770,7 @@ function buildCompletedEvent(
|
|||
|
||||
const chartData = {
|
||||
wpm: TestInput.wpmHistory,
|
||||
raw: rawPerSecond,
|
||||
burst: rawPerSecond,
|
||||
err: chartErr,
|
||||
};
|
||||
|
||||
|
@ -948,7 +913,44 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
TestStats.removeAfkData();
|
||||
}
|
||||
|
||||
const ce = buildCompletedEvent(difficultyFailed);
|
||||
// stats
|
||||
const stats = TestStats.calculateStats();
|
||||
if (stats.time % 1 !== 0 && Config.mode !== "time") {
|
||||
TestStats.setLastSecondNotRound();
|
||||
}
|
||||
|
||||
PaceCaret.setLastTestWpm(stats.wpm);
|
||||
|
||||
// if the last second was not rounded, add another data point to the history
|
||||
if (TestStats.lastSecondNotRound && !difficultyFailed) {
|
||||
const wpmAndRaw = TestStats.calculateWpmAndRaw();
|
||||
TestInput.pushToWpmHistory(wpmAndRaw.wpm);
|
||||
TestInput.pushToRawHistory(wpmAndRaw.raw);
|
||||
TestInput.pushKeypressesToHistory();
|
||||
TestInput.pushErrorToHistory();
|
||||
TestInput.pushAfkToHistory();
|
||||
}
|
||||
|
||||
const rawPerSecond = TestInput.keypressCountHistory.map((count) =>
|
||||
Math.round((count / 5) * 60)
|
||||
);
|
||||
|
||||
//adjust last second if last second is not round
|
||||
// if (TestStats.lastSecondNotRound && stats.time % 1 >= 0.1) {
|
||||
if (
|
||||
Config.mode !== "time" &&
|
||||
TestStats.lastSecondNotRound &&
|
||||
stats.time % 1 >= 0.5
|
||||
) {
|
||||
const timescale = 1 / (stats.time % 1);
|
||||
|
||||
//multiply last element of rawBefore by scale, and round it
|
||||
rawPerSecond[rawPerSecond.length - 1] = Math.round(
|
||||
(rawPerSecond[rawPerSecond.length - 1] as number) * timescale
|
||||
);
|
||||
}
|
||||
|
||||
const ce = buildCompletedEvent(stats, rawPerSecond);
|
||||
|
||||
console.debug("Completed event object", ce);
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ type CharCount = {
|
|||
correctSpaces: number;
|
||||
};
|
||||
|
||||
type Stats = {
|
||||
export type Stats = {
|
||||
wpm: number;
|
||||
wpmRaw: number;
|
||||
acc: number;
|
||||
|
|
|
@ -33,6 +33,51 @@ export function smooth(
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a conditional smoothing algorithm to an array of numbers.
|
||||
* Values are only smoothed if they fall within a specified value window relative to the current value.
|
||||
* This preserves large changes in the data while smoothing smaller variations.
|
||||
* @param arr The input array of numbers.
|
||||
* @param windowSize The size of the window used for smoothing.
|
||||
* @param valueWindowSize The maximum difference allowed for values to be included in smoothing.
|
||||
* @param getter An optional function to extract values from the array elements. Defaults to the identity function.
|
||||
* @returns An array of smoothed values, where each value is the average of itself and nearby values within both the position and value windows.
|
||||
*/
|
||||
export function smoothWithValueWindow(
|
||||
arr: number[],
|
||||
windowSize: number,
|
||||
valueWindowSize: number,
|
||||
getter = (value: number): number => value
|
||||
): number[] {
|
||||
const get = getter;
|
||||
const result = [];
|
||||
|
||||
for (let i = 0; i < arr.length; i += 1) {
|
||||
const currentValue = get(arr[i] as number);
|
||||
const leftOffset = i - windowSize;
|
||||
const from = leftOffset >= 0 ? leftOffset : 0;
|
||||
const to = i + windowSize + 1;
|
||||
|
||||
let count = 0;
|
||||
let sum = 0;
|
||||
|
||||
for (let j = from; j < to && j < arr.length; j += 1) {
|
||||
const neighborValue = get(arr[j] as number);
|
||||
|
||||
// Only include values that are within the value window
|
||||
if (Math.abs(neighborValue - currentValue) <= valueWindowSize) {
|
||||
sum += neighborValue;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If no values were within the window, use the original value
|
||||
result[i] = count > 0 ? sum / count : currentValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle an array of elements using the Fisher–Yates algorithm.
|
||||
* This function mutates the input array.
|
||||
|
|
|
@ -17,9 +17,16 @@ export const IncompleteTestSchema = z.object({
|
|||
});
|
||||
export type IncompleteTest = z.infer<typeof IncompleteTestSchema>;
|
||||
|
||||
export const OldChartDataSchema = z.object({
|
||||
wpm: z.array(z.number().nonnegative()).max(122),
|
||||
raw: z.array(z.number().int().nonnegative()).max(122),
|
||||
err: z.array(z.number().nonnegative()).max(122),
|
||||
});
|
||||
export type OldChartData = z.infer<typeof OldChartDataSchema>;
|
||||
|
||||
export const ChartDataSchema = z.object({
|
||||
wpm: z.array(z.number().nonnegative()).max(122),
|
||||
raw: z.array(z.number().nonnegative()).max(122),
|
||||
burst: z.array(z.number().int().nonnegative()).max(122),
|
||||
err: z.array(z.number().nonnegative()).max(122),
|
||||
});
|
||||
export type ChartData = z.infer<typeof ChartDataSchema>;
|
||||
|
|
Loading…
Add table
Reference in a new issue