Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/Components/ChartPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState
private _lastMethodNameRef: MethodName = MethodName.CFOP;
private _lastUse4SegmentTimingRef: boolean = false;
private _lastIsDarkRef: boolean = false;
private _lastRecordHistoryAllDaysRef: boolean = false;

private _isDark(): boolean {
return (this.context as { isDark?: boolean } | undefined)?.isDark ?? false;
Expand All @@ -119,7 +120,8 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState
this._lastBadTimeRef !== p.badTime ||
this._lastMethodNameRef !== p.methodName ||
this._lastUse4SegmentTimingRef !== p.use4SegmentTiming ||
this._lastIsDarkRef !== isDark
this._lastIsDarkRef !== isDark ||
this._lastRecordHistoryAllDaysRef !== p.recordHistoryAllDays
);
}

Expand All @@ -136,6 +138,7 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState
this._lastMethodNameRef = p.methodName;
this._lastUse4SegmentTimingRef = p.use4SegmentTiming;
this._lastIsDarkRef = isDark;
this._lastRecordHistoryAllDaysRef = p.recordHistoryAllDays;
}

private _sendWork(): void {
Expand All @@ -155,6 +158,7 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState
methodName: p.methodName,
use4SegmentTiming: p.use4SegmentTiming,
isDark: this._isDark(),
recordHistoryAllDays: p.recordHistoryAllDays,
});
}

Expand Down Expand Up @@ -269,7 +273,19 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState

if (p.steps.length === Const.MethodSteps[p.methodName].length) {
charts.push(buildChartHtml(<Line data={c.goodBad as ChartData<"line">} options={createOptions(ChartType.Line, "Solve Number", "Percentage", p.useLogScale, true, false, isDark)} />, "Percentage of 'Good' and 'Bad' Solves", "This chart shows your running average of solves considered 'good' and 'bad'. This can be configured in the filter panel. Just set the good and bad values to times you feel are correct"));
charts.push(buildChartHtml(<Line data={c.recordHistory as ChartData<"line">} options={createOptions(ChartType.Line, "Date", "Time (s)", p.useLogScale, true, true, isDark)} />, "History of Records", "This chart shows your history of PBs. Note that this will only show solves that meet the criteria in your filters, so don't be alarmed if you don't see your PB here. As a note, Ao12 removes the best and worst solves of the 12. Ao100 removes the best and worst 5. Ao1000 removes the best and worst 50."));
{
const rh = c.recordHistory as { datasets: unknown[]; xAxisMin?: Date; xAxisMax?: Date };
const rhOptions = createOptions(ChartType.Line, "Date", "Time (s)", p.useLogScale, true, true, isDark);
if (p.recordHistoryAllDays) {
// 'timeseries' evenly spaces data points so gaps disappear; 'time' renders
// a continuous timeline. Also pin min/max so the axis spans all solve dates.
(rhOptions as any).scales.x.type = 'time';
const DAY_MS = 86_400_000;
if (rh.xAxisMin) (rhOptions as any).scales.x.min = rh.xAxisMin.valueOf() - 3 * DAY_MS;
if (rh.xAxisMax) (rhOptions as any).scales.x.max = rh.xAxisMax.valueOf() + 3 * DAY_MS;
}
charts.push(buildChartHtml(<Line data={rh as unknown as ChartData<"line">} options={rhOptions} />, "History of Records", "This chart shows your history of PBs. Note that this will only show solves that meet the criteria in your filters, so don't be alarmed if you don't see your PB here. As a note, Ao12 removes the best and worst solves of the 12. Ao100 removes the best and worst 5. Ao1000 removes the best and worst 50."));
}
}

