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
52 changes: 48 additions & 4 deletions src/Components/ChartPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "../Helpers/Types";
import { Chart as ChartJS, ChartData, CategoryScale } from 'chart.js/auto';
import { createOptions, buildChartHtml } from "../Helpers/ChartHelpers";
import { Row, Spinner, Tooltip } from "react-bootstrap";
import { Row, Col, Card, ButtonGroup, Button, OverlayTrigger, Ratio, Spinner, Tooltip } from "react-bootstrap";
import { ThemeContext } from "../contexts/ThemeContext";
import { Line, Bar, Doughnut } from 'react-chartjs-2';
import { Const } from "../Helpers/Constants";
Expand Down Expand Up @@ -70,7 +70,7 @@ const BEST_SOLVES_COLS = [

export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState & { _propsKey?: string }> {
static contextType = ThemeContext;
state: ChartPanelState & { _propsKey?: string } = { chartData: null, isComputing: false };
state: ChartPanelState & { _propsKey?: string } = { chartData: null, isComputing: false, solvesPerPeriod: 'day' };

static getDerivedStateFromProps(
nextProps: ChartPanelProps,
Expand Down 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 @@ -249,6 +253,34 @@ export class ChartPanel extends React.Component<ChartPanelProps, ChartPanelState
}
charts.push(buildChartHtml(<div style={chartDataGridWrapStyle}><DataGrid style={chartDataGridStyle} rows={c.streakRows as StreakRow[]} columns={STREAK_COLS} /></div>, "Longest Daily Streaks", "How many days in a row you've achieved solves of each time"));
charts.push(buildChartHtml(<Line data={c.dailyRecord as ChartData<"line">} options={createOptions(ChartType.Line, "Date", "Time (s)", p.useLogScale, true, true, isDark)} />, "Daily Fastest Solve", "This chart shows the fastest solve for each day, based on the selected filters"));
{
const period = this.state.solvesPerPeriod;
const periodLabels: Record<typeof period, string> = { day: 'Daily', week: 'Weekly', month: 'Monthly' };
const periodData = { day: c.solvesPerDay, week: c.solvesPerWeek, month: c.solvesPerMonth }[period];
const xAxisLabel = { day: 'Date', week: 'Week Starting', month: 'Month' }[period];
charts.push(
<Col key="solvesPerPeriod" className="col-12 col-md-6">
<Card className="p-2 p-md-3 shadow-sm">
<OverlayTrigger placement="top" overlay={<Tooltip id="tooltip-solves-period">Number of solves completed per day, week, or month</Tooltip>}>
<Card.Text className="text-center fw-bold">Solve Count ⓘ</Card.Text>
</OverlayTrigger>
<div className="d-flex justify-content-center mb-2">
<ButtonGroup size="sm">
{(['day', 'week', 'month'] as const).map(p => (
<Button key={p} variant={period === p ? 'primary' : 'outline-secondary'}
onClick={() => this.setState({ solvesPerPeriod: p })}>
{periodLabels[p]}
</Button>
))}
</ButtonGroup>
</div>
<Ratio aspectRatio="4x3">
<Bar data={periodData as ChartData<"bar">} options={createOptions(ChartType.Bar, xAxisLabel, "Solves", false, false, false, isDark)} />
</Ratio>
</Card>
</Col>
);
}
charts.push(buildChartHtml(<div style={chartDataGridWrapStyle}><DataGrid style={chartDataGridStyle} rows={c.recordRows as RecordRow[]} columns={RECORD_COLS} /></div>, "Current Records", "This chart shows your current records for Single, Ao5, Ao12, Ao100, and Ao1000"));

if (hasOll) {
Expand All @@ -269,7 +301,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
7 changes: 5 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,12 +205,14 @@ export interface ChartPanelProps {
methodName: MethodName,
steps: StepName[],
useLogScale: boolean,
use4SegmentTiming: boolean
use4SegmentTiming: boolean,
recordHistoryAllDays: boolean
}

export interface ChartPanelState {
chartData: Record<string, unknown> | null;
isComputing: boolean;
solvesPerPeriod: 'day' | 'week' | 'month';
}

export interface StreakRow {
Expand Down
Loading