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">
import { RevlogRange } from "./graphs";
import { RevlogRange, GraphRange } from "./graphs";
import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n";
import { HistogramData } from "./histogram-graph";
import { gatherData, buildHistogram, GraphData, AddedRange } from "./added";
import { gatherData, buildHistogram, GraphData } from "./added";
import pb from "../backend/proto";
import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n;
let svg = null as HTMLElement | SVGElement | null;
let histogramData = null as HistogramData | null;
let range: AddedRange = AddedRange.Month;
let graphRange: GraphRange = GraphRange.Month;
let addedData: GraphData | null = null;
$: if (sourceData) {
@ -20,14 +21,10 @@
}
$: if (addedData) {
histogramData = buildHistogram(addedData, range, i18n);
histogramData = buildHistogram(addedData, graphRange, i18n);
}
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);
</script>
@ -35,22 +32,7 @@
<h1>{title}</h1>
<div class="range-box-inner">
<label>
<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>
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
</div>
<div class="subtitle">{subtitle}</div>

View file

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

View file

@ -2,15 +2,11 @@
import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n";
import { HistogramData } from "./histogram-graph";
import { defaultGraphBounds } from "./graphs";
import {
gatherData,
GraphData,
FutureDueRange,
buildHistogram,
} from "./future-due";
import { defaultGraphBounds, GraphRange, RevlogRange } from "./graphs";
import { gatherData, GraphData, buildHistogram } from "./future-due";
import pb from "../backend/proto";
import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n;
@ -19,21 +15,17 @@
let histogramData = null as HistogramData | null;
let backlog: boolean = true;
let svg = null as HTMLElement | SVGElement | null;
let range: FutureDueRange = FutureDueRange.Month;
let graphRange: GraphRange = GraphRange.Month;
$: if (sourceData) {
graphData = gatherData(sourceData);
}
$: 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 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 backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
</script>
@ -47,22 +39,7 @@
{backlogLabel}
</label>
<label>
<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>
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
</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} />
<CardCounts {sourceData} {i18n} />
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
<FutureDue {sourceData} {revlogRange} {i18n} />
<FutureDue {sourceData} {i18n} />
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
<IntervalsGraph {sourceData} {i18n} />
<EaseGraph {sourceData} {i18n} />
<HourGraph {sourceData} {i18n} />
<ButtonsGraph {sourceData} {i18n} />
<AddedGraph {sourceData} {revlogRange} {i18n} />
<HourGraph {sourceData} {revlogRange} {i18n} />
<ButtonsGraph {sourceData} {revlogRange} {i18n} />
<AddedGraph {sourceData} {i18n} />
{/if}

View file

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

View file

@ -1,12 +1,13 @@
<script lang="typescript">
import { HistogramData, histogramGraph } from "./histogram-graph";
import AxisTicks from "./AxisTicks.svelte";
import { defaultGraphBounds, RevlogRange } from "./graphs";
import { GraphData, gatherData, renderReviews, ReviewRange } from "./reviews";
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graphs";
import { GraphData, gatherData, renderReviews } from "./reviews";
import pb from "../backend/proto";
import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n";
import NoDataOverlay from "./NoDataOverlay.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let revlogRange: RevlogRange;
@ -16,7 +17,7 @@
let bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null;
let range: ReviewRange = ReviewRange.Month;
let graphRange: GraphRange = GraphRange.Month;
let showTime = false;
$: if (sourceData) {
@ -24,14 +25,10 @@
}
$: 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 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);
let subtitle: string;
@ -51,24 +48,7 @@
{time}
</label>
<label>
<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}
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} />
</div>
<div class="subtitle">{subtitle}</div>

View file

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

View file

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

View file

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

View file

@ -35,11 +35,20 @@ export async function getGraphData(
return pb.BackendProto.GraphsOut.decode(bytes);
}
// amount of data to fetch from backend
export enum RevlogRange {
Year = 1,
All = 2,
}
// period a graph should cover
export enum GraphRange {
Month = 0,
ThreeMonths = 1,
Year = 2,
AllTime = 3,
}
export interface GraphsContext {
cards: pb.BackendProto.Card[];
revlog: pb.BackendProto.RevlogEntry[];
@ -77,3 +86,26 @@ export function setDataAvailable(
.duration(600)
.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 { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds, setDataAvailable } from "./graphs";
import {
GraphBounds,
setDataAvailable,
GraphRange,
millisecondCutoffForRange,
} from "./graphs";
import { area, curveBasis } from "d3-shape";
import { I18n } from "../i18n";
@ -25,21 +30,21 @@ interface Hour {
correctCount: number;
}
export interface GraphData {
hours: Hour[];
}
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) => {
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[]) {
if (review.reviewKind == ReviewKind.EARLY_REVIEW) {
continue;
}
if (cutoff && (review.id as number) < cutoff) {
continue;
}
const hour = Math.floor(
(((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(
svgElem: SVGElement,
bounds: GraphBounds,
sourceData: GraphData,
i18n: I18n
origData: pb.BackendProto.GraphsOut,
i18n: I18n,
range: GraphRange
): void {
const data = sourceData.hours;
const data = gatherData(origData, range);
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 { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip";
import { GraphBounds, setDataAvailable } from "./graphs";
import { GraphBounds, setDataAvailable, GraphRange } from "./graphs";
import { area, curveBasis } from "d3-shape";
import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
import { timeSpan, dayLabel } from "../time";
@ -38,13 +38,6 @@ export interface GraphData {
reviewTime: Map<number, Reviews>;
}
export enum ReviewRange {
Month = 0,
ThreeMonths = 1,
Year = 2,
AllTime = 3,
}
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
type BinType = Bin<Map<number, Reviews[]>, number>;
@ -112,7 +105,7 @@ export function renderReviews(
svgElem: SVGElement,
bounds: GraphBounds,
sourceData: GraphData,
range: ReviewRange,
range: GraphRange,
showTime: boolean,
i18n: I18n
): void {
@ -123,16 +116,16 @@ export function renderReviews(
let xMin = 0;
// cap max to selected range
switch (range) {
case ReviewRange.Month:
case GraphRange.Month:
xMin = -31;
break;
case ReviewRange.ThreeMonths:
case GraphRange.ThreeMonths:
xMin = -90;
break;
case ReviewRange.Year:
case GraphRange.Year:
xMin = -365;
break;
case ReviewRange.AllTime:
case GraphRange.AllTime:
xMin = min(sourceData.reviewCount.keys())!;
break;
}