return (
Expand Down
30 changes: 22 additions & 8 deletions src/Components/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MultiSelect } from "react-multi-select-component";
import { CrossColor, FilterPanelProps, FilterPanelState, Filters, getStep, MethodName, Option, Solve, SolveCleanliness, SolveLuckiness, Step, StepName } from "../Helpers/Types";
import { ChartPanel } from "./ChartPanel";
import { calculateMovingAverage, calculateMovingStdDev } from "../Helpers/MathHelpers";
import { FormControl, Card, Row, Offcanvas, Col, Button, Tooltip, OverlayTrigger, Alert, Container, CardText, Spinner } from 'react-bootstrap';
import { FormControl, Card, Row, Offcanvas, Col, Button, Tooltip, OverlayTrigger, Alert, Container, Spinner } from 'react-bootstrap';
import { Const } from "../Helpers/Constants";
import { CalculateAllSessionOptions, CalculateBenchmarkTimes, CalculateWindowSize } from "../Helpers/CubeHelpers";
import ReactSwitch from "react-switch";
Expand Down Expand Up @@ -80,14 +80,15 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
goodTime: 15,
method: { label: MethodName.CFOP, value: MethodName.CFOP },
useLogScale: false,
use4SegmentTiming: true
use4SegmentTiming: true,
recordHistoryAllDays: false
}

