From cb7fb6146c05c3cf66b888fb88dc38cccef2079f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 26 Jun 2020 14:05:58 +1000 Subject: [PATCH] hour graph --- ts/.eslintrc.js | 2 +- ts/src/stats/ButtonsGraph.svelte | 1 - ts/src/stats/GraphsPage.svelte | 2 + ts/src/stats/HourGraph.svelte | 32 ++++++++ ts/src/stats/hours.ts | 130 +++++++++++++++++++++++++++++-- 5 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 ts/src/stats/HourGraph.svelte diff --git a/ts/.eslintrc.js b/ts/.eslintrc.js index 871397b23..db1abdbe2 100644 --- a/ts/.eslintrc.js +++ b/ts/.eslintrc.js @@ -10,7 +10,7 @@ module.exports = { "prefer-const": "warn", "@typescript-eslint/ban-ts-ignore": "warn", "@typescript-eslint/no-unused-vars": [ - "error", + "warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], }, diff --git a/ts/src/stats/ButtonsGraph.svelte b/ts/src/stats/ButtonsGraph.svelte index 87704672c..cd527c425 100644 --- a/ts/src/stats/ButtonsGraph.svelte +++ b/ts/src/stats/ButtonsGraph.svelte @@ -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; diff --git a/ts/src/stats/GraphsPage.svelte b/ts/src/stats/GraphsPage.svelte index 20474fabd..f5a0324a1 100644 --- a/ts/src/stats/GraphsPage.svelte +++ b/ts/src/stats/GraphsPage.svelte @@ -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 @@ + diff --git a/ts/src/stats/HourGraph.svelte b/ts/src/stats/HourGraph.svelte new file mode 100644 index 000000000..83de82174 --- /dev/null +++ b/ts/src/stats/HourGraph.svelte @@ -0,0 +1,32 @@ + + +
+

Hours

+ + + + + + + + +
diff --git a/ts/src/stats/hours.ts b/ts/src/stats/hours.ts index b6ed33163..22bc1aaed 100644 --- a/ts/src/stats/hours.ts +++ b/ts/src/stats/hours.ts @@ -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(".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(".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() + .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); +}