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
@ -13,6 +13,7 @@ copy_files_into_group(
|
||||
copy_files_into_group(
|
||||
name = "congrats_page",
|
||||
srcs = [
|
||||
"congrats.css",
|
||||
"congrats.html",
|
||||
"congrats.js",
|
||||
],
|
||||
|
@ -10,8 +10,8 @@ svelte(
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "bootstrap",
|
||||
srcs = ["bootstrap.ts"],
|
||||
name = "index",
|
||||
srcs = ["index.ts"],
|
||||
deps = [
|
||||
"CongratsPage",
|
||||
"lib",
|
||||
@ -38,17 +38,19 @@ esbuild(
|
||||
"--global-name=anki",
|
||||
"--inject:ts/protobuf-shim.js",
|
||||
],
|
||||
entry_point = "bootstrap.ts",
|
||||
entry_point = "index.ts",
|
||||
external = [
|
||||
"protobufjs/light",
|
||||
],
|
||||
output_css = True,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"CongratsPage",
|
||||
"bootstrap",
|
||||
"index",
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"//ts/lib:fluent_proto",
|
||||
"//ts/sass:core_css",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import "../sass/core.css";
|
||||
|
||||
import { I18n } from "anki/i18n";
|
||||
import pb from "anki/backend_proto";
|
||||
import { buildNextLearnMsg } from "./lib";
|
||||
|
@ -3,14 +3,15 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<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>
|
||||
<body>
|
||||
<div id="main"></div>
|
||||
|
||||
<script>
|
||||
anki.congrats(document.getElementById("main"));
|
||||
</script>
|
||||
</body>
|
||||
<script src="../js/vendor/protobuf.min.js"></script>
|
||||
<script src="congrats.js"></script>
|
||||
<script>
|
||||
anki.congrats(document.getElementById("main"));
|
||||
</script>
|
||||
</html>
|
||||
|
@ -1,11 +1,12 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { setupI18n } from "anki/i18n";
|
||||
import { checkNightMode } from "anki/nightmode";
|
||||
|
||||
import CongratsPage from "./CongratsPage.svelte";
|
||||
|
||||
export async function congrats(target: HTMLDivElement): Promise<void> {
|
||||
checkNightMode();
|
||||
const i18n = await setupI18n();
|
@ -1,15 +1,19 @@
|
||||
<script lang="typescript">
|
||||
import { RevlogRange, GraphRange } from "./graph-helpers";
|
||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||
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 { createEventDispatcher } from "svelte";
|
||||
|
||||
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 { 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";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
@ -42,16 +46,12 @@
|
||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-added">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -1,12 +1,15 @@
|
||||
<script lang="typescript">
|
||||
import type { GraphBounds } from "./graph-helpers";
|
||||
|
||||
export let bounds: GraphBounds;
|
||||
</script>
|
||||
|
||||
<g
|
||||
class="x-ticks no-domain-line"
|
||||
transform={`translate(0,${bounds.height - bounds.marginBottom})`} />
|
||||
<g class="y-ticks no-domain-line" transform={`translate(${bounds.marginLeft}, 0)`} />
|
||||
<g
|
||||
class="y2-ticks no-domain-line"
|
||||
transform={`translate(${bounds.width - bounds.marginRight}, 0)`} />
|
||||
<style lang="scss">
|
||||
g :global(.domain) {
|
||||
opacity: 0.05;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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")
|
||||
|
||||
sass_binary(
|
||||
name = "graphs_shared",
|
||||
src = "graphs_shared.scss",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//ts/sass:core_lib",
|
||||
],
|
||||
name = "ticks",
|
||||
src = "ticks.scss",
|
||||
)
|
||||
|
||||
svelte_files = glob(["*.svelte"])
|
||||
@ -24,8 +20,8 @@ compile_svelte(
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "bootstrap",
|
||||
srcs = ["bootstrap.ts"],
|
||||
name = "index",
|
||||
srcs = ["index.ts"],
|
||||
deps = [
|
||||
"GraphsPage",
|
||||
"lib",
|
||||
@ -39,7 +35,7 @@ ts_library(
|
||||
name = "lib",
|
||||
srcs = glob(
|
||||
["*.ts"],
|
||||
exclude = ["bootstrap.ts"],
|
||||
exclude = ["index.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//ts/lib",
|
||||
@ -62,7 +58,7 @@ esbuild(
|
||||
"--global-name=anki",
|
||||
"--inject:ts/protobuf-shim.js",
|
||||
],
|
||||
entry_point = "bootstrap.ts",
|
||||
entry_point = "index.ts",
|
||||
external = [
|
||||
"protobufjs/light",
|
||||
],
|
||||
@ -72,8 +68,9 @@ esbuild(
|
||||
"//ts/lib",
|
||||
"//ts/lib:backend_proto",
|
||||
"//ts/lib:fluent_proto",
|
||||
"bootstrap",
|
||||
"graphs_shared",
|
||||
":index",
|
||||
":ticks",
|
||||
"//ts/sass:core_css",
|
||||
] + svelte_names,
|
||||
)
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
<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 { 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 GraphRangeRadios from "./GraphRangeRadios.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 i18n: I18n;
|
||||
@ -26,14 +29,10 @@
|
||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ANSWER_BUTTONS_SUBTITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-buttons">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="bars" />
|
||||
@ -41,4 +40,4 @@
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -1,14 +1,18 @@
|
||||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
|
||||
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
||||
import type { SearchEventMap } from "./graph-helpers";
|
||||
import { gatherData, renderCalendar } from "./calendar";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
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 preferences: PreferenceStore | null = null;
|
||||
@ -65,10 +69,8 @@
|
||||
const title = i18n.tr(i18n.TR.STATISTICS_CALENDAR_TITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-calendar">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title}>
|
||||
<InputBox>
|
||||
<span>
|
||||
<button on:click={() => targetYear--} disabled={minYear >= targetYear}>
|
||||
◄
|
||||
@ -80,7 +82,7 @@
|
||||
►
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="weekdays" />
|
||||
@ -88,4 +90,4 @@
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</svg>
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -1,12 +1,16 @@
|
||||
<script lang="typescript">
|
||||
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 type { SearchEventMap } from "./graph-helpers";
|
||||
import { gatherData, renderCards } from "./card-counts";
|
||||
import type { GraphData, TableDatum } from "./card-counts";
|
||||
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 i18n: I18n;
|
||||
@ -68,15 +72,13 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="graph" id="graph-card-counts">
|
||||
<h1>{graphData.title}</h1>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph title={graphData.title}>
|
||||
<InputBox>
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={$cardCountsSeparateInactive} />
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<div class="counts-outer">
|
||||
<svg
|
||||
@ -113,4 +115,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -1,12 +1,15 @@
|
||||
<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 { 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 TableData from "./TableData.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
@ -32,12 +35,8 @@
|
||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-ease">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<Graph {title} {subtitle}>
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -1,15 +1,19 @@
|
||||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
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 { GraphRange, RevlogRange } from "./graph-helpers";
|
||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||
import { gatherData, buildHistogram } 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";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
@ -44,12 +48,8 @@
|
||||
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-future-due">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
{#if graphData && graphData.haveBacklog}
|
||||
<label>
|
||||
<input type="checkbox" bind:checked={$futureDueShowBacklog} />
|
||||
@ -58,9 +58,9 @@
|
||||
{/if}
|
||||
|
||||
<GraphRangeRadios bind:graphRange {i18n} revlogRange={RevlogRange.All} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
|
||||
<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">
|
||||
import "./graphs_shared.css";
|
||||
import "../sass/core.css";
|
||||
|
||||
import type { SvelteComponent } from "svelte/internal";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
@ -52,27 +53,41 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if controller}
|
||||
<svelte:component
|
||||
this={controller}
|
||||
{i18n}
|
||||
{search}
|
||||
{days}
|
||||
{active}
|
||||
on:update={refresh} />
|
||||
{/if}
|
||||
<style lang="scss">
|
||||
@media only screen and (max-width: 600px) {
|
||||
.base {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
{#if sourceData}
|
||||
<div tabindex="-1" class="no-focus-outline">
|
||||
{#each graphs as graph}
|
||||
<svelte:component
|
||||
this={graph}
|
||||
{sourceData}
|
||||
{preferences}
|
||||
{revlogRange}
|
||||
{i18n}
|
||||
{nightMode}
|
||||
on:search={browserSearch} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
.no-focus-outline:focus {
|
||||
outline: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="base">
|
||||
{#if controller}
|
||||
<svelte:component
|
||||
this={controller}
|
||||
{i18n}
|
||||
{search}
|
||||
{days}
|
||||
{active}
|
||||
on:update={refresh} />
|
||||
{/if}
|
||||
|
||||
{#if sourceData}
|
||||
<div tabindex="-1" class="no-focus-outline">
|
||||
{#each graphs as graph}
|
||||
<svelte:component
|
||||
this={graph}
|
||||
{sourceData}
|
||||
{preferences}
|
||||
{revlogRange}
|
||||
{i18n}
|
||||
{nightMode}
|
||||
on:search={browserSearch} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,13 +1,14 @@
|
||||
<script lang="typescript">
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import { histogramGraph } from "./histogram-graph";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
import AxisTicks from "./AxisTicks.svelte";
|
||||
import { defaultGraphBounds } from "./graph-helpers";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import CumulativeOverlay from "./CumulativeOverlay.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 i18n: I18n;
|
||||
|
@ -1,13 +1,16 @@
|
||||
<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 { 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 GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||
import CumulativeOverlay from "./CumulativeOverlay.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 i18n: I18n;
|
||||
@ -26,14 +29,10 @@
|
||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_HOURS_SUBTITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-hour">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
<g class="bars" />
|
||||
@ -42,4 +41,4 @@
|
||||
<AxisTicks {bounds} />
|
||||
<NoDataOverlay {bounds} {i18n} />
|
||||
</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">
|
||||
import { timeSpan, MONTH } from "anki/time";
|
||||
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 {
|
||||
gatherIntervalData,
|
||||
@ -8,11 +16,7 @@
|
||||
prepareIntervalData,
|
||||
} 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 TableData from "./TableData.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
@ -42,17 +46,13 @@
|
||||
}
|
||||
|
||||
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 all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_TIME);
|
||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
||||
</script>
|
||||
|
||||
<div class="graph intervals" id="graph-intervals">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<label>
|
||||
<input type="radio" bind:group={range} value={IntervalRange.Month} />
|
||||
{month}
|
||||
@ -69,9 +69,9 @@
|
||||
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<HistogramGraph data={histogramData} {i18n} />
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -6,6 +6,19 @@
|
||||
const noData = i18n.tr(i18n.TR.STATISTICS_NO_DATA);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.no-data {
|
||||
rect {
|
||||
fill: var(--window-bg);
|
||||
}
|
||||
|
||||
text {
|
||||
text-anchor: middle;
|
||||
fill: grey;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<g class="no-data">
|
||||
<rect x="0" y="0" width={bounds.width} height={bounds.height} />
|
||||
<text x="{bounds.width / 2}," y={bounds.height / 2}>{noData}</text>
|
||||
|
@ -1,6 +1,8 @@
|
||||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import InputBox from "./InputBox.svelte";
|
||||
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
||||
|
||||
@ -81,10 +83,55 @@
|
||||
const all = i18n.tr(i18n.TR.STATISTICS_RANGE_ALL_HISTORY);
|
||||
</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="spin" class:active>◐</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<InputBox>
|
||||
<label>
|
||||
<input type="radio" bind:group={searchRange} value={SearchRange.Deck} />
|
||||
{deck}
|
||||
@ -105,9 +152,9 @@
|
||||
searchRange = SearchRange.Custom;
|
||||
}}
|
||||
placeholder={searchLabel} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<InputBox>
|
||||
<label>
|
||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.Year} />
|
||||
{year}
|
||||
@ -116,7 +163,7 @@
|
||||
<input type="radio" bind:group={revlogRange} value={RevlogRange.All} />
|
||||
{all}
|
||||
</label>
|
||||
</div>
|
||||
</InputBox>
|
||||
</div>
|
||||
|
||||
<div class="range-box-pad" />
|
||||
|
@ -1,17 +1,21 @@
|
||||
<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 { I18n } from "anki/i18n";
|
||||
|
||||
import Graph from "./Graph.svelte";
|
||||
import InputBox from "./InputBox.svelte";
|
||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||
import CumulativeOverlay from "./CumulativeOverlay.svelte";
|
||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||
import TableData from "./TableData.svelte";
|
||||
import AxisTicks from "./AxisTicks.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 revlogRange: RevlogRange;
|
||||
export let i18n: I18n;
|
||||
@ -50,16 +54,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="graph" id="graph-reviews">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<div class="subtitle">{subtitle}</div>
|
||||
|
||||
<div class="range-box-inner">
|
||||
<Graph {title} {subtitle}>
|
||||
<InputBox>
|
||||
<label> <input type="checkbox" bind:checked={showTime} /> {time} </label>
|
||||
|
||||
<GraphRangeRadios bind:graphRange {i18n} {revlogRange} followRevlog={true} />
|
||||
</div>
|
||||
</InputBox>
|
||||
|
||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||
{#each [4, 3, 2, 1, 0] as i}
|
||||
@ -72,4 +72,4 @@
|
||||
</svg>
|
||||
|
||||
<TableData {i18n} {tableData} />
|
||||
</div>
|
||||
</Graph>
|
||||
|
@ -6,7 +6,22 @@
|
||||
export let tableData: TableDatum[];
|
||||
</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()}>
|
||||
{#each tableData as { label, value }}
|
||||
<tr>
|
||||
|
@ -1,9 +1,12 @@
|
||||
<script lang="typescript">
|
||||
import { gatherData } from "./today";
|
||||
import type { TodayData } from "./today";
|
||||
import type pb from "anki/backend_proto";
|
||||
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 i18n: I18n;
|
||||
|
||||
@ -13,14 +16,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if todayData}
|
||||
<div class="graph" id="graph-today-stats">
|
||||
<h1>{todayData.title}</h1>
|
||||
<style lang="scss">
|
||||
.legend {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="legend-outer">
|
||||
{#if todayData}
|
||||
<Graph title={todayData.title}>
|
||||
<div class="legend">
|
||||
{#each todayData.lines as line}
|
||||
<div>{line}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Graph>
|
||||
{/if}
|
||||
|
@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import {
|
||||
interpolateRdYlGn,
|
||||
select,
|
||||
@ -25,7 +26,6 @@ import {
|
||||
GraphRange,
|
||||
millisecondCutoffForRange,
|
||||
} from "./graph-helpers";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
|
||||
@ -153,9 +153,8 @@ export function renderButtons(
|
||||
const xGroup = scaleBand()
|
||||
.domain(["learning", "young", "mature"])
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisBottom(xGroup)
|
||||
.tickFormat(((d: GroupKind) => {
|
||||
let kind: string;
|
||||
@ -174,7 +173,8 @@ export function renderButtons(
|
||||
return `${kind} \u200e(${totalCorrect(d).percent}%)`;
|
||||
}) as any)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
const xButton = scaleBand()
|
||||
.domain(["1", "2", "3", "4"])
|
||||
@ -189,13 +189,13 @@ export function renderButtons(
|
||||
const y = scaleLinear()
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax]);
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
// x bars
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
|
||||
/* eslint
|
||||
@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 {
|
||||
interpolateBlues,
|
||||
@ -21,6 +21,7 @@ import {
|
||||
timeSaturday,
|
||||
} from "d3";
|
||||
import type { CountableTimeInterval } from "d3";
|
||||
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import {
|
||||
GraphBounds,
|
||||
@ -28,7 +29,7 @@ import {
|
||||
RevlogRange,
|
||||
SearchDispatch,
|
||||
} from "./graph-helpers";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { clickableClass } from "./graph-styles";
|
||||
|
||||
export interface GraphData {
|
||||
// indexed by day, where day is relative to today
|
||||
@ -203,24 +204,22 @@ export function renderCalendar(
|
||||
.data(data)
|
||||
.join("rect")
|
||||
.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("x", (d) => x(d.weekNumber + 1)!)
|
||||
.attr("y", (d) => bounds.marginTop + d.weekDay * height)
|
||||
.attr("x", (d: DayDatum) => x(d.weekNumber + 1)!)
|
||||
.attr("y", (d: DayDatum) => bounds.marginTop + d.weekDay * height)
|
||||
.on("mousemove", (event: MouseEvent, d: DayDatum) => {
|
||||
const [x, y] = pointer(event, document.body);
|
||||
showTooltip(tooltipText(d), x, y);
|
||||
})
|
||||
.on("mouseout", hideTooltip)
|
||||
.attr("class", (d: any): string => {
|
||||
return d.count > 0 ? "clickable" : "";
|
||||
})
|
||||
.on("click", function (_event: MouseEvent, d: any) {
|
||||
.attr("class", (d: DayDatum): string => (d.count > 0 ? clickableClass : ""))
|
||||
.on("click", function (_event: MouseEvent, d: DayDatum) {
|
||||
if (d.count > 0) {
|
||||
dispatch("search", { query: `"prop:rated=${d.day}"` });
|
||||
}
|
||||
})
|
||||
.transition()
|
||||
.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 { showTooltip, hideTooltip } from "./tooltip";
|
||||
import { GraphBounds, setDataAvailable } from "./graph-helpers";
|
||||
import { clickableClass } from "./graph-styles";
|
||||
|
||||
export interface HistogramData {
|
||||
scale: ScaleLinear<number, number>;
|
||||
@ -57,14 +58,14 @@ export function histogramGraph(
|
||||
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
||||
|
||||
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisBottom(x)
|
||||
.ticks(7)
|
||||
.tickSizeOuter(0)
|
||||
.tickFormat((data.xTickFormat ?? null) as any)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
// y scale
|
||||
|
||||
@ -73,13 +74,13 @@ export function histogramGraph(
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax])
|
||||
.nice();
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
// x bars
|
||||
|
||||
@ -125,15 +126,15 @@ export function histogramGraph(
|
||||
const yAreaScale = y.copy().domain([0, data.total]).nice();
|
||||
|
||||
if (data.showArea && data.bins.length && areaData.slice(-1)[0]) {
|
||||
svg.select<SVGGElement>(".y2-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisRight(yAreaScale)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
svg.select("path.cumulative-overlay")
|
||||
svg.select("path.area")
|
||||
.datum(areaData as any)
|
||||
.attr(
|
||||
"d",
|
||||
@ -179,7 +180,7 @@ export function histogramGraph(
|
||||
if (data.onClick) {
|
||||
hoverzone
|
||||
.filter(([bin]) => bin.length > 0)
|
||||
.attr("class", "clickable")
|
||||
.attr("class", clickableClass)
|
||||
.on("click", (_event, [bin]) => data.onClick!(bin));
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import type { I18n } from "anki/i18n";
|
||||
import pb from "anki/backend_proto";
|
||||
import {
|
||||
interpolateBlues,
|
||||
@ -20,6 +21,7 @@ import {
|
||||
area,
|
||||
curveBasis,
|
||||
} from "d3";
|
||||
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
import {
|
||||
GraphBounds,
|
||||
@ -27,9 +29,7 @@ import {
|
||||
GraphRange,
|
||||
millisecondCutoffForRange,
|
||||
} from "./graph-helpers";
|
||||
import type { I18n } from "anki/i18n";
|
||||
|
||||
type ButtonCounts = [number, number, number, number];
|
||||
import { oddTickClass } from "./graph-styles";
|
||||
|
||||
interface Hour {
|
||||
hour: number;
|
||||
@ -97,16 +97,12 @@ export function renderHours(
|
||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
||||
.paddingInner(0.1);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
.call(axisBottom(x).tickSizeOuter(0))
|
||||
.call((selection) =>
|
||||
selection.transition(trans).call(axisBottom(x).tickSizeOuter(0))
|
||||
)
|
||||
.selectAll(".tick")
|
||||
.selectAll("text")
|
||||
.attr("class", (n: any) => {
|
||||
if (n % 2 != 0) {
|
||||
return "tick-odd";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
.classed(oddTickClass, (d: any): boolean => d % 2 != 0);
|
||||
|
||||
const cappedRange = scaleLinear().range([0.1, 0.8]);
|
||||
const colour = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain([
|
||||
@ -120,13 +116,13 @@ export function renderHours(
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax])
|
||||
.nice();
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
const yArea = y.copy().domain([0, 1]);
|
||||
|
||||
@ -162,14 +158,14 @@ export function renderHours(
|
||||
)
|
||||
);
|
||||
|
||||
svg.select<SVGGElement>(".y2-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisRight(yArea)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickFormat((n: any) => `${Math.round(n * 100)}%`)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
svg.select("path.cumulative-overlay")
|
||||
.datum(data)
|
||||
|
@ -1,16 +1,13 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { setupI18n } from "anki/i18n";
|
||||
import GraphsPage from "./GraphsPage.svelte";
|
||||
import { checkNightMode } from "anki/nightmode";
|
||||
|
||||
import GraphsPage from "./GraphsPage.svelte";
|
||||
|
||||
export { default as RangeBox } from "./RangeBox.svelte";
|
||||
|
||||
export { default as IntervalsGraph } from "./IntervalsGraph.svelte";
|
||||
@ -28,7 +25,11 @@ export { RevlogRange } from "./graph-helpers";
|
||||
export function graphs(
|
||||
target: HTMLDivElement,
|
||||
graphs: SvelteComponent[],
|
||||
{ search = "deck:current", days = 365, controller = null as any } = {}
|
||||
{
|
||||
search = "deck:current",
|
||||
days = 365,
|
||||
controller = null as SvelteComponent | null,
|
||||
} = {}
|
||||
): void {
|
||||
const nightMode = checkNightMode();
|
||||
|
@ -7,6 +7,8 @@
|
||||
*/
|
||||
|
||||
import pb from "anki/backend_proto";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { timeSpan, dayLabel } from "anki/time";
|
||||
import {
|
||||
interpolateGreens,
|
||||
interpolateReds,
|
||||
@ -29,11 +31,9 @@ import {
|
||||
} 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 { timeSpan, dayLabel } from "anki/time";
|
||||
import type { I18n } from "anki/i18n";
|
||||
import { GraphBounds, setDataAvailable, GraphRange } from "./graph-helpers";
|
||||
import { showTooltip, hideTooltip } from "./tooltip";
|
||||
|
||||
interface Reviews {
|
||||
learn: number;
|
||||
@ -167,9 +167,9 @@ export function renderReviews(
|
||||
}
|
||||
|
||||
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||
svg.select<SVGGElement>(".x-ticks")
|
||||
.transition(trans)
|
||||
.call(axisBottom(x).ticks(7).tickSizeOuter(0));
|
||||
svg.select<SVGGElement>(".x-ticks").call((selection) =>
|
||||
selection.transition(trans).call(axisBottom(x).ticks(7).tickSizeOuter(0))
|
||||
);
|
||||
|
||||
// y scale
|
||||
|
||||
@ -190,14 +190,14 @@ export function renderReviews(
|
||||
.range([bounds.height - bounds.marginBottom, bounds.marginTop])
|
||||
.domain([0, yMax])
|
||||
.nice();
|
||||
svg.select<SVGGElement>(".y-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisLeft(y)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickSizeOuter(0)
|
||||
.tickFormat(yTickFormat as any)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
// x bars
|
||||
|
||||
@ -331,14 +331,14 @@ export function renderReviews(
|
||||
const yAreaScale = y.copy().domain([0, yCumMax]).nice();
|
||||
|
||||
if (yCumMax) {
|
||||
svg.select<SVGGElement>(".y2-ticks")
|
||||
.transition(trans)
|
||||
.call(
|
||||
svg.select<SVGGElement>(".y2-ticks").call((selection) =>
|
||||
selection.transition(trans).call(
|
||||
axisRight(yAreaScale)
|
||||
.ticks(bounds.height / 50)
|
||||
.tickFormat(yTickFormat as any)
|
||||
.tickSizeOuter(0)
|
||||
);
|
||||
)
|
||||
);
|
||||
|
||||
svg.select("path.cumulative-overlay")
|
||||
.datum(areaData as any)
|
||||
|
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);
|
||||
margin: 1em;
|
||||
transition: opacity 0.5s ease-out;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
a {
|
||||
|
Loading…
Reference in New Issue
Block a user