static passesFilters(solve: Solve, filters: Filters) {
if (solve.isCorrupt) {
return false;
}
if (solve.method != filters.method) {
if (solve.method !== filters.method) {
return false;
}
if (filters.sources.indexOf(solve.source) < 0) {
Expand Down Expand Up @@ -115,11 +116,11 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt

// TODO: check case logic properly
const pllStep = getStep(solve, StepName.PLL);
if (solve.method == MethodName.CFOP && pllStep?.case !== undefined && filters.pllCases.indexOf(pllStep.case) < 0) {
if (solve.method === MethodName.CFOP && pllStep?.case !== undefined && filters.pllCases.indexOf(pllStep.case) < 0) {
return false;
}
const ollStep = getStep(solve, StepName.OLL);
if (solve.method == MethodName.CFOP && ollStep?.case !== undefined && filters.ollCases.indexOf(ollStep.case) < 0) {
if (solve.method === MethodName.CFOP && ollStep?.case !== undefined && filters.ollCases.indexOf(ollStep.case) < 0) {
return false;
}

Expand Down Expand Up @@ -158,7 +159,7 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt

// For each step, check if it is 3 standard deviations more than the average
static markAllMistakes(allSolves: Solve[], windowSize: number): Solve[] {
if (allSolves.length == 0) {
if (allSolves.length === 0) {
return [];
}

Expand Down Expand Up @@ -302,7 +303,8 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
badTime: prevState.badTime,
goodTime: prevState.goodTime,
useLogScale: prevState.useLogScale,
use4SegmentTiming: prevState.use4SegmentTiming
use4SegmentTiming: prevState.use4SegmentTiming,
recordHistoryAllDays: prevState.recordHistoryAllDays
};
}

Expand Down Expand Up @@ -334,7 +336,8 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
badTime: prevState.badTime,
goodTime: prevState.goodTime,
useLogScale: prevState.useLogScale,
use4SegmentTiming: prevState.use4SegmentTiming
use4SegmentTiming: prevState.use4SegmentTiming,
recordHistoryAllDays: prevState.recordHistoryAllDays
}

// Update anything that needs it
Expand Down Expand Up @@ -567,6 +570,10 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
this.setState({ use4SegmentTiming: checked });
}

setRecordHistoryAllDays(checked: boolean) {
this.setState({ recordHistoryAllDays: checked });
}

setCleanliness(selectedList: any[]) {
this.setState({
solveCleanliness: selectedList,
Expand Down Expand Up @@ -853,6 +860,12 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
"Show recognition, pre-AUF, execution, and post-AUF as separate segments in timing charts. When off, shows only recognition and execution."
)}

{this.createFilterHtml(
<ReactSwitch id="recordHistoryAllDays" checked={this.state.recordHistoryAllDays} onChange={this.setRecordHistoryAllDays.bind(this)} />,
"Record History: All Days",
"When on, the History of Records chart shows every calendar day from the first to the last solve. When off (default), only days where a new record was set are shown."
)}

{this.createFilterHtml(
<div className="row align-items-center g-2">
<div className="col-auto d-flex align-items-center gap-2">
Expand Down Expand Up @@ -922,6 +935,7 @@ export class FilterPanel extends React.Component<FilterPanelProps, FilterPanelSt
steps={this.state.filters.steps}
useLogScale={this.state.useLogScale}
use4SegmentTiming={this.state.use4SegmentTiming}
recordHistoryAllDays={this.state.recordHistoryAllDays}
/>
</Col>
</Row>
Expand Down
2 changes: 1 addition & 1 deletion src/Helpers/CsvParser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ function parseCubeastCsv(stringVal: string, splitter: string): Solve[] {
"date": (obj, value) => { obj.date = moment.utc(value, 'YYYY-MM-DD hh:mm:ss').toDate(); },
"solution_rotation": (obj, value) => {
obj.crossColor = Const.crossMappings.get(value) ?? CrossColor.Unknown;
if (obj.crossColor == CrossColor.Unknown) {
if (obj.crossColor === CrossColor.Unknown) {
//console.log("Unknown solution rotation: ", value);
//obj.isCorrupt = true;
};
Expand Down
2 changes: 0 additions & 2 deletions src/Helpers/MathHelpers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { Deque } from "@datastructures-js/deque";
import { Const } from "./Constants";
import { Records } from "./Types";
var Set = require("sorted-set");

export function sumArray(data: number[]): number {
return data.reduce((acc, curr) => acc + curr, 0);
Expand Down
6 changes: 4 additions & 2 deletions src/Helpers/Types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ export interface FilterPanelState {
goodTime: number,
method: Option,
useLogScale: boolean,
use4SegmentTiming: boolean
use4SegmentTiming: boolean,
recordHistoryAllDays: boolean
}

export interface FileInputProps {
Expand All @@ -204,7 +205,8 @@ export interface ChartPanelProps {
methodName: MethodName,
steps: StepName[],
useLogScale: boolean,
use4SegmentTiming: boolean
use4SegmentTiming: boolean,
recordHistoryAllDays: boolean
}

export interface ChartPanelState {
Expand Down
69 changes: 52 additions & 17 deletions src/Workers/chartWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface WorkerInput {
methodName: MethodName;
use4SegmentTiming: boolean;
isDark: boolean;
recordHistoryAllDays: boolean;
}

// ── Streak helpers ───────────────────────────────────────────────────────────
Expand Down Expand Up @@ -111,40 +112,74 @@ function buildRecordRows(solves: Solve[]): RecordRow[] {
const ao5 = Math.min.apply(null, calculateMovingAverage(times, 5));
const ao12 = Math.min.apply(null, calculateMovingAverageChopped(times, 12, 1));
const ao100 = Math.min.apply(null, calculateMovingAverageChopped(times, 100, 5));
//const ao1000 = Math.min.apply(null, calculateMovingAverageChopped(times, 1000, 50));
const ao1000 = Math.min.apply(null, calculateMovingAverageChopped(times, 1000, 50));
return [
{ recordType: 'Single', time: single.toFixed(3) },
{ recordType: 'Ao5', time: ao5.toFixed(3) },
{ recordType: 'Ao12', time: ao12.toFixed(3) },
{ recordType: 'Ao100', time: ao100.toFixed(3) },
//{ recordType: 'Ao1000', time: ao1000.toFixed(3) },
{ recordType: 'Ao1000', time: ao1000.toFixed(3) },
];
}

function buildRecordDataset(dates: Date[], times: number[]) {
const records = [{ x: dates[0], y: times[0] }];
for (let i = 1; i < times.length; i++) {
if (times[i] < records[records.length - 1].y) {
records.push({ x: dates[i], y: times[i] });
function buildRecordDatasetDaily(dates: Date[], times: number[]) {
if (dates.length === 0) return [];

// Step 1: group by day, keep the minimum value per day
const dayMap: { [key: string]: { date: Date; value: number } } = {};
for (let i = 0; i < dates.length; i++) {
const day = dates[i].toLocaleDateString('en-CA');
if (!dayMap[day] || times[i] < dayMap[day].value) {
dayMap[day] = { date: dates[i], value: times[i] };
}
}
const sortedKeys = Object.keys(dayMap).sort();
const dayData = sortedKeys.map(k => dayMap[k]);

// Step 2: build the record history (only keep days that set a new PB)
const records: { date: Date; value: number }[] = [dayData[0]];
for (let i = 1; i < dayData.length; i++) {
if (dayData[i].value < records[records.length - 1].value) {
records.push(dayData[i]);
}
}
return records;

return records.map(r => ({ x: r.date, y: r.value }));
}

function buildRecordHistory(solves: Solve[]) {
function buildRecordHistory(solves: Solve[], allDays: boolean) {
const dates = solves.map(x => x.date);
const times = solves.map(x => x.time);
const ao5 = calculateMovingAverage(times, 5);
const ao12 = calculateMovingAverageChopped(times, 12, 1);
const ao100 = calculateMovingAverageChopped(times, 100, 5);
//const ao1000 = calculateMovingAverageChopped(times, 1000, 50);
const ao1000 = calculateMovingAverageChopped(times, 1000, 50);

// When allDays is on, expose the full solve date range so ChartPanel can set
// min/max on the 'time' scale — no null anchors needed, avoids rendering issues.
let xAxisMin: Date | undefined;
let xAxisMax: Date | undefined;
if (allDays && dates.length > 0) {
let minMs = dates[0].valueOf();
let maxMs = dates[0].valueOf();
for (let i = 1; i < dates.length; i++) {
const ms = dates[i].valueOf();
if (ms < minMs) minMs = ms;
if (ms > maxMs) maxMs = ms;
}
xAxisMin = new Date(minMs);
xAxisMax = new Date(maxMs);
}

return {
xAxisMin,
xAxisMax,
datasets: [
{ label: 'Record Single', data: buildRecordDataset(dates, times) },
{ label: 'Record Ao5', data: buildRecordDataset(dates.slice(4), ao5) },
{ label: 'Record Ao12', data: buildRecordDataset(dates.slice(11), ao12) },
{ label: 'Record Ao100', data: buildRecordDataset(dates.slice(99), ao100) },
//{ label: 'Record Ao1000', data: buildRecordDataset(dates.slice(999), ao1000) },
{ label: 'Record Single', data: buildRecordDatasetDaily(dates, times) },
{ label: 'Record Ao5', data: buildRecordDatasetDaily(dates.slice(4), ao5) },
{ label: 'Record Ao12', data: buildRecordDatasetDaily(dates.slice(11), ao12) },
{ label: 'Record Ao100', data: buildRecordDatasetDaily(dates.slice(99), ao100) },
{ label: 'Record Ao1000', data: buildRecordDatasetDaily(dates.slice(999), ao1000) },
],
};
}
Expand Down Expand Up @@ -342,7 +377,7 @@ function computeBestSolvesData(solves: Solve[]): FastestSolve[] {
// ── Main computation ──────────────────────────────────────────────────────────

function computeAllChartData(input: WorkerInput): Record<string, unknown> {
const { solves, windowSize, pointsPerGraph, steps, goodTime, badTime, methodName, use4SegmentTiming, isDark } = input;
const { solves, windowSize, pointsPerGraph, steps, goodTime, badTime, methodName, use4SegmentTiming, isDark, recordHistoryAllDays } = input;
const hasOll = steps.includes(StepName.OLL);
const hasPll = steps.includes(StepName.PLL);

Expand Down Expand Up @@ -371,7 +406,7 @@ function computeAllChartData(input: WorkerInput): Record<string, unknown> {
streakRows: buildAllStreakRows(solves),
recordRows: buildRecordRows(solves),
goodBad: buildGoodBadData(solves, windowSize, pointsPerGraph, goodTime, badTime),
recordHistory: buildRecordHistory(solves),
recordHistory: buildRecordHistory(solves, recordHistoryAllDays),
stepPercentages: buildStepPercentages(solves, steps, windowSize),
typicalCompare: buildTypicalCompare(solves, windowSize),
bestSolvesData: computeBestSolvesData(solves),
Expand Down
2 changes: 1 addition & 1 deletion src/tests/CubeHelpers.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GetEmptyStep, GetEmptySolve, CalculateBenchmarkTimes, CalculateWindowSize, CalculateMostUsedMethod, CalculateAllSessionOptions } from '../Helpers/CubeHelpers';
import { describe, expect, test } from '@jest/globals';
import { expect, test } from '@jest/globals';
import { MethodName, Solve } from '../Helpers/Types';

test('GetEmptyStep returns an empty step', () => {
Expand Down