mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 07:22:23 -04:00
merge in Henrik's TS/Svelte refactor with some changes
- The previous commits moved the majority of the remaining global css into components; move the remaining @emotion/css references into ticks.scss and the styling of the Graph.svelte. This is not as elegant as the emotion solution, but builds a whole lot faster, and most of our styling can be scoped to a component anyway. - Leave the .html files in ts/ for now. AnkiMobile uses them, and AnkiDroid likely will in the future too. In the long run we'll likely move to loading the JS into an existing page instead of loading a separate page, but at that point we can just exclude the .html file from copy_files_into_group() without affecting other clients. Closes #1074
This commit is contained in:
parent
95ccfc1ed3
commit
7d8f19e6e4
34 changed files with 467 additions and 402 deletions
|
@ -13,6 +13,7 @@ copy_files_into_group(
|
||||||
copy_files_into_group(
|
copy_files_into_group(
|
||||||
name = "congrats_page",
|
name = "congrats_page",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
"congrats.css",
|
||||||
"congrats.html",
|
"congrats.html",
|
||||||
"congrats.js",
|
"congrats.js",
|
||||||
],
|
],
|
||||||
|
|
|
@ -10,8 +10,8 @@ svelte(
|
||||||
)
|
)
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "bootstrap",
|
name = "index",
|
||||||
srcs = ["bootstrap.ts"],
|
srcs = ["index.ts"],
|
||||||
deps = [
|
deps = [
|
||||||
"CongratsPage",
|
"CongratsPage",
|
||||||
"lib",
|
"lib",
|
||||||
|
@ -38,17 +38,19 @@ esbuild(
|
||||||
"--global-name=anki",
|
"--global-name=anki",
|
||||||
"--inject:ts/protobuf-shim.js",
|
"--inject:ts/protobuf-shim.js",
|
||||||
],
|
],
|
||||||
entry_point = "bootstrap.ts",
|
entry_point = "index.ts",
|
||||||
external = [
|
external = [
|
||||||
"protobufjs/light",
|
"protobufjs/light",
|
||||||
],
|
],
|
||||||
|
output_css = True,
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"CongratsPage",
|
"CongratsPage",
|
||||||
"bootstrap",
|
"index",
|
||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
"//ts/lib:backend_proto",
|
"//ts/lib:backend_proto",
|
||||||
"//ts/lib:fluent_proto",
|
"//ts/lib:fluent_proto",
|
||||||
|
"//ts/sass:core_css",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import "../sass/core.css";
|
||||||
|
|
||||||
import { I18n } from "anki/i18n";
|
import { I18n } from "anki/i18n";
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
import { buildNextLearnMsg } from "./lib";
|
import { buildNextLearnMsg } from "./lib";
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" id="viewport" content="width=device-width" />
|
<meta name="viewport" id="viewport" content="width=device-width" />
|
||||||
<link href="../css/core.css" rel="stylesheet" />
|
<link href="congrats.css" rel="stylesheet" />
|
||||||
|
<script src="../js/vendor/protobuf.min.js"></script>
|
||||||
|
<script src="congrats.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main"></div>
|
<div id="main"></div>
|
||||||
</body>
|
|
||||||
<script src="../js/vendor/protobuf.min.js"></script>
|
|
||||||
<script src="congrats.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
anki.congrats(document.getElementById("main"));
|
anki.congrats(document.getElementById("main"));
|
||||||
</script>
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { setupI18n } from "anki/i18n";
|
|
||||||
import CongratsPage from "./CongratsPage.svelte";
|
|
||||||
import { getCongratsInfo } from "./lib";
|
import { getCongratsInfo } from "./lib";
|
||||||
|
import { setupI18n } from "anki/i18n";
|
||||||
import { checkNightMode } from "anki/nightmode";
|
import { checkNightMode } from "anki/nightmode";
|
||||||
|
|
||||||
|
import CongratsPage from "./CongratsPage.svelte";
|
||||||
|
|
||||||
export async function congrats(target: HTMLDivElement): Promise<void> {
|
export async function congrats(target: HTMLDivElement): Promise<void> {
|
||||||
checkNightMode();
|
checkNightMode();
|
||||||
const i18n = await setupI18n();
|
const i18n = await setupI18n();
|
|
@ -1,15 +1,19 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { RevlogRange, GraphRange } from "./graph-helpers";
|
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
import type { HistogramData } from "./histogram-graph";
|
|
||||||
import { gatherData, buildHistogram } from "./added";
|
|
||||||
import type { GraphData } from "./added";
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
|
import { RevlogRange, GraphRange } from "./graph-helpers";
|
||||||
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
|
import type { HistogramData } from "./histogram-graph";
|
||||||
|
import { gatherData, buildHistogram } from "./added";
|
||||||
|
import type { GraphData } from "./added";
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
@ -42,16 +46,12 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-added">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} {i18n} />
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
|
||||||
<TableData {i18n} {tableData} />
|
<TableData {i18n} {tableData} />
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { GraphBounds } from "./graph-helpers";
|
import type { GraphBounds } from "./graph-helpers";
|
||||||
|
|
||||||
export let bounds: GraphBounds;
|
export let bounds: GraphBounds;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<g
|
<style lang="scss">
|
||||||
class="x-ticks no-domain-line"
|
g :global(.domain) {
|
||||||
transform={`translate(0,${bounds.height - bounds.marginBottom})`} />
|
opacity: 0.05;
|
||||||
<g class="y-ticks no-domain-line" transform={`translate(${bounds.marginLeft}, 0)`} />
|
}
|
||||||
<g
|
</style>
|
||||||
class="y2-ticks no-domain-line"
|
|
||||||
transform={`translate(${bounds.width - bounds.marginRight}, 0)`} />
|
<g class="x-ticks" transform={`translate(0, ${bounds.height - bounds.marginBottom})`} />
|
||||||
|
<g class="y-ticks" transform={`translate(${bounds.marginLeft}, 0)`} />
|
||||||
|
<g class="y2-ticks" transform={`translate(${bounds.width - bounds.marginRight}, 0)`} />
|
||||||
|
|
|
@ -6,12 +6,8 @@ load("//ts:esbuild.bzl", "esbuild")
|
||||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||||
|
|
||||||
sass_binary(
|
sass_binary(
|
||||||
name = "graphs_shared",
|
name = "ticks",
|
||||||
src = "graphs_shared.scss",
|
src = "ticks.scss",
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
deps = [
|
|
||||||
"//ts/sass:core_lib",
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
svelte_files = glob(["*.svelte"])
|
svelte_files = glob(["*.svelte"])
|
||||||
|
@ -24,8 +20,8 @@ compile_svelte(
|
||||||
)
|
)
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "bootstrap",
|
name = "index",
|
||||||
srcs = ["bootstrap.ts"],
|
srcs = ["index.ts"],
|
||||||
deps = [
|
deps = [
|
||||||
"GraphsPage",
|
"GraphsPage",
|
||||||
"lib",
|
"lib",
|
||||||
|
@ -39,7 +35,7 @@ ts_library(
|
||||||
name = "lib",
|
name = "lib",
|
||||||
srcs = glob(
|
srcs = glob(
|
||||||
["*.ts"],
|
["*.ts"],
|
||||||
exclude = ["bootstrap.ts"],
|
exclude = ["index.ts"],
|
||||||
),
|
),
|
||||||
deps = [
|
deps = [
|
||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
|
@ -62,7 +58,7 @@ esbuild(
|
||||||
"--global-name=anki",
|
"--global-name=anki",
|
||||||
"--inject:ts/protobuf-shim.js",
|
"--inject:ts/protobuf-shim.js",
|
||||||
],
|
],
|
||||||
entry_point = "bootstrap.ts",
|
entry_point = "index.ts",
|
||||||
external = [
|
external = [
|
||||||
"protobufjs/light",
|
"protobufjs/light",
|
||||||
],
|
],
|
||||||
|
@ -72,8 +68,9 @@ esbuild(
|
||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
"//ts/lib:backend_proto",
|
"//ts/lib:backend_proto",
|
||||||
"//ts/lib:fluent_proto",
|
"//ts/lib:fluent_proto",
|
||||||
"bootstrap",
|
":index",
|
||||||
"graphs_shared",
|
":ticks",
|
||||||
|
"//ts/sass:core_css",
|
||||||
] + svelte_names,
|
] + svelte_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { defaultGraphBounds, GraphRange, RevlogRange } from "./graph-helpers";
|
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
|
||||||
import { renderButtons } from "./buttons";
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import HoverColumns from "./HoverColumns.svelte";
|
import HoverColumns from "./HoverColumns.svelte";
|
||||||
|
import { renderButtons } from "./buttons";
|
||||||
|
import { defaultGraphBounds, GraphRange, RevlogRange } from "./graph-helpers";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -26,14 +29,10 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ANSWER_BUTTONS_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_ANSWER_BUTTONS_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-buttons">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
<g class="bars" />
|
<g class="bars" />
|
||||||
|
@ -41,4 +40,4 @@
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
<NoDataOverlay {bounds} {i18n} />
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -1,14 +1,18 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
|
|
||||||
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
||||||
import type { SearchEventMap } from "./graph-helpers";
|
import type { SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, renderCalendar } from "./calendar";
|
import { gatherData, renderCalendar } from "./calendar";
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
import type { GraphData } from "./calendar";
|
import type { GraphData } from "./calendar";
|
||||||
import type pb from "anki/backend_proto";
|
|
||||||
import type { I18n } from "anki/i18n";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let preferences: PreferenceStore | null = null;
|
export let preferences: PreferenceStore | null = null;
|
||||||
|
@ -65,10 +69,8 @@
|
||||||
const title = i18n.tr(i18n.TR.STATISTICS_CALENDAR_TITLE);
|
const title = i18n.tr(i18n.TR.STATISTICS_CALENDAR_TITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-calendar">
|
<Graph {title}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<span>
|
<span>
|
||||||
<button on:click={() => targetYear--} disabled={minYear >= targetYear}>
|
<button on:click={() => targetYear--} disabled={minYear >= targetYear}>
|
||||||
◄
|
◄
|
||||||
|
@ -80,7 +82,7 @@
|
||||||
►
|
►
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
<g class="weekdays" />
|
<g class="weekdays" />
|
||||||
|
@ -88,4 +90,4 @@
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
<NoDataOverlay {bounds} {i18n} />
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
|
||||||
import { defaultGraphBounds } from "./graph-helpers";
|
import { defaultGraphBounds } from "./graph-helpers";
|
||||||
import type { SearchEventMap } from "./graph-helpers";
|
import type { SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, renderCards } from "./card-counts";
|
import { gatherData, renderCards } from "./card-counts";
|
||||||
import type { GraphData, TableDatum } from "./card-counts";
|
import type { GraphData, TableDatum } from "./card-counts";
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
import type pb from "anki/backend_proto";
|
|
||||||
import type { I18n } from "anki/i18n";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -68,15 +72,13 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="graph" id="graph-card-counts">
|
<Graph title={graphData.title}>
|
||||||
<h1>{graphData.title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" bind:checked={$cardCountsSeparateInactive} />
|
<input type="checkbox" bind:checked={$cardCountsSeparateInactive} />
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<div class="counts-outer">
|
<div class="counts-outer">
|
||||||
<svg
|
<svg
|
||||||
|
@ -113,4 +115,4 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import TableData from "./TableData.svelte";
|
||||||
|
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import { gatherData, prepareData } from "./ease";
|
import { gatherData, prepareData } from "./ease";
|
||||||
import type pb from "anki/backend_proto";
|
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
|
||||||
import type { I18n } from "anki/i18n";
|
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import TableData from "./TableData.svelte";
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
@ -32,12 +35,8 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-ease">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} {i18n} />
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
|
||||||
<TableData {i18n} {tableData} />
|
<TableData {i18n} {tableData} />
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
|
import TableData from "./TableData.svelte";
|
||||||
|
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import { GraphRange, RevlogRange } from "./graph-helpers";
|
import { GraphRange, RevlogRange } from "./graph-helpers";
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, buildHistogram } from "./future-due";
|
import { gatherData, buildHistogram } from "./future-due";
|
||||||
import type { GraphData } from "./future-due";
|
import type { GraphData } from "./future-due";
|
||||||
import type pb from "anki/backend_proto";
|
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
|
||||||
import TableData from "./TableData.svelte";
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
@ -44,12 +48,8 @@
|
||||||
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-future-due">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
{#if graphData && graphData.haveBacklog}
|
{#if graphData && graphData.haveBacklog}
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" bind:checked={$futureDueShowBacklog} />
|
<input type="checkbox" bind:checked={$futureDueShowBacklog} />
|
||||||
|
@ -58,9 +58,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} {i18n} />
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
|
||||||
<TableData {i18n} {tableData} />
|
<TableData {i18n} {tableData} />
|
||||||
</div>
|
</Graph>
|
||||||
|
|
44
ts/graphs/Graph.svelte
Normal file
44
ts/graphs/Graph.svelte
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<script context="module">
|
||||||
|
// custom tick styling
|
||||||
|
import "./ticks.css";
|
||||||
|
|
||||||
|
// see graph-style.ts for constants referencing global styles
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="typescript">
|
||||||
|
export let title: string;
|
||||||
|
export let subtitle: string | null = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.graph {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 60em;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
margin-top: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
.graph :global(.graph-element-clickable) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="graph">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
|
||||||
|
{#if subtitle}
|
||||||
|
<div class="subtitle">{subtitle}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</div>
|
|
@ -1,5 +1,6 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import "./graphs_shared.css";
|
import "../sass/core.css";
|
||||||
|
|
||||||
import type { SvelteComponent } from "svelte/internal";
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
@ -52,6 +53,19 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.base {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-focus-outline:focus {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="base">
|
||||||
{#if controller}
|
{#if controller}
|
||||||
<svelte:component
|
<svelte:component
|
||||||
this={controller}
|
this={controller}
|
||||||
|
@ -76,3 +90,4 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { I18n } from "anki/i18n";
|
||||||
import { histogramGraph } from "./histogram-graph";
|
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds } from "./graph-helpers";
|
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
||||||
import HoverColumns from "./HoverColumns.svelte";
|
import HoverColumns from "./HoverColumns.svelte";
|
||||||
|
|
||||||
import type { I18n } from "anki/i18n";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
|
import { histogramGraph } from "./histogram-graph";
|
||||||
|
import { defaultGraphBounds } from "./graph-helpers";
|
||||||
|
|
||||||
export let data: HistogramData | null = null;
|
export let data: HistogramData | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graph-helpers";
|
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
|
||||||
import { renderHours } from "./hours";
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
||||||
import HoverColumns from "./HoverColumns.svelte";
|
import HoverColumns from "./HoverColumns.svelte";
|
||||||
|
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graph-helpers";
|
||||||
|
import { renderHours } from "./hours";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -26,14 +29,10 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_HOURS_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_HOURS_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-hour">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
<g class="bars" />
|
<g class="bars" />
|
||||||
|
@ -42,4 +41,4 @@
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
<NoDataOverlay {bounds} {i18n} />
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</Graph>
|
||||||
|
|
19
ts/graphs/InputBox.svelte
Normal file
19
ts/graphs/InputBox.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 480px) and (orientation: portrait) {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :global(*) {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
|
@ -1,6 +1,14 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { timeSpan, MONTH } from "anki/time";
|
import { timeSpan, MONTH } from "anki/time";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
import type pb from "anki/backend_proto";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
|
import TableData from "./TableData.svelte";
|
||||||
|
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import {
|
import {
|
||||||
gatherIntervalData,
|
gatherIntervalData,
|
||||||
|
@ -8,11 +16,7 @@
|
||||||
prepareIntervalData,
|
prepareIntervalData,
|
||||||
} from "./intervals";
|
} from "./intervals";
|
||||||
import type { IntervalGraphData } from "./intervals";
|
import type { IntervalGraphData } from "./intervals";
|
||||||
import type pb from "anki/backend_proto";
|
|
||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import TableData from "./TableData.svelte";
|
|
||||||
import { createEventDispatcher } from "svelte";
|
|
||||||
import type { PreferenceStore } from "./preferences";
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
|
@ -42,17 +46,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE);
|
const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE);
|
||||||
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
||||||
const month = timeSpan(i18n, 1 * MONTH);
|
const month = timeSpan(i18n, 1 * MONTH);
|
||||||
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
|
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph intervals" id="graph-intervals">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={IntervalRange.Month} />
|
<input type="radio" bind:group={range} value={IntervalRange.Month} />
|
||||||
{month}
|
{month}
|
||||||
|
@ -69,9 +69,9 @@
|
||||||
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
||||||
{all}
|
{all}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} {i18n} />
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
|
||||||
<TableData {i18n} {tableData} />
|
<TableData {i18n} {tableData} />
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -6,6 +6,19 @@
|
||||||
const noData = i18n.tr(i18n.TR.STATISTICS_NO_DATA);
|
const noData = i18n.tr(i18n.TR.STATISTICS_NO_DATA);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.no-data {
|
||||||
|
rect {
|
||||||
|
fill: var(--window-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<g class="no-data">
|
<g class="no-data">
|
||||||
<rect x="0" y="0" width={bounds.width} height={bounds.height} />
|
<rect x="0" y="0" width={bounds.width} height={bounds.height} />
|
||||||
<text x="{bounds.width / 2}," y={bounds.height / 2}>{noData}</text>
|
<text x="{bounds.width / 2}," y={bounds.height / 2}>{noData}</text>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
|
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
import { RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
import { RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
||||||
|
|
||||||
|
@ -81,10 +83,55 @@
|
||||||
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_HISTORY);
|
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_HISTORY);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.range-box {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-fg);
|
||||||
|
background: var(--window-bg);
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 2em;
|
||||||
|
animation: spin;
|
||||||
|
animation-duration: 1s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-box-pad {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="range-box">
|
<div class="range-box">
|
||||||
<div class="spin" class:active>◐</div>
|
<div class="spin" class:active>◐</div>
|
||||||
|
|
||||||
<div class="range-box-inner">
|
<InputBox>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={searchRange} value={SearchRange.Deck} />
|
<input type="radio" bind:group={searchRange} value={SearchRange.Deck} />
|
||||||
{deck}
|
{deck}
|
||||||
|
@ -105,9 +152,9 @@
|
||||||
searchRange = SearchRange.Custom;
|
searchRange = SearchRange.Custom;
|
||||||
}}
|
}}
|
||||||
placeholder={searchLabel} />
|
placeholder={searchLabel} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<div class="range-box-inner">
|
<InputBox>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
||||||
{year}
|
{year}
|
||||||
|
@ -116,7 +163,7 @@
|
||||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
||||||
{all}
|
{all}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</InputBox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="range-box-pad" />
|
<div class="range-box-pad" />
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
|
||||||
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graph-helpers";
|
|
||||||
import type { TableDatum } from "./graph-helpers";
|
|
||||||
import { gatherData, renderReviews } from "./reviews";
|
|
||||||
import type { GraphData } from "./reviews";
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
import InputBox from "./InputBox.svelte";
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import HoverColumns from "./HoverColumns.svelte";
|
import HoverColumns from "./HoverColumns.svelte";
|
||||||
|
|
||||||
|
import { defaultGraphBounds, RevlogRange, GraphRange } from "./graph-helpers";
|
||||||
|
import type { TableDatum } from "./graph-helpers";
|
||||||
|
import { gatherData, renderReviews } from "./reviews";
|
||||||
|
import type { GraphData } from "./reviews";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let revlogRange: RevlogRange;
|
export let revlogRange: RevlogRange;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -50,16 +54,12 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="graph" id="graph-reviews">
|
<Graph {title} {subtitle}>
|
||||||
<h1>{title}</h1>
|
<InputBox>
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
|
||||||
<label> <input type="checkbox" bind:checked={showTime} /> {time} </label>
|
<label> <input type="checkbox" bind:checked={showTime} /> {time} </label>
|
||||||
|
|
||||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||||
</div>
|
</InputBox>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
{#each [4, 3, 2, 1, 0] as i}
|
{#each [4, 3, 2, 1, 0] as i}
|
||||||
|
@ -72,4 +72,4 @@
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<TableData {i18n} {tableData} />
|
<TableData {i18n} {tableData} />
|
||||||
</div>
|
</Graph>
|
||||||
|
|
|
@ -6,7 +6,22 @@
|
||||||
export let tableData: TableDatum[];
|
export let tableData: TableDatum[];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="centered">
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-end {
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-start {
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
<table dir={i18n.direction()}>
|
<table dir={i18n.direction()}>
|
||||||
{#each tableData as { label, value }}
|
{#each tableData as { label, value }}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { gatherData } from "./today";
|
|
||||||
import type { TodayData } from "./today";
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
|
import Graph from "./Graph.svelte";
|
||||||
|
|
||||||
|
import type { TodayData } from "./today";
|
||||||
|
import { gatherData } from "./today";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
|
||||||
|
@ -13,14 +16,18 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if todayData}
|
<style lang="scss">
|
||||||
<div class="graph" id="graph-today-stats">
|
.legend {
|
||||||
<h1>{todayData.title}</h1>
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="legend-outer">
|
{#if todayData}
|
||||||
|
<Graph title={todayData.title}>
|
||||||
|
<div class="legend">
|
||||||
{#each todayData.lines as line}
|
{#each todayData.lines as line}
|
||||||
<div>{line}</div>
|
<div>{line}</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Graph>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
import {
|
import {
|
||||||
interpolateRdYlGn,
|
interpolateRdYlGn,
|
||||||
select,
|
select,
|
||||||
|
@ -25,7 +26,6 @@ import {
|
||||||
GraphRange,
|
GraphRange,
|
||||||
millisecondCutoffForRange,
|
millisecondCutoffForRange,
|
||||||
} from "./graph-helpers";
|
} from "./graph-helpers";
|
||||||
import type { I18n } from "anki/i18n";
|
|
||||||
|
|
||||||
type ButtonCounts = [number, number, number, number];
|
type ButtonCounts = [number, number, number, number];
|
||||||
|
|
||||||
|
@ -153,9 +153,8 @@ export function renderButtons(
|
||||||
const xGroup = scaleBand()
|
const xGroup = scaleBand()
|
||||||
.domain(["learning", "young", "mature"])
|
.domain(["learning", "young", "mature"])
|
||||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
svg.select<SVGGElement>(".x-ticks")
|
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisBottom(xGroup)
|
axisBottom(xGroup)
|
||||||
.tickFormat(((d: GroupKind) => {
|
.tickFormat(((d: GroupKind) => {
|
||||||
let kind: string;
|
let kind: string;
|
||||||
|
@ -174,6 +173,7 @@ export function renderButtons(
|
||||||
return `${kind} \u200e(${totalCorrect(d).percent}%)`;
|
return `${kind} \u200e(${totalCorrect(d).percent}%)`;
|
||||||
}) as any)
|
}) as any)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const xButton = scaleBand()
|
const xButton = scaleBand()
|
||||||
|
@ -189,12 +189,12 @@ export function renderButtons(
|
||||||
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]);
|
||||||
svg.select<SVGGElement>(".y-ticks")
|
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// x bars
|
// x bars
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
/* eslint
|
/* eslint
|
||||||
@typescript-eslint/no-non-null-assertion: "off",
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
import {
|
import {
|
||||||
interpolateBlues,
|
interpolateBlues,
|
||||||
|
@ -21,6 +21,7 @@ import {
|
||||||
timeSaturday,
|
timeSaturday,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
import type { CountableTimeInterval } from "d3";
|
import type { CountableTimeInterval } from "d3";
|
||||||
|
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import {
|
import {
|
||||||
GraphBounds,
|
GraphBounds,
|
||||||
|
@ -28,7 +29,7 @@ import {
|
||||||
RevlogRange,
|
RevlogRange,
|
||||||
SearchDispatch,
|
SearchDispatch,
|
||||||
} from "./graph-helpers";
|
} from "./graph-helpers";
|
||||||
import type { I18n } from "anki/i18n";
|
import { clickableClass } from "./graph-styles";
|
||||||
|
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
// indexed by day, where day is relative to today
|
// indexed by day, where day is relative to today
|
||||||
|
@ -203,24 +204,22 @@ export function renderCalendar(
|
||||||
.data(data)
|
.data(data)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("fill", emptyColour)
|
.attr("fill", emptyColour)
|
||||||
.attr("width", (d) => x(d.weekNumber + 1)! - x(d.weekNumber)! - 2)
|
.attr("width", (d: DayDatum) => x(d.weekNumber + 1)! - x(d.weekNumber)! - 2)
|
||||||
.attr("height", height - 2)
|
.attr("height", height - 2)
|
||||||
.attr("x", (d) => x(d.weekNumber + 1)!)
|
.attr("x", (d: DayDatum) => x(d.weekNumber + 1)!)
|
||||||
.attr("y", (d) => bounds.marginTop + d.weekDay * height)
|
.attr("y", (d: DayDatum) => bounds.marginTop + d.weekDay * height)
|
||||||
.on("mousemove", (event: MouseEvent, d: DayDatum) => {
|
.on("mousemove", (event: MouseEvent, d: DayDatum) => {
|
||||||
const [x, y] = pointer(event, document.body);
|
const [x, y] = pointer(event, document.body);
|
||||||
showTooltip(tooltipText(d), x, y);
|
showTooltip(tooltipText(d), x, y);
|
||||||
})
|
})
|
||||||
.on("mouseout", hideTooltip)
|
.on("mouseout", hideTooltip)
|
||||||
.attr("class", (d: any): string => {
|
.attr("class", (d: DayDatum): string => (d.count > 0 ? clickableClass : ""))
|
||||||
return d.count > 0 ? "clickable" : "";
|
.on("click", function (_event: MouseEvent, d: DayDatum) {
|
||||||
})
|
|
||||||
.on("click", function (_event: MouseEvent, d: any) {
|
|
||||||
if (d.count > 0) {
|
if (d.count > 0) {
|
||||||
dispatch("search", { query: `"prop:rated=${d.day}"` });
|
dispatch("search", { query: `"prop:rated=${d.day}"` });
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.transition()
|
.transition()
|
||||||
.duration(800)
|
.duration(800)
|
||||||
.attr("fill", (d) => (d.count === 0 ? emptyColour : blues(d.count)!));
|
.attr("fill", (d: DayDatum) => (d.count === 0 ? emptyColour : blues(d.count)!));
|
||||||
}
|
}
|
||||||
|
|
13
ts/graphs/graph-styles.ts
Normal file
13
ts/graphs/graph-styles.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
// Global css classes used by subcomponents
|
||||||
|
|
||||||
|
// Graph.svelte
|
||||||
|
export const oddTickClass = "tick-odd";
|
||||||
|
export const clickableClass = "graph-element-clickable";
|
||||||
|
|
||||||
|
// It would be nice to define these in the svelte file that declares them,
|
||||||
|
// but currently this trips the tooling up:
|
||||||
|
// https://github.com/sveltejs/svelte/issues/5817
|
||||||
|
// export { oddTickClass, clickableClass } from "./Graph.svelte";
|
|
@ -1,155 +0,0 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
|
||||||
|
|
||||||
@use '../sass/core';
|
|
||||||
|
|
||||||
* {
|
|
||||||
overscroll-behavior: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
max-width: 60em;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
margin-top: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-domain-line .domain {
|
|
||||||
opacity: 0.05;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tick {
|
|
||||||
line {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
text {
|
|
||||||
opacity: 0.5;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 800px) {
|
|
||||||
.tick text {
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media only screen and (max-width: 600px) {
|
|
||||||
body {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tick text {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tick-odd {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-box {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
color: var(--text-fg);
|
|
||||||
background: var(--window-bg);
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media print {
|
|
||||||
.range-box {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-box-pad {
|
|
||||||
height: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-box-inner {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-width: 480px) and (orientation: portrait) {
|
|
||||||
.range-box-inner {
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-box-inner > * {
|
|
||||||
padding-left: 0.5em;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin {
|
|
||||||
position: absolute;
|
|
||||||
animation: spin;
|
|
||||||
animation-duration: 1s;
|
|
||||||
animation-iteration-count: infinite;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 2em;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spin.active {
|
|
||||||
opacity: 0.5;
|
|
||||||
transition: opacity 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-outer {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-data {
|
|
||||||
text {
|
|
||||||
text-anchor: middle;
|
|
||||||
fill: grey;
|
|
||||||
}
|
|
||||||
rect {
|
|
||||||
fill: var(--window-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-end {
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.align-start {
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-focus-outline:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import type { ScaleLinear, ScaleSequential, Bin } from "d3";
|
import type { ScaleLinear, ScaleSequential, Bin } from "d3";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds, setDataAvailable } from "./graph-helpers";
|
import { GraphBounds, setDataAvailable } from "./graph-helpers";
|
||||||
|
import { clickableClass } from "./graph-styles";
|
||||||
|
|
||||||
export interface HistogramData {
|
export interface HistogramData {
|
||||||
scale: ScaleLinear<number, number>;
|
scale: ScaleLinear<number, number>;
|
||||||
|
@ -57,13 +58,13 @@ export function histogramGraph(
|
||||||
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
||||||
|
|
||||||
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
svg.select<SVGGElement>(".x-ticks")
|
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisBottom(x)
|
axisBottom(x)
|
||||||
.ticks(7)
|
.ticks(7)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
.tickFormat((data.xTickFormat ?? null) as any)
|
.tickFormat((data.xTickFormat ?? null) as any)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// y scale
|
// y scale
|
||||||
|
@ -73,12 +74,12 @@ export function histogramGraph(
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
.domain([0, yMax])
|
.domain([0, yMax])
|
||||||
.nice();
|
.nice();
|
||||||
svg.select<SVGGElement>(".y-ticks")
|
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// x bars
|
// x bars
|
||||||
|
@ -125,15 +126,15 @@ export function histogramGraph(
|
||||||
const yAreaScale = y.copy().domain([0, data.total]).nice();
|
const yAreaScale = y.copy().domain([0, data.total]).nice();
|
||||||
|
|
||||||
if (data.showArea && data.bins.length && areaData.slice(-1)[0]) {
|
if (data.showArea && data.bins.length && areaData.slice(-1)[0]) {
|
||||||
svg.select<SVGGElement>(".y2-ticks")
|
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisRight(yAreaScale)
|
axisRight(yAreaScale)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
svg.select("path.cumulative-overlay")
|
svg.select("path.area")
|
||||||
.datum(areaData as any)
|
.datum(areaData as any)
|
||||||
.attr(
|
.attr(
|
||||||
"d",
|
"d",
|
||||||
|
@ -179,7 +180,7 @@ export function histogramGraph(
|
||||||
if (data.onClick) {
|
if (data.onClick) {
|
||||||
hoverzone
|
hoverzone
|
||||||
.filter(([bin]) => bin.length > 0)
|
.filter(([bin]) => bin.length > 0)
|
||||||
.attr("class", "clickable")
|
.attr("class", clickableClass)
|
||||||
.on("click", (_event, [bin]) => data.onClick!(bin));
|
.on("click", (_event, [bin]) => data.onClick!(bin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
import {
|
import {
|
||||||
interpolateBlues,
|
interpolateBlues,
|
||||||
|
@ -20,6 +21,7 @@ import {
|
||||||
area,
|
area,
|
||||||
curveBasis,
|
curveBasis,
|
||||||
} from "d3";
|
} from "d3";
|
||||||
|
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import {
|
import {
|
||||||
GraphBounds,
|
GraphBounds,
|
||||||
|
@ -27,9 +29,7 @@ import {
|
||||||
GraphRange,
|
GraphRange,
|
||||||
millisecondCutoffForRange,
|
millisecondCutoffForRange,
|
||||||
} from "./graph-helpers";
|
} from "./graph-helpers";
|
||||||
import type { I18n } from "anki/i18n";
|
import { oddTickClass } from "./graph-styles";
|
||||||
|
|
||||||
type ButtonCounts = [number, number, number, number];
|
|
||||||
|
|
||||||
interface Hour {
|
interface Hour {
|
||||||
hour: number;
|
hour: number;
|
||||||
|
@ -97,16 +97,12 @@ export function renderHours(
|
||||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
||||||
.paddingInner(0.1);
|
.paddingInner(0.1);
|
||||||
svg.select<SVGGElement>(".x-ticks")
|
svg.select<SVGGElement>(".x-ticks")
|
||||||
.transition(trans)
|
.call((selection) =>
|
||||||
.call(axisBottom(x).tickSizeOuter(0))
|
selection.transition(trans).call(axisBottom(x).tickSizeOuter(0))
|
||||||
|
)
|
||||||
|
.selectAll(".tick")
|
||||||
.selectAll("text")
|
.selectAll("text")
|
||||||
.attr("class", (n: any) => {
|
.classed(oddTickClass, (d: any): boolean => d % 2 != 0);
|
||||||
if (n % 2 != 0) {
|
|
||||||
return "tick-odd";
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cappedRange = scaleLinear().range([0.1, 0.8]);
|
const cappedRange = scaleLinear().range([0.1, 0.8]);
|
||||||
const colour = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain([
|
const colour = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain([
|
||||||
|
@ -120,12 +116,12 @@ export function renderHours(
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
.domain([0, yMax])
|
.domain([0, yMax])
|
||||||
.nice();
|
.nice();
|
||||||
svg.select<SVGGElement>(".y-ticks")
|
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const yArea = y.copy().domain([0, 1]);
|
const yArea = y.copy().domain([0, 1]);
|
||||||
|
@ -162,13 +158,13 @@ export function renderHours(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
svg.select<SVGGElement>(".y2-ticks")
|
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisRight(yArea)
|
axisRight(yArea)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickFormat((n: any) => `${Math.round(n * 100)}%`)
|
.tickFormat((n: any) => `${Math.round(n * 100)}%`)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
svg.select("path.cumulative-overlay")
|
svg.select("path.cumulative-overlay")
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
/* eslint
|
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { SvelteComponent } from "svelte/internal";
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
|
|
||||||
import { setupI18n } from "anki/i18n";
|
import { setupI18n } from "anki/i18n";
|
||||||
import GraphsPage from "./GraphsPage.svelte";
|
|
||||||
import { checkNightMode } from "anki/nightmode";
|
import { checkNightMode } from "anki/nightmode";
|
||||||
|
|
||||||
|
import GraphsPage from "./GraphsPage.svelte";
|
||||||
|
|
||||||
export { default as RangeBox } from "./RangeBox.svelte";
|
export { default as RangeBox } from "./RangeBox.svelte";
|
||||||
|
|
||||||
export { default as IntervalsGraph } from "./IntervalsGraph.svelte";
|
export { default as IntervalsGraph } from "./IntervalsGraph.svelte";
|
||||||
|
@ -28,7 +25,11 @@ export { RevlogRange } from "./graph-helpers";
|
||||||
export function graphs(
|
export function graphs(
|
||||||
target: HTMLDivElement,
|
target: HTMLDivElement,
|
||||||
graphs: SvelteComponent[],
|
graphs: SvelteComponent[],
|
||||||
{ search = "deck:current", days = 365, controller = null as any } = {}
|
{
|
||||||
|
search = "deck:current",
|
||||||
|
days = 365,
|
||||||
|
controller = null as SvelteComponent | null,
|
||||||
|
} = {}
|
||||||
): void {
|
): void {
|
||||||
const nightMode = checkNightMode();
|
const nightMode = checkNightMode();
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pb from "anki/backend_proto";
|
import pb from "anki/backend_proto";
|
||||||
|
import type { I18n } from "anki/i18n";
|
||||||
|
import { timeSpan, dayLabel } from "anki/time";
|
||||||
import {
|
import {
|
||||||
interpolateGreens,
|
interpolateGreens,
|
||||||
interpolateReds,
|
interpolateReds,
|
||||||
|
@ -29,11 +31,9 @@ import {
|
||||||
} from "d3";
|
} from "d3";
|
||||||
import type { Bin } from "d3";
|
import type { Bin } from "d3";
|
||||||
|
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
|
||||||
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
|
||||||
import type { TableDatum } from "./graph-helpers";
|
import type { TableDatum } from "./graph-helpers";
|
||||||
import { timeSpan, dayLabel } from "anki/time";
|
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
||||||
import type { I18n } from "anki/i18n";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
|
|
||||||
interface Reviews {
|
interface Reviews {
|
||||||
learn: number;
|
learn: number;
|
||||||
|
@ -167,9 +167,9 @@ export function renderReviews(
|
||||||
}
|
}
|
||||||
|
|
||||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
svg.select<SVGGElement>(".x-ticks")
|
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(axisBottom(x).ticks(7).tickSizeOuter(0))
|
||||||
.call(axisBottom(x).ticks(7).tickSizeOuter(0));
|
);
|
||||||
|
|
||||||
// y scale
|
// y scale
|
||||||
|
|
||||||
|
@ -190,13 +190,13 @@ export function renderReviews(
|
||||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||||
.domain([0, yMax])
|
.domain([0, yMax])
|
||||||
.nice();
|
.nice();
|
||||||
svg.select<SVGGElement>(".y-ticks")
|
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisLeft(y)
|
axisLeft(y)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
.tickFormat(yTickFormat as any)
|
.tickFormat(yTickFormat as any)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// x bars
|
// x bars
|
||||||
|
@ -331,13 +331,13 @@ export function renderReviews(
|
||||||
const yAreaScale = y.copy().domain([0, yCumMax]).nice();
|
const yAreaScale = y.copy().domain([0, yCumMax]).nice();
|
||||||
|
|
||||||
if (yCumMax) {
|
if (yCumMax) {
|
||||||
svg.select<SVGGElement>(".y2-ticks")
|
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||||
.transition(trans)
|
selection.transition(trans).call(
|
||||||
.call(
|
|
||||||
axisRight(yAreaScale)
|
axisRight(yAreaScale)
|
||||||
.ticks(bounds.height / 50)
|
.ticks(bounds.height / 50)
|
||||||
.tickFormat(yTickFormat as any)
|
.tickFormat(yTickFormat as any)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
svg.select("path.cumulative-overlay")
|
svg.select("path.cumulative-overlay")
|
||||||
|
|
40
ts/graphs/ticks.scss
Normal file
40
ts/graphs/ticks.scss
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
|
// Customizing the standard x and y tick markers and text on the graphs. The `tick`
|
||||||
|
// class is automatically added by d3. We apply our custom ticks only to ticks
|
||||||
|
// that are nested under a Graph component.
|
||||||
|
|
||||||
|
.graph {
|
||||||
|
.tick {
|
||||||
|
line {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 800px) {
|
||||||
|
.tick {
|
||||||
|
text {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 600px) {
|
||||||
|
.tick {
|
||||||
|
text {
|
||||||
|
font-size: 16px;
|
||||||
|
// on small screens, hide every second row on graphs that have
|
||||||
|
// marked the ticks as odd
|
||||||
|
&.tick-odd {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ body {
|
||||||
background: var(--window-bg);
|
background: var(--window-bg);
|
||||||
margin: 1em;
|
margin: 1em;
|
||||||
transition: opacity 0.5s ease-out;
|
transition: opacity 0.5s ease-out;
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
Loading…
Reference in a new issue