mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
review graph and tooltip improvements
This commit is contained in:
parent
894e824460
commit
67bb92d2f4
7 changed files with 99 additions and 44 deletions
|
@ -7,7 +7,7 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { assertUnreachable } from "../typing";
|
import { assertUnreachable } from "../typing";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { getGraphData, GraphRange } from "./graphs";
|
import { getGraphData, RevlogRange } from "./graphs";
|
||||||
import IntervalsGraph from "./IntervalsGraph.svelte";
|
import IntervalsGraph from "./IntervalsGraph.svelte";
|
||||||
import EaseGraph from "./EaseGraph.svelte";
|
import EaseGraph from "./EaseGraph.svelte";
|
||||||
import AddedGraph from "./AddedGraph.svelte";
|
import AddedGraph from "./AddedGraph.svelte";
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let searchRange: SearchRange = SearchRange.Deck;
|
let searchRange: SearchRange = SearchRange.Deck;
|
||||||
let range: GraphRange = GraphRange.Month;
|
let revlogRange: RevlogRange = RevlogRange.Month;
|
||||||
let days: number = 31;
|
let days: number = 31;
|
||||||
let refreshing = false;
|
let refreshing = false;
|
||||||
|
|
||||||
|
@ -61,14 +61,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
switch (range as GraphRange) {
|
switch (revlogRange as RevlogRange) {
|
||||||
case GraphRange.Month:
|
case RevlogRange.Month:
|
||||||
days = 31;
|
days = 31;
|
||||||
break;
|
break;
|
||||||
case GraphRange.Year:
|
case RevlogRange.Year:
|
||||||
days = 365;
|
days = 365;
|
||||||
break;
|
break;
|
||||||
case GraphRange.All:
|
case RevlogRange.All:
|
||||||
days = 0;
|
days = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -108,25 +108,25 @@
|
||||||
<div class="range-box-inner">
|
<div class="range-box-inner">
|
||||||
Review history:
|
Review history:
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={GraphRange.Month} />
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.Month} />
|
||||||
Month
|
Month
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={GraphRange.Year} />
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
||||||
Year
|
Year
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={GraphRange.All} />
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
||||||
All
|
All
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="range-box-pad" />
|
<div class="range-box-pad" />
|
||||||
|
|
||||||
<ReviewsGraph {sourceData} />
|
|
||||||
<FutureDue {sourceData} />
|
|
||||||
<TodayStats {sourceData} />
|
<TodayStats {sourceData} />
|
||||||
<CardCounts {sourceData} />
|
<CardCounts {sourceData} />
|
||||||
|
<FutureDue {sourceData} />
|
||||||
|
<ReviewsGraph {sourceData} {revlogRange} />
|
||||||
<IntervalsGraph {sourceData} />
|
<IntervalsGraph {sourceData} />
|
||||||
<EaseGraph {sourceData} />
|
<EaseGraph {sourceData} />
|
||||||
<ButtonsGraph {sourceData} />
|
<ButtonsGraph {sourceData} />
|
||||||
|
|
|
@ -2,18 +2,32 @@
|
||||||
import { HistogramData, histogramGraph } from "./histogram-graph";
|
import { HistogramData, histogramGraph } from "./histogram-graph";
|
||||||
import AxisLabels from "./AxisLabels.svelte";
|
import AxisLabels from "./AxisLabels.svelte";
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds } from "./graphs";
|
import { defaultGraphBounds, RevlogRange } from "./graphs";
|
||||||
import { GraphData, gatherData, renderReviews, ReviewRange } from "./reviews";
|
import { GraphData, gatherData, renderReviews, ReviewRange } from "./reviews";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
export let revlogRange: RevlogRange = RevlogRange.Month;
|
||||||
|
|
||||||
let graphData: GraphData | null = null;
|
let graphData: GraphData | null = null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
let range = ReviewRange.Month;
|
let range: ReviewRange;
|
||||||
let showTime = false;
|
let showTime = false;
|
||||||
|
let tooltip = null as null | HTMLDivElement;
|
||||||
|
|
||||||
|
$: switch (revlogRange as RevlogRange) {
|
||||||
|
case RevlogRange.Month:
|
||||||
|
range = ReviewRange.Month;
|
||||||
|
break;
|
||||||
|
case RevlogRange.Year:
|
||||||
|
range = ReviewRange.Year;
|
||||||
|
break;
|
||||||
|
case RevlogRange.All:
|
||||||
|
range = ReviewRange.AllTime;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const xText = "";
|
const xText = "";
|
||||||
const yText = "Times pressed";
|
const yText = "Times pressed";
|
||||||
|
@ -24,7 +38,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (graphData) {
|
$: if (graphData) {
|
||||||
renderReviews(svg as SVGElement, bounds, graphData, range, showTime);
|
renderReviews(svg as SVGElement, bounds, graphData, range, showTime, tooltip);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -37,6 +51,7 @@
|
||||||
Time
|
Time
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{#if revlogRange >= RevlogRange.Year}
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={ReviewRange.Month} />
|
<input type="radio" bind:group={range} value={ReviewRange.Month} />
|
||||||
Month
|
Month
|
||||||
|
@ -49,10 +64,13 @@
|
||||||
<input type="radio" bind:group={range} value={ReviewRange.Year} />
|
<input type="radio" bind:group={range} value={ReviewRange.Year} />
|
||||||
Year
|
Year
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if revlogRange === RevlogRange.All}
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={ReviewRange.AllTime} />
|
<input type="radio" bind:group={range} value={ReviewRange.AllTime} />
|
||||||
All time
|
All time
|
||||||
</label>
|
</label>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
|
@ -65,4 +83,6 @@
|
||||||
<AxisLabels {bounds} {xText} {yText} />
|
<AxisLabels {bounds} {xText} {yText} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
|
<div bind:this={tooltip} class="tooltip-area" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export enum FutureDueRange {
|
||||||
|
|
||||||
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)
|
||||||
.map((c) => c.due - data.daysElapsed);
|
.map((c) => c.due - data.daysElapsed);
|
||||||
const dueCounts = rollup(
|
const dueCounts = rollup(
|
||||||
due,
|
due,
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 20px;
|
font-size: 15px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
|
@ -48,6 +48,7 @@
|
||||||
|
|
||||||
.range-box {
|
.range-box {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 4em;
|
height: 4em;
|
||||||
|
@ -85,6 +86,11 @@
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hoverzone rect:hover {
|
||||||
|
fill: grey;
|
||||||
|
opacity: 0.05;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
-webkit-transform: rotate(0deg);
|
-webkit-transform: rotate(0deg);
|
||||||
|
@ -108,3 +114,22 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
transition: opacity 1s;
|
transition: opacity 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-area {
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-area > * {
|
||||||
|
flex: 1 100%;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-outer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-outer div {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ export async function getGraphData(
|
||||||
return pb.BackendProto.GraphsOut.decode(bytes);
|
return pb.BackendProto.GraphsOut.decode(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GraphRange {
|
export enum RevlogRange {
|
||||||
Month = 1,
|
Month = 1,
|
||||||
Year = 2,
|
Year = 2,
|
||||||
All = 3,
|
All = 3,
|
||||||
|
|
|
@ -106,19 +106,14 @@ function cumulativeBinValue(bin: BinType, idx: number): number {
|
||||||
return sum(totalsForBin(bin).slice(0, idx + 1));
|
return sum(totalsForBin(bin).slice(0, idx + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
function tooltipText(d: BinType, cumulative: number): string {
|
|
||||||
return `bin: ${JSON.stringify(totalsForBin(d))}<br>cumulative: ${cumulative}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderReviews(
|
export function renderReviews(
|
||||||
svgElem: SVGElement,
|
svgElem: SVGElement,
|
||||||
bounds: GraphBounds,
|
bounds: GraphBounds,
|
||||||
sourceData: GraphData,
|
sourceData: GraphData,
|
||||||
range: ReviewRange,
|
range: ReviewRange,
|
||||||
showTime: boolean
|
showTime: boolean,
|
||||||
|
tooltipArea: HTMLDivElement
|
||||||
): void {
|
): void {
|
||||||
console.log(sourceData);
|
|
||||||
|
|
||||||
const xMax = 0;
|
const xMax = 0;
|
||||||
let xMin = 0;
|
let xMin = 0;
|
||||||
// cap max to selected range
|
// cap max to selected range
|
||||||
|
@ -137,7 +132,6 @@ export function renderReviews(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const desiredBars = Math.min(70, Math.abs(xMin!));
|
const desiredBars = Math.min(70, Math.abs(xMin!));
|
||||||
console.log(`xmin ${xMin}`);
|
|
||||||
|
|
||||||
const x = scaleLinear().domain([xMin!, xMax]);
|
const x = scaleLinear().domain([xMin!, xMax]);
|
||||||
const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount;
|
const sourceMap = showTime ? sourceData.reviewTime : sourceData.reviewCount;
|
||||||
|
@ -159,7 +153,6 @@ export function renderReviews(
|
||||||
// y scale
|
// y scale
|
||||||
|
|
||||||
const yMax = max(bins, (b: Bin<any, any>) => cumulativeBinValue(b, 4))!;
|
const yMax = max(bins, (b: Bin<any, any>) => cumulativeBinValue(b, 4))!;
|
||||||
console.log(`ymax ${yMax}`);
|
|
||||||
const y = scaleLinear()
|
const y = scaleLinear()
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
.domain([0, yMax]);
|
.domain([0, yMax]);
|
||||||
|
@ -196,6 +189,23 @@ export function renderReviews(
|
||||||
x.domain() as any
|
x.domain() as any
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function tooltipText(d: BinType, cumulative: number): string {
|
||||||
|
let buf = `<div>day ${d.x0}-${d.x1}</div>`;
|
||||||
|
const totals = totalsForBin(d);
|
||||||
|
const lines = [
|
||||||
|
[darkerGreens(1), `Mature: ${totals[0]}`],
|
||||||
|
[lighterGreens(1), `Young: ${totals[1]}`],
|
||||||
|
[blues(1), `New/learn: ${totals[2]}`],
|
||||||
|
[reds(1), `Relearn: ${totals[3]}`],
|
||||||
|
[oranges(1), `Early: ${totals[4]}`],
|
||||||
|
["grey", `Total: ${cumulative}`],
|
||||||
|
];
|
||||||
|
for (const [colour, text] of lines) {
|
||||||
|
buf += `<div><span style="color: ${colour}">■</span>${text}</div>`;
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
const updateBar = (sel: any, idx: number): any => {
|
const updateBar = (sel: any, idx: number): any => {
|
||||||
return sel
|
return sel
|
||||||
.attr("width", barWidth)
|
.attr("width", barWidth)
|
||||||
|
|
|
@ -12,8 +12,8 @@ function showTooltipInner(msg: string, x: number, y: number): void {
|
||||||
document.body.appendChild(tooltipDiv);
|
document.body.appendChild(tooltipDiv);
|
||||||
}
|
}
|
||||||
tooltipDiv.innerHTML = msg;
|
tooltipDiv.innerHTML = msg;
|
||||||
tooltipDiv.style.left = `${x - 50}px`;
|
tooltipDiv.style.right = `${document.body.clientWidth - x + 10}px`;
|
||||||
tooltipDiv.style.top = `${y - 50}px`;
|
tooltipDiv.style.top = `${y + 20}px`;
|
||||||
|
|
||||||
tooltipDiv.style.opacity = "1";
|
tooltipDiv.style.opacity = "1";
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue