Skip to content

Commit 0b3ae89

Browse files
committed
feat: add the typing speed change over time chart
1 parent 952cda5 commit 0b3ae89

File tree

4 files changed

+143
-4
lines changed

4 files changed

+143
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useFormatter } from "@keybr/lesson-ui";
2+
import { Range, Vector } from "@keybr/math";
3+
import { computeSpeed, type Stats, type Step } from "@keybr/textinput";
4+
import {
5+
Canvas,
6+
formatDuration,
7+
type Rect,
8+
type ShapeList,
9+
Shapes,
10+
} from "@keybr/widget";
11+
import { type ReactNode } from "react";
12+
import { Chart, chartArea, type SizeProps } from "./Chart.tsx";
13+
import { withStyles } from "./decoration.ts";
14+
import { paintScatterPlot, projection } from "./graph.ts";
15+
import { type ChartStyles, useChartStyles } from "./use-chart-styles.ts";
16+
17+
export function RollingSpeedChart({
18+
stats,
19+
steps,
20+
width,
21+
height,
22+
}: {
23+
readonly stats: Stats;
24+
readonly steps: readonly Step[];
25+
} & SizeProps): ReactNode {
26+
const styles = useChartStyles();
27+
const paint = usePaint(styles, stats, steps);
28+
return (
29+
<Chart width={width} height={height}>
30+
<Canvas paint={chartArea(styles, paint)} />
31+
</Chart>
32+
);
33+
}
34+
35+
function usePaint(styles: ChartStyles, stats: Stats, steps: readonly Step[]) {
36+
const { formatSpeed } = useFormatter();
37+
const g = withStyles(styles);
38+
39+
const vTime = new Vector();
40+
const vSpeed = new Vector();
41+
const vBumps = new Vector();
42+
const head = steps[0];
43+
for (let index = 1; index < steps.length; index++) {
44+
const delta = Math.min(10, index);
45+
const prev = steps[index - delta];
46+
const curr = steps[index];
47+
const speed = computeSpeed(delta, curr.timeStamp - prev.timeStamp);
48+
vTime.add(curr.timeStamp - head.timeStamp);
49+
vSpeed.add(speed);
50+
if (curr.typo) {
51+
vBumps.add(curr.timeStamp - head.timeStamp);
52+
}
53+
}
54+
55+
const rTime = Range.from(vTime);
56+
const rSpeed = Range.from(vSpeed);
57+
58+
return (box: Rect): ShapeList => {
59+
const proj = projection(box, rTime, rSpeed);
60+
return [
61+
g.paintGrid(box, "horizontal", { lines: 5 }),
62+
g.paintGrid(box, "vertical", { lines: 5 }),
63+
g.paintAxis(box, "bottom"),
64+
g.paintAxis(box, "left"),
65+
paintScatterPlot(proj, vTime, vSpeed, {
66+
style: styles.speed,
67+
}),
68+
pointAverageSpeedLine(),
69+
paintBumps(),
70+
g.paintTicks(box, rTime, "bottom", {
71+
lines: 5,
72+
fmt: (value) => formatDuration(value, { showMillis: true }),
73+
}),
74+
g.paintTicks(box, rSpeed, "left", { fmt: formatSpeed }),
75+
];
76+
77+
function pointAverageSpeedLine(): ShapeList {
78+
const y = Math.round(proj.y(stats.speed));
79+
return [
80+
Shapes.fill(styles.threshold, [
81+
Shapes.rect({
82+
x: box.x - 10,
83+
y: y,
84+
width: box.width + 20,
85+
height: 1,
86+
}),
87+
]),
88+
Shapes.fillText({
89+
x: box.x + box.width + 15,
90+
y: y,
91+
value: formatSpeed(stats.speed),
92+
style: {
93+
...styles.thresholdLabel,
94+
textAlign: "left",
95+
textBaseline: "middle",
96+
},
97+
}),
98+
];
99+
}
100+
101+
function paintBumps(): ShapeList {
102+
const r = Math.round(
103+
Math.min(Math.max(proj.box.width / vTime.length, 2), 6),
104+
);
105+
return Shapes.fill(
106+
styles.accuracy,
107+
[...vBumps].map((time) => {
108+
const x = Math.round(proj.x(time));
109+
return [
110+
Shapes.rect({
111+
x: x,
112+
y: box.y,
113+
width: 1,
114+
height: box.height,
115+
}),
116+
Shapes.circle({
117+
cx: x,
118+
cy: box.y + box.height,
119+
r,
120+
}),
121+
];
122+
}),
123+
);
124+
}
125+
};
126+
}

packages/keybr-chart/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export * from "./KeySpeedChart.tsx";
1010
export * from "./KeySpeedHistogram.tsx";
1111
export * from "./Marker.tsx";
1212
export * from "./ProgressOverviewChart.tsx";
13+
export * from "./RollingSpeedChart.tsx";
1314
export * from "./SpeedChart.tsx";
1415
export * from "./SpeedHistogram.tsx";
1516
export * from "./TimeToTypeHistogram.tsx";

packages/page-typing-test/lib/components/ReportScreen.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
AccuracyHistogram,
33
makeAccuracyDistribution,
44
makeSpeedDistribution,
5+
RollingSpeedChart,
56
SpeedHistogram,
67
TimeToTypeHistogram,
78
} from "@keybr/chart";
@@ -127,6 +128,17 @@ export function ReportScreen({ result }: { readonly result: TestResult }) {
127128

128129
<Para align="center">Time to type a character histogram.</Para>
129130

131+
<Box alignItems="center" justifyContent="center">
132+
<RollingSpeedChart
133+
stats={result.stats}
134+
steps={result.steps}
135+
width="45rem"
136+
height="15rem"
137+
/>
138+
</Box>
139+
140+
<Para align="center">Typing speed change over time chart.</Para>
141+
130142
<Spacer size={3} />
131143

132144
<Replay result={result} />

packages/page-typing-test/lib/mock.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const mockSteps: readonly Step[] = [
4545
timeStamp: 1762.234,
4646
codePoint: 32,
4747
timeToType: 96.30099999999993,
48-
typo: false,
48+
typo: true,
4949
},
5050
{
5151
timeStamp: 1910.01,
@@ -153,7 +153,7 @@ export const mockSteps: readonly Step[] = [
153153
timeStamp: 4137.875,
154154
codePoint: 101,
155155
timeToType: 79.90999999999985,
156-
typo: false,
156+
typo: true,
157157
},
158158
{
159159
timeStamp: 4217.762,
@@ -273,7 +273,7 @@ export const mockSteps: readonly Step[] = [
273273
timeStamp: 6953.907,
274274
codePoint: 110,
275275
timeToType: 59.72400000000016,
276-
typo: false,
276+
typo: true,
277277
},
278278
{
279279
timeStamp: 7097.736,
@@ -561,7 +561,7 @@ export const mockSteps: readonly Step[] = [
561561
timeStamp: 14890.601,
562562
codePoint: 32,
563563
timeToType: 132.44900000000052,
564-
typo: false,
564+
typo: true,
565565
},
566566
{
567567
timeStamp: 15086.195,

0 commit comments

Comments
 (0)