diff --git a/rslib/ftl/statistics.ftl b/rslib/ftl/statistics.ftl
index 8f20ce765..8fc0c04ac 100644
--- a/rslib/ftl/statistics.ftl
+++ b/rslib/ftl/statistics.ftl
@@ -157,3 +157,5 @@ statistics-hours-range = From { $hourStart }:00~{ $hourEnd }:00
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
statistics-hours-title = Hourly Breakdown
statistics-hours-subtitle = Review success rate for each hour of the day.
+# shown when graph is empty
+statistics-no-data = NO DATA
diff --git a/ts/src/stats/AddedGraph.svelte b/ts/src/stats/AddedGraph.svelte
index 4c7ce7f17..6188b6518 100644
--- a/ts/src/stats/AddedGraph.svelte
+++ b/ts/src/stats/AddedGraph.svelte
@@ -44,31 +44,29 @@
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
-{#if histogramData}
-
-
{title}
+
diff --git a/ts/src/stats/ButtonsGraph.svelte b/ts/src/stats/ButtonsGraph.svelte
index f4a053d4c..9ea4c1fb3 100644
--- a/ts/src/stats/ButtonsGraph.svelte
+++ b/ts/src/stats/ButtonsGraph.svelte
@@ -4,6 +4,7 @@
import { gatherData, GraphData, renderButtons } from "./buttons";
import pb from "../backend/proto";
import { I18n } from "../i18n";
+ import NoDataOverlay from "./NoDataOverlay.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n;
@@ -29,5 +30,6 @@
+
diff --git a/ts/src/stats/CalendarGraph.svelte b/ts/src/stats/CalendarGraph.svelte
index 41f2f6f29..fd2b3508c 100644
--- a/ts/src/stats/CalendarGraph.svelte
+++ b/ts/src/stats/CalendarGraph.svelte
@@ -1,9 +1,8 @@
-{#if graphData}
-
-
{graphData.title}
+
-
+
+
{graphData.title}
-
{total}: {graphData.totalCards}
+
-
-{/if}
+
{total}: {graphData.totalCards}
+
+
diff --git a/ts/src/stats/EaseGraph.svelte b/ts/src/stats/EaseGraph.svelte
index 8b44e39ee..38dbe4536 100644
--- a/ts/src/stats/EaseGraph.svelte
+++ b/ts/src/stats/EaseGraph.svelte
@@ -19,12 +19,10 @@
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
-{#if histogramData}
-
-
{title}
+
+
{title}
-
{subtitle}
+
{subtitle}
-
-
-{/if}
+
+
diff --git a/ts/src/stats/FutureDue.svelte b/ts/src/stats/FutureDue.svelte
index 8c9e0e93b..d9214bdb9 100644
--- a/ts/src/stats/FutureDue.svelte
+++ b/ts/src/stats/FutureDue.svelte
@@ -52,38 +52,35 @@
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
-{#if histogramData}
+
+
{title}
-
diff --git a/ts/src/stats/GraphsPage.svelte b/ts/src/stats/GraphsPage.svelte
index 4aba44c70..6d1b93455 100644
--- a/ts/src/stats/GraphsPage.svelte
+++ b/ts/src/stats/GraphsPage.svelte
@@ -141,13 +141,15 @@
-
-
-
-
-
-
-
-
-
-
+{#if sourceData}
+
+
+
+
+
+
+
+
+
+
+{/if}
diff --git a/ts/src/stats/HistogramGraph.svelte b/ts/src/stats/HistogramGraph.svelte
index ac9c635a1..062bf8e1f 100644
--- a/ts/src/stats/HistogramGraph.svelte
+++ b/ts/src/stats/HistogramGraph.svelte
@@ -2,15 +2,16 @@
import { HistogramData, histogramGraph } from "./histogram-graph";
import AxisTicks from "./AxisTicks.svelte";
import { defaultGraphBounds } from "./graphs";
+ import NoDataOverlay from "./NoDataOverlay.svelte";
+ import { I18n } from "../i18n";
export let data: HistogramData | null = null;
+ export let i18n: I18n;
let bounds = defaultGraphBounds();
let svg = null as HTMLElement | SVGElement | null;
- $: if (data) {
- histogramGraph(svg as SVGElement, bounds, data);
- }
+ $: histogramGraph(svg as SVGElement, bounds, data);
diff --git a/ts/src/stats/HourGraph.svelte b/ts/src/stats/HourGraph.svelte
index 34031e1fc..f86a4be17 100644
--- a/ts/src/stats/HourGraph.svelte
+++ b/ts/src/stats/HourGraph.svelte
@@ -4,6 +4,7 @@
import { gatherData, GraphData, renderHours } from "./hours";
import pb from "../backend/proto";
import { I18n } from "../i18n";
+ import NoDataOverlay from "./NoDataOverlay.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n;
@@ -30,5 +31,6 @@
+
diff --git a/ts/src/stats/IntervalsGraph.svelte b/ts/src/stats/IntervalsGraph.svelte
index c9a20d724..9b83026c9 100644
--- a/ts/src/stats/IntervalsGraph.svelte
+++ b/ts/src/stats/IntervalsGraph.svelte
@@ -34,44 +34,36 @@
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
-{#if histogramData}
-
-
{title}
+
diff --git a/ts/src/stats/NoDataOverlay.svelte b/ts/src/stats/NoDataOverlay.svelte
new file mode 100644
index 000000000..76d6b0613
--- /dev/null
+++ b/ts/src/stats/NoDataOverlay.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {noData}
+
+
diff --git a/ts/src/stats/ReviewsGraph.svelte b/ts/src/stats/ReviewsGraph.svelte
index 211c8d964..f8e1b679b 100644
--- a/ts/src/stats/ReviewsGraph.svelte
+++ b/ts/src/stats/ReviewsGraph.svelte
@@ -6,6 +6,7 @@
import pb from "../backend/proto";
import { timeSpan, MONTH, YEAR } from "../time";
import { I18n } from "../i18n";
+ import NoDataOverlay from "./NoDataOverlay.svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let revlogRange: RevlogRange = RevlogRange.Month;
@@ -93,6 +94,7 @@
+
diff --git a/ts/src/stats/added.ts b/ts/src/stats/added.ts
index c8c9c55bf..555d3092b 100644
--- a/ts/src/stats/added.ts
+++ b/ts/src/stats/added.ts
@@ -7,7 +7,7 @@
*/
import pb from "../backend/proto";
-import { extent, histogram } from "d3-array";
+import { extent, histogram, sum } from "d3-array";
import { scaleLinear, scaleSequential } from "d3-scale";
import { HistogramData } from "./histogram-graph";
import { interpolateBlues } from "d3-scale-chromatic";
@@ -69,6 +69,11 @@ export function buildHistogram(
.domain(scale.domain() as any)
.thresholds(scale.ticks(desiredBars))(data.daysAdded);
+ // empty graph?
+ if (!sum(bins, (bin) => bin.length)) {
+ return null;
+ }
+
const colourScale = scaleSequential(interpolateBlues).domain([xMin!, xMax]);
function hoverText(
diff --git a/ts/src/stats/buttons.ts b/ts/src/stats/buttons.ts
index e7ce57d90..e0bc18444 100644
--- a/ts/src/stats/buttons.ts
+++ b/ts/src/stats/buttons.ts
@@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip";
-import { GraphBounds } from "./graphs";
+import { GraphBounds, setDataAvailable } from "./graphs";
import { I18n } from "../i18n";
type ButtonCounts = [number, number, number, number];
@@ -103,6 +103,13 @@ export function renderButtons(
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
+ if (!yMax) {
+ setDataAvailable(svg, false);
+ return;
+ } else {
+ setDataAvailable(svg, true);
+ }
+
const xGroup = scaleBand()
.domain(["learning", "young", "mature"])
.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
diff --git a/ts/src/stats/calendar.ts b/ts/src/stats/calendar.ts
index 52c985b1c..7b6e4d7f6 100644
--- a/ts/src/stats/calendar.ts
+++ b/ts/src/stats/calendar.ts
@@ -12,7 +12,7 @@ import "d3-transition";
import { select, mouse } from "d3-selection";
import { scaleLinear, scaleSequential } from "d3-scale";
import { showTooltip, hideTooltip } from "./tooltip";
-import { GraphBounds } from "./graphs";
+import { GraphBounds, setDataAvailable } from "./graphs";
import { timeDay, timeYear, timeWeek } from "d3-time";
import { I18n } from "../i18n";
@@ -80,6 +80,14 @@ export function renderCalendar(
maxCount = count;
}
}
+
+ if (!maxCount) {
+ setDataAvailable(svg, false);
+ return;
+ } else {
+ setDataAvailable(svg, true);
+ }
+
// fill in any blanks
const startDate = timeYear(nowForYear);
for (let i = 0; i < 366; i++) {
diff --git a/ts/src/stats/card-counts.ts b/ts/src/stats/card-counts.ts
index 1926674dd..ed4ed0055 100644
--- a/ts/src/stats/card-counts.ts
+++ b/ts/src/stats/card-counts.ts
@@ -94,12 +94,14 @@ export function renderCards(
total: n,
};
});
- const xMax = summed.slice(-1)[0];
+ // ensuring a non-zero range makes a better animation
+ // in the empty data case
+ const xMax = Math.max(1, summed.slice(-1)[0]);
const x = scaleLinear().domain([0, xMax]);
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
- x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
+ x.range([bounds.marginLeft, bounds.width - bounds.marginRight - bounds.marginLeft]);
const tooltipText = (d: any): string => {
const pct = ((d.count[1] / xMax) * 100).toFixed(2);
@@ -113,7 +115,7 @@ export function renderCards(
showTooltip(tooltipText(d), x, y);
})
.transition(trans)
- .attr("width", (d) => x(d.total) - bounds.marginLeft);
+ .attr("width", (d) => x(d.total));
};
data.reverse();
diff --git a/ts/src/stats/future-due.ts b/ts/src/stats/future-due.ts
index 58a18dbac..2e501bf3c 100644
--- a/ts/src/stats/future-due.ts
+++ b/ts/src/stats/future-due.ts
@@ -88,6 +88,11 @@ export function buildHistogram(
.domain(x.domain() as any)
.thresholds(x.ticks(desiredBars))(data.entries() as any);
+ // empty graph?
+ if (!sum(bins, (bin) => bin.length)) {
+ return null;
+ }
+
const adjustedRange = scaleLinear().range([0.8, 0.3]);
const colourScale = scaleSequential((n) =>
interpolateGreens(adjustedRange(n))
diff --git a/ts/src/stats/graphs.scss b/ts/src/stats/graphs.scss
index 8a95c9b64..8b7fb9362 100644
--- a/ts/src/stats/graphs.scss
+++ b/ts/src/stats/graphs.scss
@@ -126,6 +126,16 @@ body.night-mode {
color: $night-fg;
}
+.no-data {
+ text {
+ text-anchor: middle;
+ fill: grey;
+ }
+ rect {
+ fill: $day-bg;
+ }
+}
+
.night-mode {
.graph-tooltip {
background: $night-bg;
@@ -139,6 +149,9 @@ body.night-mode {
fill: $night-fg;
opacity: 0.1;
}
+ .no-data rect {
+ fill: $night-bg;
+ }
}
.centered {
diff --git a/ts/src/stats/graphs.ts b/ts/src/stats/graphs.ts
index 9a8896b28..5b7258b0e 100644
--- a/ts/src/stats/graphs.ts
+++ b/ts/src/stats/graphs.ts
@@ -7,6 +7,7 @@
@typescript-eslint/ban-ts-ignore: "off" */
import pb from "../backend/proto";
+import { Selection } from "d3-selection";
async function fetchData(search: string, days: number): Promise {
const resp = await fetch("/_anki/graphData", {
@@ -66,3 +67,14 @@ export function defaultGraphBounds(): GraphBounds {
marginBottom: 40,
};
}
+
+export function setDataAvailable(
+ svg: Selection,
+ available: boolean
+): void {
+ svg.select(".no-data")
+ .attr("pointer-events", available ? "none" : "all")
+ .transition()
+ .duration(600)
+ .attr("opacity", available ? 0 : 1);
+}
diff --git a/ts/src/stats/histogram-graph.ts b/ts/src/stats/histogram-graph.ts
index 86806df44..9cfeb0632 100644
--- a/ts/src/stats/histogram-graph.ts
+++ b/ts/src/stats/histogram-graph.ts
@@ -13,7 +13,7 @@ import { scaleLinear, ScaleLinear, ScaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { area, curveBasis } from "d3-shape";
import { showTooltip, hideTooltip } from "./tooltip";
-import { GraphBounds } from "./graphs";
+import { GraphBounds, setDataAvailable } from "./graphs";
export interface HistogramData {
scale: ScaleLinear;
@@ -33,13 +33,20 @@ export interface HistogramData {
export function histogramGraph(
svgElem: SVGElement,
bounds: GraphBounds,
- data: HistogramData
+ data: HistogramData | null
): void {
- const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
-
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
+ if (!data) {
+ setDataAvailable(svg, false);
+ return;
+ } else {
+ setDataAvailable(svg, true);
+ }
+
+ const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
+
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
svg.select(".x-ticks")
.transition(trans)
diff --git a/ts/src/stats/hours.ts b/ts/src/stats/hours.ts
index e2b2d903d..0fcb61c45 100644
--- a/ts/src/stats/hours.ts
+++ b/ts/src/stats/hours.ts
@@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip";
-import { GraphBounds } from "./graphs";
+import { GraphBounds, setDataAvailable } from "./graphs";
import { area, curveBasis } from "d3-shape";
import { I18n } from "../i18n";
@@ -66,6 +66,13 @@ export function renderHours(
const svg = select(svgElem);
const trans = svg.transition().duration(600) as any;
+ if (!yMax) {
+ setDataAvailable(svg, false);
+ return;
+ } else {
+ setDataAvailable(svg, true);
+ }
+
const x = scaleBand()
.domain(data.map((d) => d.hour.toString()))
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
diff --git a/ts/src/stats/intervals.ts b/ts/src/stats/intervals.ts
index 8ec794a00..9c8d54ba7 100644
--- a/ts/src/stats/intervals.ts
+++ b/ts/src/stats/intervals.ts
@@ -7,7 +7,7 @@
*/
import pb from "../backend/proto";
-import { extent, histogram, quantile } from "d3-array";
+import { extent, histogram, quantile, sum } from "d3-array";
import { scaleLinear, scaleSequential } from "d3-scale";
import { CardQueue } from "../cards";
import { HistogramData } from "./histogram-graph";
@@ -98,6 +98,11 @@ export function prepareIntervalData(
.domain(scale.domain() as any)
.thresholds(scale.ticks(desiredBars))(allIntervals);
+ // empty graph?
+ if (!sum(bins, (bin) => bin.length)) {
+ return null;
+ }
+
// start slightly darker
const shiftedMin = xMin! - Math.round((xMax - xMin!) / 10);
const colourScale = scaleSequential(interpolateBlues).domain([shiftedMin, xMax]);
diff --git a/ts/src/stats/reviews.ts b/ts/src/stats/reviews.ts
index 6ce9bc790..fdfb67818 100644
--- a/ts/src/stats/reviews.ts
+++ b/ts/src/stats/reviews.ts
@@ -18,7 +18,7 @@ import { select, mouse } from "d3-selection";
import { scaleLinear, scaleSequential } from "d3-scale";
import { axisBottom, axisLeft } from "d3-axis";
import { showTooltip, hideTooltip } from "./tooltip";
-import { GraphBounds } from "./graphs";
+import { GraphBounds, setDataAvailable } from "./graphs";
import { area, curveBasis } from "d3-shape";
import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
import { timeSpan, dayLabel } from "../time";
@@ -116,6 +116,9 @@ export function renderReviews(
showTime: boolean,
i18n: I18n
): void {
+ const svg = select(svgElem);
+ const trans = svg.transition().duration(600) as any;
+
const xMax = 1;
let xMin = 0;
// cap max to selected range
@@ -144,8 +147,13 @@ export function renderReviews(
.domain(x.domain() as any)
.thresholds(x.ticks(desiredBars))(sourceMap.entries() as any);
- const svg = select(svgElem);
- const trans = svg.transition().duration(600) as any;
+ // empty graph?
+ if (!sum(bins, (bin) => bin.length)) {
+ setDataAvailable(svg, false);
+ return;
+ } else {
+ setDataAvailable(svg, true);
+ }
x.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
svg.select(".x-ticks")