add range selectors to answer button and hour graphs

This commit is contained in:
Damien Elmes 2020-07-17 13:46:06 +10:00
parent f741b05f56
commit ec9e3646c4
13 changed files with 162 additions and 140 deletions

View file

@ -1,18 +1,19 @@
<script lang="typescript"> <script lang="typescript">
import { RevlogRange } from "./graphs"; import { RevlogRange, GraphRange } from "./graphs";
import { timeSpan, MONTH, YEAR } from "../time"; import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { HistogramData } from "./histogram-graph"; import { HistogramData } from "./histogram-graph";
import { gatherData, buildHistogram, GraphData, AddedRange } from "./added"; import { gatherData, buildHistogram, GraphData } from "./added";
import pb from "../backend/proto"; import pb from "../backend/proto";
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
let range: AddedRange = AddedRange.Month; let graphRange: GraphRange = GraphRange.Month;
let addedData: GraphData | null = null; let addedData: GraphData | null = null;
$: if (sourceData) { $: if (sourceData) {
@ -20,14 +21,10 @@
} }
$: if (addedData) { $: if (addedData) {
histogramData = buildHistogram(addedData, range, i18n); histogramData = buildHistogram(addedData, graphRange, i18n);
} }
const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE);
const month = timeSpan(i18n, 1 * MONTH);
const month3 = timeSpan(i18n, 3 * MONTH);
const year = timeSpan(i18n, 1 * YEAR);
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE); const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
</script> </script>
@ -35,22 +32,7 @@
<h1>{title}</h1> <h1>{title}</h1>
<div class="range-box-inner"> <div class="range-box-inner">
<label> <GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
<input type="radio" bind:group={range} value={AddedRange.Month} />
{month}
</label>
<label>
<input type="radio" bind:group={range} value={AddedRange.ThreeMonths} />
{month3}
</label>
<label>
<input type="radio" bind:group={range} value={AddedRange.Year} />
{year}
</label>
<label>
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
{all}
</label>
</div> </div>
<div class="subtitle">{subtitle}</div> <div class="subtitle">{subtitle}</div>

View file

@ -1,20 +1,24 @@
<script lang="typescript"> <script lang="typescript">
import { defaultGraphBounds } from "./graphs"; import { defaultGraphBounds, GraphRange, RevlogRange } from "./graphs";
import AxisTicks from "./AxisTicks.svelte"; import AxisTicks from "./AxisTicks.svelte";
import { gatherData, GraphData, renderButtons } from "./buttons"; import { renderButtons } from "./buttons";
import pb from "../backend/proto"; import pb from "../backend/proto";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import NoDataOverlay from "./NoDataOverlay.svelte"; import NoDataOverlay from "./NoDataOverlay.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
export let revlogRange: RevlogRange;
let graphRange: GraphRange = GraphRange.Year;
const bounds = defaultGraphBounds(); const bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
$: if (sourceData) { $: if (sourceData) {
renderButtons(svg as SVGElement, bounds, gatherData(sourceData), i18n); renderButtons(svg as SVGElement, bounds, sourceData, i18n, graphRange);
} }
const title = i18n.tr(i18n.TR.STATISTICS_ANSWER_BUTTONS_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_ANSWER_BUTTONS_TITLE);
@ -24,6 +28,10 @@
<div class="graph" id="graph-buttons"> <div class="graph" id="graph-buttons">
<h1>{title}</h1> <h1>{title}</h1>
<div class="range-box-inner">
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} />
</div>
<div class="subtitle">{subtitle}</div> <div class="subtitle">{subtitle}</div>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>

View file

