mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
hour graph
This commit is contained in:
parent
e08b607ab4
commit
cb7fb6146c
5 changed files with 159 additions and 8 deletions
|
@ -10,7 +10,7 @@ module.exports = {
|
|||
"prefer-const": "warn",
|
||||
"@typescript-eslint/ban-ts-ignore": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
"warn",
|
||||
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
||||
],
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import AxisLabels from "./AxisLabels.svelte";
|
||||
import { gatherData, GraphData, renderButtons } from "./buttons";
|
||||
import pb from "../backend/proto";
|
||||
import HistogramGraph from "./HistogramGraph.svelte";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
import TodayStats from "./TodayStats.svelte";
|
||||
import ButtonsGraph from "./ButtonsGraph.svelte";
|
||||
import CardCounts from "./CardCounts.svelte";
|
||||
import HourGraph from "./HourGraph.svelte";
|
||||
|
||||
let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
|
@ -117,3 +118,4 @@
|
|||
<IntervalsGraph {sourceData} />
|
||||
<EaseGraph {sourceData} />
|
||||
<ButtonsGraph {sourceData} />
|
||||
<HourGraph {sourceData} />
|
||||
|
|
32
ts/src/stats/HourGraph.svelte
Normal file
32
ts/src/stats/HourGraph.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script lang="typescript">
|
||||
import { defaultGraphBounds } from "./graphs";
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import AxisLabels from "./AxisLabels.svelte";
|
||||
import { gatherData, GraphData, renderHours } from "./hours";
|
||||
import pb from "../backend/proto";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
|
||||
const bounds = defaultGraphBounds();
|
||||
const xText = "";
|
||||
const yText = "Times pressed";
|
||||
|
||||
let svg = null as HTMLElement | SVGElement | null;
|
||||
|
||||
$: if (sourceData) {
|
||||
console.log("gathering data");
|
||||
renderHours(svg as SVGElement, bounds, gatherData(sourceData));
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="graph">
|
||||
<h1>Hours</h1>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<path class="area" />
|
||||
<g class="bars" />
|
||||
<g class="hoverzone" />
|
||||
<AxisTicks {bounds} />
|
||||
<AxisLabels {bounds} {xText} {yText} />
|
||||
</svg>
|
||||
</div>
|
|
@ -1,7 +1,20 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import pb from "../backend/proto";
|
||||
import { interpolateBlues } from "d3-scale-chromatic";
|
||||
import "d3-transition";
|
||||
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 } from "./graphs";
|
||||
import { area, curveBasis } from "d3-shape";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
||||
|
@ -18,19 +31,18 @@ export interface GraphData {
|
|||
const ReviewKind = pb.BackendProto.RevlogEntry.ReviewKind;
|
||||
|
||||
export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
||||
const hours = Array(24).map((n: number) => {
|
||||
return { hour: n, totalCount: 0, correctCount: 0 } as Hour;
|
||||
const hours = [...Array(24)].map((_n, idx: number) => {
|
||||
return { hour: idx, totalCount: 0, correctCount: 0 } as Hour;
|
||||
});
|
||||
|
||||
// fixme: relative to midnight, not rollover
|
||||
|
||||
for (const review of data.revlog as pb.BackendProto.RevlogEntry[]) {
|
||||
if (review.reviewKind == ReviewKind.EARLY_REVIEW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hour =
|
||||
(((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24;
|
||||
const hour = Math.floor(
|
||||
(((review.id as number) / 1000 + data.localOffsetSecs) / 3600) % 24
|
||||
);
|
||||
hours[hour].totalCount += 1;
|
||||
if (review.buttonChosen != 1) {
|
||||
hours[hour].correctCount += 1;
|
||||
|
@ -39,3 +51,109 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
|
|||
|
||||
return { hours };
|
||||
}
|
||||
|
||||
function tooltipText(d: Hour): string {
|
||||
return JSON.stringify(d);
|
||||
}
|
||||
|
||||
export function renderHours(
|
||||
svgElem: SVGElement,
|
||||
bounds: GraphBounds,
|
||||
sourceData: GraphData
|
||||
): void {
|
||||
const data = sourceData.hours;
|
||||
|
||||
console.log(data);
|
||||
|
||||
const yMax = Math.max(...data.map((d) => d.totalCount));
|
||||
|
||||
const svg = select(svgElem);
|
||||
const trans = svg.transition().duration(600) as any;
|
||||
|
||||
const x = scaleBand()
|
||||
.domain(data.map((d) => d.hour.toString()))
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
||||
.paddingInner(0.1);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
.call(axisBottom(x).tickSizeOuter(0));
|
||||
|
||||
const colour = scaleSequential(interpolateBlues).domain([0, yMax]);
|
||||
|
||||
// y scale
|
||||
|
||||
const y = scaleLinear()
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax]);
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 80)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
|
||||
const yArea = y.copy().domain([0, 1]);
|
||||
|
||||
// x bars
|
||||
|
||||
const updateBar = (sel: any): any => {
|
||||
return sel
|
||||
.attr("width", x.bandwidth())
|
||||
.transition(trans)
|
||||
.attr("x", (d: Hour) => x(d.hour.toString())!)
|
||||
.attr("y", (d: Hour) => y(d.totalCount)!)
|
||||
.attr("height", (d: Hour) => y(0) - y(d.totalCount))
|
||||
.attr("fill", (d: Hour) => colour(d.totalCount!));
|
||||
};
|
||||
|
||||
svg.select("g.bars")
|
||||
.selectAll("rect")
|
||||
.data(data)
|
||||
.join(
|
||||
(enter) =>
|
||||
enter
|
||||
.append("rect")
|
||||
.attr("rx", 1)
|
||||
.attr("x", (d: Hour) => x(d.hour.toString())!)
|
||||
.attr("y", y(0))
|
||||
.attr("height", 0)
|
||||
.call(updateBar),
|
||||
(update) => update.call(updateBar),
|
||||
(remove) =>
|
||||
remove.call((remove) =>
|
||||
remove.transition(trans).attr("height", 0).attr("y", y(0))
|
||||
)
|
||||
);
|
||||
|
||||
svg.select("path.area")
|
||||
.datum(data)
|
||||
.attr(
|
||||
"d",
|
||||
area<Hour>()
|
||||
.curve(curveBasis)
|
||||
.x((d: Hour) => {
|
||||
return x(d.hour.toString())! + x.bandwidth() / 2;
|
||||
})
|
||||
.y0(bounds.height - bounds.marginBottom)
|
||||
.y1((d: Hour) => {
|
||||
const correctRatio = d.correctCount! / d.totalCount!;
|
||||
return yArea(isNaN(correctRatio) ? 0 : correctRatio);
|
||||
})
|
||||
);
|
||||
|
||||
// hover/tooltip
|
||||
svg.select("g.hoverzone")
|
||||
.selectAll("rect")
|
||||
.data(data)
|
||||
.join("rect")
|
||||
.attr("x", (d: Hour) => x(d.hour.toString())!)
|
||||
.attr("y", () => y(yMax)!)
|
||||
.attr("width", x.bandwidth())
|
||||
.attr("height", () => y(0) - y(yMax!))
|
||||
.on("mousemove", function (this: any, d: Hour) {
|
||||
const [x, y] = mouse(document.body);
|
||||
showTooltip(tooltipText(d), x, y);
|
||||
})
|
||||
.on("mouseout", hideTooltip);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue