diff --git a/src/Components/ChartPanel.tsx b/src/Components/ChartPanel.tsx index 99ab640e..8ea0f5e3 100644 --- a/src/Components/ChartPanel.tsx +++ b/src/Components/ChartPanel.tsx @@ -100,6 +100,7 @@ export class ChartPanel extends React.Component} 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(} 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(} 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 ( diff --git a/src/Components/FilterPanel.tsx b/src/Components/FilterPanel.tsx index 072f1681..64ed2e00 100644 --- a/src/Components/FilterPanel.tsx +++ b/src/Components/FilterPanel.tsx @@ -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"; @@ -80,14 +80,15 @@ export class FilterPanel extends React.Component, + "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(
@@ -922,6 +935,7 @@ export class FilterPanel extends React.Component diff --git a/src/Helpers/CsvParser.tsx b/src/Helpers/CsvParser.tsx index 2b21c75c..9187810c 100644 --- a/src/Helpers/CsvParser.tsx +++ b/src/Helpers/CsvParser.tsx @@ -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; }; diff --git a/src/Helpers/MathHelpers.tsx b/src/Helpers/MathHelpers.tsx index dee346a5..286052ca 100644 --- a/src/Helpers/MathHelpers.tsx +++ b/src/Helpers/MathHelpers.tsx @@ -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); diff --git a/src/Helpers/Types.tsx b/src/Helpers/Types.tsx index e095b85c..e4ff1788 100644 --- a/src/Helpers/Types.tsx +++ b/src/Helpers/Types.tsx @@ -177,7 +177,8 @@ export interface FilterPanelState { goodTime: number, method: Option, useLogScale: boolean, - use4SegmentTiming: boolean + use4SegmentTiming: boolean, + recordHistoryAllDays: boolean } export interface FileInputProps { @@ -204,7 +205,8 @@ export interface ChartPanelProps { methodName: MethodName, steps: StepName[], useLogScale: boolean, - use4SegmentTiming: boolean + use4SegmentTiming: boolean, + recordHistoryAllDays: boolean } export interface ChartPanelState { diff --git a/src/Workers/chartWorker.ts b/src/Workers/chartWorker.ts index ba997368..f5c68114 100644 --- a/src/Workers/chartWorker.ts +++ b/src/Workers/chartWorker.ts @@ -51,6 +51,7 @@ interface WorkerInput { methodName: MethodName; use4SegmentTiming: boolean; isDark: boolean; + recordHistoryAllDays: boolean; } // ── Streak helpers ─────────────────────────────────────────────────────────── @@ -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) }, ], }; } @@ -342,7 +377,7 @@ function computeBestSolvesData(solves: Solve[]): FastestSolve[] { // ── Main computation ────────────────────────────────────────────────────────── function computeAllChartData(input: WorkerInput): Record { - 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); @@ -371,7 +406,7 @@ function computeAllChartData(input: WorkerInput): Record { 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), diff --git a/src/tests/CubeHelpers.test.tsx b/src/tests/CubeHelpers.test.tsx index 8f8a2539..ff2e4b0c 100644 --- a/src/tests/CubeHelpers.test.tsx +++ b/src/tests/CubeHelpers.test.tsx @@ -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', () => {