@ -2,15 +2,11 @@
import { timeSpan, MONTH, YEAR } from "../time"; import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { HistogramData } from "./histogram-graph"; import { HistogramData } from "./histogram-graph";
import { defaultGraphBounds } from "./graphs"; import { defaultGraphBounds, GraphRange, RevlogRange } from "./graphs";
import { import { gatherData, GraphData, buildHistogram } from "./future-due";
gatherData,
GraphData,
FutureDueRange,
buildHistogram,
} from "./future-due";
import pb from "../backend/proto"; import pb from "../backend/proto";
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
@ -19,21 +15,17 @@
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
let backlog: boolean = true; let backlog: boolean = true;
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
let range: FutureDueRange = FutureDueRange.Month; let graphRange: GraphRange = GraphRange.Month;
$: if (sourceData) { $: if (sourceData) {
graphData = gatherData(sourceData); graphData = gatherData(sourceData);
} }
$: if (graphData) { $: if (graphData) {
histogramData = buildHistogram(graphData, range, backlog, i18n); histogramData = buildHistogram(graphData, graphRange, backlog, i18n);
} }
const title = i18n.tr(i18n.TR.STATISTICS_FUTURE_DUE_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_FUTURE_DUE_TITLE);
const month = timeSpan(i18n, 1 * MONTH);
const month3 = timeSpan(i18n, 3 * MONTH);
const year = timeSpan(i18n, 1 * YEAR);
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
const subtitle = i18n.tr(i18n.TR.STATISTICS_FUTURE_DUE_SUBTITLE); const subtitle = i18n.tr(i18n.TR.STATISTICS_FUTURE_DUE_SUBTITLE);
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX); const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
</script> </script>
@ -47,22 +39,7 @@
{backlogLabel} {backlogLabel}
</label> </label>
<label> <GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
<input type="radio" bind:group={range} value={FutureDueRange.Month} />
{month}
</label>
<label>
<input type="radio" bind:group={range} value={FutureDueRange.Quarter} />
{month3}
</label>
<label>
<input type="radio" bind:group={range} value={FutureDueRange.Year} />
{year}
</label>
<label>
<input type="radio" bind:group={range} value={FutureDueRange.AllTime} />
{all}
</label>
</div> </div>
<div class="subtitle">{subtitle}</div> <div class="subtitle">{subtitle}</div>

View file

@ -0,0 +1,33 @@
<script lang="typescript">
import { I18n } from "../i18n";
import { RevlogRange, GraphRange } from "./graphs";
import { timeSpan, MONTH, YEAR } from "../time";
export let i18n: I18n;
export let revlogRange: RevlogRange;
export let graphRange: GraphRange;
const month = timeSpan(i18n, 1 * MONTH);
const month3 = timeSpan(i18n, 3 * MONTH);
const year = timeSpan(i18n, 1 * YEAR);
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
</script>
<label>
<input type="radio" bind:group={graphRange} value={GraphRange.Month} />
{month}
</label>
<label>
<input type="radio" bind:group={graphRange} value={GraphRange.ThreeMonths} />
{month3}
</label>
<label>
<input type="radio" bind:group={graphRange} value={GraphRange.Year} />
{year}
</label>
{#if revlogRange === RevlogRange.All}
<label>
<input type="radio" bind:group={graphRange} value={GraphRange.AllTime} />
{all}
</label>
{/if}

View file

@ -138,11 +138,11 @@
<TodayStats {sourceData} {i18n} /> <TodayStats {sourceData} {i18n} />
<CardCounts {sourceData} {i18n} /> <CardCounts {sourceData} {i18n} />
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} /> <CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
<FutureDue {sourceData} {revlogRange} {i18n} /> <FutureDue {sourceData} {i18n} />
<ReviewsGraph {sourceData} {revlogRange} {i18n} /> <ReviewsGraph {sourceData} {revlogRange} {i18n} />
<IntervalsGraph {sourceData} {i18n} /> <IntervalsGraph {sourceData} {i18n} />
<EaseGraph {sourceData} {i18n} /> <EaseGraph {sourceData} {i18n} />
<HourGraph {sourceData} {i18n} /> <HourGraph {sourceData} {revlogRange} {i18n} />
<ButtonsGraph {sourceData} {i18n} /> <ButtonsGraph {sourceData} {revlogRange} {i18n} />
<AddedGraph {sourceData} {revlogRange} {i18n} /> <AddedGraph {sourceData} {i18n} />
{/if} {/if}

View file

@ -1,20 +1,24 @@
<script lang="typescript"> <script lang="typescript">
import { defaultGraphBounds } from "./graphs"; import { timeSpan, MONTH, YEAR } from "../time";
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graphs";
import AxisTicks from "./AxisTicks.svelte"; import AxisTicks from "./AxisTicks.svelte";
import { gatherData, GraphData, renderHours } from "./hours"; import { renderHours } from "./hours";
import pb from "../backend/proto"; import pb from "../backend/proto";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import NoDataOverlay from "./NoDataOverlay.svelte"; import NoDataOverlay from "./NoDataOverlay.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
export let revlogRange: RevlogRange;
let graphRange: GraphRange = GraphRange.Year;
const bounds = defaultGraphBounds(); const bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
$: if (sourceData) { $: if (sourceData) {
renderHours(svg as SVGElement, bounds, gatherData(sourceData), i18n); renderHours(svg as SVGElement, bounds, sourceData, i18n, graphRange);
} }
const title = i18n.tr(i18n.TR.STATISTICS_HOURS_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_HOURS_TITLE);
@ -24,6 +28,10 @@
<div class="graph" id="graph-hour"> <div class="graph" id="graph-hour">
<h1>{title}</h1> <h1>{title}</h1>
<div class="range-box-inner">
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} />
</div>
<div class="subtitle">{subtitle}</div> <div class="subtitle">{subtitle}</div>
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}> <svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>

View file

@ -1,12 +1,13 @@
<script lang="typescript"> <script lang="typescript">
import { HistogramData, histogramGraph } from "./histogram-graph"; import { HistogramData, histogramGraph } from "./histogram-graph";
import AxisTicks from "./AxisTicks.svelte"; import AxisTicks from "./AxisTicks.svelte";
import { defaultGraphBounds, RevlogRange } from "./graphs"; import { defaultGraphBounds, RevlogRange, GraphRange } from "./graphs";
import { GraphData, gatherData, renderReviews, ReviewRange } from "./reviews"; import { GraphData, gatherData, renderReviews } from "./reviews";
import pb from "../backend/proto"; import pb from "../backend/proto";
import { timeSpan, MONTH, YEAR } from "../time"; import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import NoDataOverlay from "./NoDataOverlay.svelte"; import NoDataOverlay from "./NoDataOverlay.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let revlogRange: RevlogRange; export let revlogRange: RevlogRange;
@ -16,7 +17,7 @@
let bounds = defaultGraphBounds(); let bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
let range: ReviewRange = ReviewRange.Month; let graphRange: GraphRange = GraphRange.Month;
let showTime = false; let showTime = false;
$: if (sourceData) { $: if (sourceData) {
@ -24,14 +25,10 @@
} }
$: if (graphData) { $: if (graphData) {
renderReviews(svg as SVGElement, bounds, graphData, range, showTime, i18n); renderReviews(svg as SVGElement, bounds, graphData, graphRange, showTime, i18n);
} }
const title = i18n.tr(i18n.TR.STATISTICS_REVIEWS_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_REVIEWS_TITLE);
const month = timeSpan(i18n, 1 * MONTH);
const month3 = timeSpan(i18n, 3 * MONTH);
const year = timeSpan(i18n, 1 * YEAR);
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
const time = i18n.tr(i18n.TR.STATISTICS_REVIEWS_TIME_CHECKBOX); const time = i18n.tr(i18n.TR.STATISTICS_REVIEWS_TIME_CHECKBOX);
let subtitle: string; let subtitle: string;
@ -51,24 +48,7 @@
{time} {time}
</label> </label>
<label> <GraphRangeRadios bind:graphRange {i18n} {revlogRange} />
<input type="radio" bind:group={range} value={ReviewRange.Month} />
{month}
</label>
<label>
<input type="radio" bind:group={range} value={ReviewRange.ThreeMonths} />
{month3}
</label>
<label>
<input type="radio" bind:group={range} value={ReviewRange.Year} />
{year}
</label>
{#if revlogRange === RevlogRange.All}
<label>
<input type="radio" bind:group={range} value={ReviewRange.AllTime} />
{all}
</label>
{/if}
</div> </div>
<div class="subtitle">{subtitle}</div> <div class="subtitle">{subtitle}</div>

View file

@ -13,13 +13,7 @@ import { HistogramData } from "./histogram-graph";
import { interpolateBlues } from "d3-scale-chromatic"; import { interpolateBlues } from "d3-scale-chromatic";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { dayLabel } from "../time"; import { dayLabel } from "../time";
import { GraphRange } from "./graphs";
export enum AddedRange {
Month = 0,
ThreeMonths = 1,
Year = 2,
AllTime = 3,
}
export interface GraphData { export interface GraphData {
daysAdded: number[]; daysAdded: number[];
@ -35,7 +29,7 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
export function buildHistogram( export function buildHistogram(
data: GraphData, data: GraphData,
range: AddedRange, range: GraphRange,
i18n: I18n i18n: I18n
): HistogramData | null { ): HistogramData | null {
// get min/max // get min/max
@ -49,16 +43,16 @@ export function buildHistogram(
// cap max to selected range // cap max to selected range
switch (range) { switch (range) {
case AddedRange.Month: case GraphRange.Month:
xMin = -31; xMin = -31;
break; break;
case AddedRange.ThreeMonths: case GraphRange.ThreeMonths:
xMin = -90; xMin = -90;
break; break;
case AddedRange.Year: case GraphRange.Year:
xMin = -365; xMin = -365;
break; break;
case AddedRange.AllTime: case GraphRange.AllTime:
break; break;
} }
const xMax = 1; const xMax = 1;

View file

@ -13,7 +13,12 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale"; import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis"; import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip"; import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds, setDataAvailable } from "./graphs"; import {
GraphBounds,
setDataAvailable,
GraphRange,
millisecondCutoffForRange,
} from "./graphs";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { sum } from "d3-array"; import { sum } from "d3-array";
@ -27,12 +32,20 @@ export interface GraphData {
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData { export function gatherData(
data: pb.BackendProto.GraphsOut,
range: GraphRange
): GraphData {
const cutoff = millisecondCutoffForRange(range, data.nextDayAtSecs);
const learning: ButtonCounts = [0, 0, 0, 0]; const learning: ButtonCounts = [0, 0, 0, 0];
const young: ButtonCounts = [0, 0, 0, 0]; const young: ButtonCounts = [0, 0, 0, 0];
const mature: ButtonCounts = [0, 0, 0, 0]; const mature: ButtonCounts = [0, 0, 0, 0];
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) { for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
if (cutoff && (review.id as number) < cutoff) {
continue;
}
let buttonNum = review.buttonChosen; let buttonNum = review.buttonChosen;
if (buttonNum <= 0 || buttonNum > 4) { if (buttonNum <= 0 || buttonNum > 4) {
continue; continue;
@ -80,9 +93,11 @@ interface TotalCorrect {
export function renderButtons( export function renderButtons(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData, origData: pb.BackendProto.GraphsOut,
i18n: I18n i18n: I18n,
range: GraphRange
): void { ): void {
const sourceData = gatherData(origData, range);
const data = [ const data = [
...sourceData.learning.map((count: number, idx: number) => { ...sourceData.learning.map((count: number, idx: number) => {
return { return {

View file

@ -14,18 +14,12 @@ import { HistogramData } from "./histogram-graph";
import { interpolateGreens } from "d3-scale-chromatic"; import { interpolateGreens } from "d3-scale-chromatic";
import { dayLabel } from "../time"; import { dayLabel } from "../time";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
import { GraphRange } from "./graphs";
export interface GraphData { export interface GraphData {
dueCounts: Map<number, number>; dueCounts: Map<number, number>;
} }
export enum FutureDueRange {
Month = 0,
Quarter = 1,
Year = 2,
AllTime = 3,
}
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData { export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
const due = (data.cards as pb.BackendProto.Card[]) const due = (data.cards as pb.BackendProto.Card[])
.filter((c) => c.queue == CardQueue.Review) // && c.due >= data.daysElapsed) .filter((c) => c.queue == CardQueue.Review) // && c.due >= data.daysElapsed)
@ -44,7 +38,7 @@ function binValue(d: Bin<Map<number, number>, number>): number {
export function buildHistogram( export function buildHistogram(
sourceData: GraphData, sourceData: GraphData,
range: FutureDueRange, range: GraphRange,
backlog: boolean, backlog: boolean,
i18n: I18n i18n: I18n
): HistogramData | null { ): HistogramData | null {
@ -63,16 +57,16 @@ export function buildHistogram(
// cap max to selected range // cap max to selected range
switch (range) { switch (range) {
case FutureDueRange.Month: case GraphRange.Month:
xMax = 31; xMax = 31;
break; break;
case FutureDueRange.Quarter: case GraphRange.ThreeMonths:
xMax = 90; xMax = 90;
break; break;
case FutureDueRange.Year: case GraphRange.Year:
xMax = 365; xMax = 365;
break; break;
case FutureDueRange.AllTime: case GraphRange.AllTime:
break; break;
} }
xMax = xMax! + 1; xMax = xMax! + 1;

View file

@ -35,11 +35,20 @@ export async function getGraphData(
return pb.BackendProto.GraphsOut.decode(bytes); return pb.BackendProto.GraphsOut.decode(bytes);
} }
// amount of data to fetch from backend
export enum RevlogRange { export enum RevlogRange {
Year = 1, Year = 1,
All = 2, All = 2,
} }
// period a graph should cover
export enum GraphRange {
Month = 0,
ThreeMonths = 1,
Year = 2,
AllTime = 3,
}
export interface GraphsContext { export interface GraphsContext {
cards: pb.BackendProto.Card[]; cards: pb.BackendProto.Card[];
revlog: pb.BackendProto.RevlogEntry[]; revlog: pb.BackendProto.RevlogEntry[];
@ -77,3 +86,26 @@ export function setDataAvailable(
.duration(600) .duration(600)
.attr("opacity", available ? 0 : 1); .attr("opacity", available ? 0 : 1);
} }
export function millisecondCutoffForRange(
range: GraphRange,
nextDayAtSecs: number
): number {
let days;
switch (range) {
case GraphRange.Month:
days = 31;
break;
case GraphRange.ThreeMonths:
days = 90;
break;
case GraphRange.Year:
days = 365;
break;
case GraphRange.AllTime:
default:
return 0;
}
return (nextDayAtSecs - 86400 * days) * 1000;
}

View file

@ -13,7 +13,12 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale"; import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis"; import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip"; import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds, setDataAvailable } from "./graphs"; import {
GraphBounds,
setDataAvailable,
GraphRange,
millisecondCutoffForRange,
} from "./graphs";
import { area, curveBasis } from "d3-shape"; import { area, curveBasis } from "d3-shape";
import { I18n } from "../i18n"; import { I18n } from "../i18n";
@ -25,21 +30,21 @@ interface Hour {
correctCount: number; correctCount: number;
} }
export interface GraphData {
hours: Hour[];
}
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData { function gatherData(data: pb.BackendProto.GraphsOut, range: GraphRange): Hour[] {
const hours = [...Array(24)].map((_n, idx: number) => { const hours = [...Array(24)].map((_n, idx: number) => {
return { hour: idx, totalCount: 0, correctCount: 0 } as Hour; return { hour: idx, totalCount: 0, correctCount: 0 } as Hour;
}); });
const cutoff = millisecondCutoffForRange(range, data.nextDayAtSecs);
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) { for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
if (review.reviewKind == ReviewKind.EARLY_REVIEW) { if (review.reviewKind == ReviewKind.EARLY_REVIEW) {
continue; continue;
} }
if (cutoff && (review.id as number) < cutoff) {
continue;
}
const hour = Math.floor( const hour = Math.floor(
(((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24 (((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24
@ -50,16 +55,17 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
} }
} }
return { hours }; return hours;
} }
export function renderHours( export function renderHours(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData, origData: pb.BackendProto.GraphsOut,
i18n: I18n i18n: I18n,
range: GraphRange
): void { ): void {
const data = sourceData.hours; const data = gatherData(origData, range);
const yMax = Math.max(...data.map((d) => d.totalCount)); const yMax = Math.max(...data.map((d) => d.totalCount));

View file

@ -18,7 +18,7 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleSequential } from "d3-scale"; import { scaleLinear, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis"; import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip"; import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds, setDataAvailable } from "./graphs"; import { GraphBounds, setDataAvailable, GraphRange } from "./graphs";
import { area, curveBasis } from "d3-shape"; import { area, curveBasis } from "d3-shape";
import { min, histogram, sum, max, Bin, cumsum } from "d3-array"; import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
import { timeSpan, dayLabel } from "../time"; import { timeSpan, dayLabel } from "../time";
@ -38,13 +38,6 @@ export interface GraphData {
reviewTime: Map<number, Reviews>; reviewTime: Map<number, Reviews>;
} }
export enum ReviewRange {
Month = 0,
ThreeMonths = 1,
Year = 2,
AllTime = 3,
}
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind; const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
type BinType = Bin<Map<number, Reviews[]>, number>; type BinType = Bin<Map<number, Reviews[]>, number>;
@ -112,7 +105,7 @@ export function renderReviews(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
sourceData: GraphData, sourceData: GraphData,
range: ReviewRange, range: GraphRange,
showTime: boolean, showTime: boolean,
i18n: I18n i18n: I18n
): void { ): void {
@ -123,16 +116,16 @@ export function renderReviews(
let xMin = 0; let xMin = 0;
// cap max to selected range // cap max to selected range
switch (range) { switch (range) {
case ReviewRange.Month: case GraphRange.Month:
xMin = -31; xMin = -31;
break; break;
case ReviewRange.ThreeMonths: case GraphRange.ThreeMonths:
xMin = -90; xMin = -90;
break; break;
case ReviewRange.Year: case GraphRange.Year:
xMin = -365; xMin = -365;
break; break;
case ReviewRange.AllTime: case GraphRange.AllTime:
xMin = min(sourceData.reviewCount.keys())!; xMin = min(sourceData.reviewCount.keys())!;
break; break;
} }