mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
add 'no data' overlay when graph empty
This commit is contained in:
parent
19541c4a9d
commit
0d287330c3
24 changed files with 244 additions and 150 deletions
|
@ -157,3 +157,5 @@ statistics-hours-range = From { $hourStart }:00~{ $hourEnd }:00
|
||||||
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
|
statistics-hours-correct = { $correct }/{ $total } correct ({ $percent }%)
|
||||||
statistics-hours-title = Hourly Breakdown
|
statistics-hours-title = Hourly Breakdown
|
||||||
statistics-hours-subtitle = Review success rate for each hour of the day.
|
statistics-hours-subtitle = Review success rate for each hour of the day.
|
||||||
|
# shown when graph is empty
|
||||||
|
statistics-no-data = NO DATA
|
||||||
|
|
|
@ -44,31 +44,29 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_ADDED_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if histogramData}
|
<div class="graph">
|
||||||
<div class="graph">
|
<h1>{title}</h1>
|
||||||
<h1>{title}</h1>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
<div class="range-box-inner">
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={AddedRange.Month} />
|
<input type="radio" bind:group={range} value={AddedRange.Month} />
|
||||||
{month}
|
{month}
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={AddedRange.Quarter} />
|
<input type="radio" bind:group={range} value={AddedRange.Quarter} />
|
||||||
{month3}
|
{month3}
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={AddedRange.Year} />
|
<input type="radio" bind:group={range} value={AddedRange.Year} />
|
||||||
{year}
|
{year}
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
|
<input type="radio" bind:group={range} value={AddedRange.AllTime} />
|
||||||
{all}
|
{all}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<div class="subtitle">{subtitle}</div>
|
||||||
|
|
||||||
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { gatherData, GraphData, renderButtons } from "./buttons";
|
import { gatherData, GraphData, renderButtons } from "./buttons";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -29,5 +30,6 @@
|
||||||
<g class="bars" />
|
<g class="bars" />
|
||||||
<g class="hoverzone" />
|
<g class="hoverzone" />
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import ReviewsGraph from "./ReviewsGraph.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
|
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds, RevlogRange } from "./graphs";
|
import { defaultGraphBounds, RevlogRange } from "./graphs";
|
||||||
import { GraphData, gatherData, renderCalendar, ReviewRange } from "./calendar";
|
import { GraphData, gatherData, renderCalendar } from "./calendar";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { timeSpan, MONTH, YEAR } from "../time";
|
import { timeSpan, MONTH, YEAR } from "../time";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
@ -76,6 +75,7 @@
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
<g class="days" />
|
<g class="days" />
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
@ -15,27 +15,31 @@
|
||||||
bounds.marginRight = 20;
|
bounds.marginRight = 20;
|
||||||
bounds.marginTop = 0;
|
bounds.marginTop = 0;
|
||||||
|
|
||||||
let graphData: GraphData | null = null;
|
let graphData: GraphData;
|
||||||
$: if (sourceData) {
|
$: {
|
||||||
graphData = gatherData(sourceData, i18n);
|
graphData = gatherData(sourceData, i18n);
|
||||||
}
|
|
||||||
|
|
||||||
$: if (graphData) {
|
|
||||||
renderCards(svg as any, bounds, graphData);
|
renderCards(svg as any, bounds, graphData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
const total = i18n.tr(i18n.TR.STATISTICS_COUNTS_TOTAL_CARDS);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if graphData}
|
<style>
|
||||||
<div class="graph">
|
svg {
|
||||||
<h1>{graphData.title}</h1>
|
transition: opacity 1s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<div class="graph">
|
||||||
<g class="days" />
|
<h1>{graphData.title}</h1>
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div class="centered">{total}: {graphData.totalCards}</div>
|
<svg
|
||||||
|
bind:this={svg}
|
||||||
|
viewBox={`0 0 ${bounds.width} ${bounds.height}`}
|
||||||
|
style="opacity: {graphData.totalCards ? 1 : 0}">
|
||||||
|
<g class="days" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
</div>
|
<div class="centered">{total}: {graphData.totalCards}</div>
|
||||||
{/if}
|
|
||||||
|
</div>
|
||||||
|
|
|
@ -19,12 +19,10 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if histogramData}
|
<div class="graph">
|
||||||
<div class="graph">
|
<h1>{title}</h1>
|
||||||
<h1>{title}</h1>
|
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
<div class="subtitle">{subtitle}</div>
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
|
@ -52,38 +52,35 @@
|
||||||
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
const backlogLabel = i18n.tr(i18n.TR.STATISTICS_BACKLOG_CHECKBOX);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if histogramData}
|
<div class="graph">
|
||||||
|
<h1>{title}</h1>
|
||||||
|
|
||||||
<div class="graph">
|
<div class="range-box-inner">
|
||||||
<h1>{title}</h1>
|
<label>
|
||||||
|
<input type="checkbox" bind:checked={backlog} />
|
||||||
<div class="range-box-inner">
|
{backlogLabel}
|
||||||
<label>
|
</label>
|
||||||
<input type="checkbox" bind:checked={backlog} />
|
|
||||||
{backlogLabel}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={range} value={FutureDueRange.Month} />
|
|
||||||
{month}
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={range} value={FutureDueRange.Quarter} />
|
|
||||||
{month3}
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={range} value={FutureDueRange.Year} />
|
|
||||||
{year}
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={range} value={FutureDueRange.AllTime} />
|
|
||||||
{all}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={FutureDueRange.Month} />
|
||||||
|
{month}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={FutureDueRange.Quarter} />
|
||||||
|
{month3}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={FutureDueRange.Year} />
|
||||||
|
{year}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" bind:group={range} value={FutureDueRange.AllTime} />
|
||||||
|
{all}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<div class="subtitle">{subtitle}</div>
|
||||||
|
|
||||||
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
|
@ -141,13 +141,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="range-box-pad" />
|
<div class="range-box-pad" />
|
||||||
|
|
||||||
<TodayStats {sourceData} {i18n} />
|
{#if sourceData}
|
||||||
<CardCounts {sourceData} {i18n} />
|
<TodayStats {sourceData} {i18n} />
|
||||||
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
|
<CardCounts {sourceData} {i18n} />
|
||||||
<FutureDue {sourceData} {revlogRange} {i18n} />
|
<CalendarGraph {sourceData} {revlogRange} {i18n} {nightMode} />
|
||||||
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
|
<FutureDue {sourceData} {revlogRange} {i18n} />
|
||||||
<IntervalsGraph {sourceData} {i18n} />
|
<ReviewsGraph {sourceData} {revlogRange} {i18n} />
|
||||||
<EaseGraph {sourceData} {i18n} />
|
<IntervalsGraph {sourceData} {i18n} />
|
||||||
<HourGraph {sourceData} {i18n} />
|
<EaseGraph {sourceData} {i18n} />
|
||||||
<ButtonsGraph {sourceData} {i18n} />
|
<HourGraph {sourceData} {i18n} />
|
||||||
<AddedGraph {sourceData} {revlogRange} {i18n} />
|
<ButtonsGraph {sourceData} {i18n} />
|
||||||
|
<AddedGraph {sourceData} {revlogRange} {i18n} />
|
||||||
|
{/if}
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
import { HistogramData, histogramGraph } from "./histogram-graph";
|
import { HistogramData, histogramGraph } from "./histogram-graph";
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds } from "./graphs";
|
import { defaultGraphBounds } from "./graphs";
|
||||||
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
export let data: HistogramData | null = null;
|
export let data: HistogramData | null = null;
|
||||||
|
export let i18n: I18n;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
|
||||||
$: if (data) {
|
$: histogramGraph(svg as SVGElement, bounds, data);
|
||||||
histogramGraph(svg as SVGElement, bounds, data);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
<svg bind:this={svg} viewBox={`0 0 ${bounds.width} ${bounds.height}`}>
|
||||||
|
@ -18,4 +19,5 @@
|
||||||
<g class="hoverzone" />
|
<g class="hoverzone" />
|
||||||
<path class="area" />
|
<path class="area" />
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { gatherData, GraphData, renderHours } from "./hours";
|
import { gatherData, GraphData, renderHours } from "./hours";
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
@ -30,5 +31,6 @@
|
||||||
<path class="area" />
|
<path class="area" />
|
||||||
<g class="hoverzone" />
|
<g class="hoverzone" />
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,44 +34,36 @@
|
||||||
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
const subtitle = i18n.tr(i18n.TR.STATISTICS_INTERVALS_SUBTITLE);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if histogramData}
|
<div class="graph intervals">
|
||||||
<div class="graph intervals">
|
<h1>{title}</h1>
|
||||||
<h1>{title}</h1>
|
|
||||||
|
|
||||||
<div class="range-box-inner">
|
<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}
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<input
|
<input type="radio" bind:group={range} value={IntervalRange.Percentile50} />
|
||||||
type="radio"
|
50%
|
||||||
bind:group={range}
|
</label>
|
||||||
value={IntervalRange.Percentile50} />
|
<label>
|
||||||
50%
|
<input type="radio" bind:group={range} value={IntervalRange.Percentile95} />
|
||||||
</label>
|
95%
|
||||||
<label>
|
</label>
|
||||||
<input
|
<label>
|
||||||
type="radio"
|
<input
|
||||||
bind:group={range}
|
type="radio"
|
||||||
value={IntervalRange.Percentile95} />
|
bind:group={range}
|
||||||
95%
|
value={IntervalRange.Percentile999} />
|
||||||
</label>
|
99.9%
|
||||||
<label>
|
</label>
|
||||||
<input
|
<label>
|
||||||
type="radio"
|
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
||||||
bind:group={range}
|
{all}
|
||||||
value={IntervalRange.Percentile999} />
|
</label>
|
||||||
99.9%
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<input type="radio" bind:group={range} value={IntervalRange.All} />
|
|
||||||
{all}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="subtitle">{subtitle}</div>
|
|
||||||
|
|
||||||
<HistogramGraph data={histogramData} />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
<div class="subtitle">{subtitle}</div>
|
||||||
|
|
||||||
|
<HistogramGraph data={histogramData} {i18n} />
|
||||||
|
</div>
|
||||||
|
|
14
ts/src/stats/NoDataOverlay.svelte
Normal file
14
ts/src/stats/NoDataOverlay.svelte
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="typescript">
|
||||||
|
import { I18n } from "../i18n";
|
||||||
|
import { GraphBounds } from "./graphs";
|
||||||
|
export let bounds: GraphBounds;
|
||||||
|
export let i18n: I18n;
|
||||||
|
const noData = i18n.tr(i18n.TR.STATISTICS_NO_DATA);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<g class="no-data">
|
||||||
|
<rect x="0" y="0" width={bounds.width} height={bounds.height} />
|
||||||
|
<text x="{bounds.width / 2}," y={bounds.height / 2} letter-spacing="3">
|
||||||
|
{noData}
|
||||||
|
</text>
|
||||||
|
</g>
|
|
@ -6,6 +6,7 @@
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
import { timeSpan, MONTH, YEAR } from "../time";
|
import { timeSpan, MONTH, YEAR } from "../time";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let revlogRange: RevlogRange = RevlogRange.Month;
|
export let revlogRange: RevlogRange = RevlogRange.Month;
|
||||||
|
@ -93,6 +94,7 @@
|
||||||
<path class="area" />
|
<path class="area" />
|
||||||
<g class="hoverzone" />
|
<g class="hoverzone" />
|
||||||
<AxisTicks {bounds} />
|
<AxisTicks {bounds} />
|
||||||
|
<NoDataOverlay {bounds} {i18n} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
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 { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
import { HistogramData } from "./histogram-graph";
|
import { HistogramData } from "./histogram-graph";
|
||||||
import { interpolateBlues } from "d3-scale-chromatic";
|
import { interpolateBlues } from "d3-scale-chromatic";
|
||||||
|
@ -69,6 +69,11 @@ export function buildHistogram(
|
||||||
.domain(scale.domain() as any)
|
.domain(scale.domain() as any)
|
||||||
.thresholds(scale.ticks(desiredBars))(data.daysAdded);
|
.thresholds(scale.ticks(desiredBars))(data.daysAdded);
|
||||||
|
|
||||||
|
// empty graph?
|
||||||
|
if (!sum(bins, (bin) => bin.length)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const colourScale = scaleSequential(interpolateBlues).domain([xMin!, xMax]);
|
const colourScale = scaleSequential(interpolateBlues).domain([xMin!, xMax]);
|
||||||
|
|
||||||
function hoverText(
|
function hoverText(
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
|
||||||
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
||||||
import { axisBottom, axisLeft } from "d3-axis";
|
import { axisBottom, axisLeft } from "d3-axis";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds } from "./graphs";
|
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
type ButtonCounts = [number, number, number, number];
|
type ButtonCounts = [number, number, number, number];
|
||||||
|
@ -103,6 +103,13 @@ export function renderButtons(
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const trans = svg.transition().duration(600) as any;
|
const trans = svg.transition().duration(600) as any;
|
||||||
|
|
||||||
|
if (!yMax) {
|
||||||
|
setDataAvailable(svg, false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setDataAvailable(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
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]);
|
||||||
|
|
|
@ -12,7 +12,7 @@ import "d3-transition";
|
||||||
import { select, mouse } from "d3-selection";
|
import { select, mouse } from "d3-selection";
|
||||||
import { scaleLinear, scaleSequential } from "d3-scale";
|
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds } from "./graphs";
|
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||||
import { timeDay, timeYear, timeWeek } from "d3-time";
|
import { timeDay, timeYear, timeWeek } from "d3-time";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
|
@ -80,6 +80,14 @@ export function renderCalendar(
|
||||||
maxCount = count;
|
maxCount = count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!maxCount) {
|
||||||
|
setDataAvailable(svg, false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setDataAvailable(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
// fill in any blanks
|
// fill in any blanks
|
||||||
const startDate = timeYear(nowForYear);
|
const startDate = timeYear(nowForYear);
|
||||||
for (let i = 0; i < 366; i++) {
|
for (let i = 0; i < 366; i++) {
|
||||||
|
|
|
@ -94,12 +94,14 @@ export function renderCards(
|
||||||
total: n,
|
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 x = scaleLinear().domain([0, xMax]);
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const trans = svg.transition().duration(600) as any;
|
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 tooltipText = (d: any): string => {
|
||||||
const pct = ((d.count[1] / xMax) * 100).toFixed(2);
|
const pct = ((d.count[1] / xMax) * 100).toFixed(2);
|
||||||
|
@ -113,7 +115,7 @@ export function renderCards(
|
||||||
showTooltip(tooltipText(d), x, y);
|
showTooltip(tooltipText(d), x, y);
|
||||||
})
|
})
|
||||||
.transition(trans)
|
.transition(trans)
|
||||||
.attr("width", (d) => x(d.total) - bounds.marginLeft);
|
.attr("width", (d) => x(d.total));
|
||||||
};
|
};
|
||||||
|
|
||||||
data.reverse();
|
data.reverse();
|
||||||
|
|
|
@ -88,6 +88,11 @@ export function buildHistogram(
|
||||||
.domain(x.domain() as any)
|
.domain(x.domain() as any)
|
||||||
.thresholds(x.ticks(desiredBars))(data.entries() 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 adjustedRange = scaleLinear().range([0.8, 0.3]);
|
||||||
const colourScale = scaleSequential((n) =>
|
const colourScale = scaleSequential((n) =>
|
||||||
interpolateGreens(adjustedRange(n))
|
interpolateGreens(adjustedRange(n))
|
||||||
|
|
|
@ -126,6 +126,16 @@ body.night-mode {
|
||||||
color: $night-fg;
|
color: $night-fg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text {
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: grey;
|
||||||
|
}
|
||||||
|
rect {
|
||||||
|
fill: $day-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.night-mode {
|
.night-mode {
|
||||||
.graph-tooltip {
|
.graph-tooltip {
|
||||||
background: $night-bg;
|
background: $night-bg;
|
||||||
|
@ -139,6 +149,9 @@ body.night-mode {
|
||||||
fill: $night-fg;
|
fill: $night-fg;
|
||||||
opacity: 0.1;
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
.no-data rect {
|
||||||
|
fill: $night-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
@typescript-eslint/ban-ts-ignore: "off" */
|
@typescript-eslint/ban-ts-ignore: "off" */
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
import pb from "../backend/proto";
|
||||||
|
import { Selection } from "d3-selection";
|
||||||
|
|
||||||
async function fetchData(search: string, days: number): Promise<Uint8Array> {
|
async function fetchData(search: string, days: number): Promise<Uint8Array> {
|
||||||
const resp = await fetch("/_anki/graphData", {
|
const resp = await fetch("/_anki/graphData", {
|
||||||
|
@ -66,3 +67,14 @@ export function defaultGraphBounds(): GraphBounds {
|
||||||
marginBottom: 40,
|
marginBottom: 40,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setDataAvailable(
|
||||||
|
svg: Selection<SVGElement, any, any, any>,
|
||||||
|
available: boolean
|
||||||
|
): void {
|
||||||
|
svg.select(".no-data")
|
||||||
|
.attr("pointer-events", available ? "none" : "all")
|
||||||
|
.transition()
|
||||||
|
.duration(600)
|
||||||
|
.attr("opacity", available ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { scaleLinear, ScaleLinear, ScaleSequential } from "d3-scale";
|
||||||
import { axisBottom, axisLeft } from "d3-axis";
|
import { axisBottom, axisLeft } from "d3-axis";
|
||||||
import { area, curveBasis } from "d3-shape";
|
import { area, curveBasis } from "d3-shape";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds } from "./graphs";
|
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||||
|
|
||||||
export interface HistogramData {
|
export interface HistogramData {
|
||||||
scale: ScaleLinear<number, number>;
|
scale: ScaleLinear<number, number>;
|
||||||
|
@ -33,13 +33,20 @@ export interface HistogramData {
|
||||||
export function histogramGraph(
|
export function histogramGraph(
|
||||||
svgElem: SVGElement,
|
svgElem: SVGElement,
|
||||||
bounds: GraphBounds,
|
bounds: GraphBounds,
|
||||||
data: HistogramData
|
data: HistogramData | null
|
||||||
): void {
|
): void {
|
||||||
const binValue = data.binValue ?? ((bin: any): number => bin.length as number);
|
|
||||||
|
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const trans = svg.transition().duration(600) as any;
|
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]);
|
const x = data.scale.range([bounds.marginLeft, bounds.width - bounds.marginRight]);
|
||||||
svg.select<SVGGElement>(".x-ticks")
|
svg.select<SVGGElement>(".x-ticks")
|
||||||
.transition(trans)
|
.transition(trans)
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { select, mouse } from "d3-selection";
|
||||||
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
import { scaleLinear, scaleBand, scaleSequential } from "d3-scale";
|
||||||
import { axisBottom, axisLeft } from "d3-axis";
|
import { axisBottom, axisLeft } from "d3-axis";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds } from "./graphs";
|
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||||
import { area, curveBasis } from "d3-shape";
|
import { area, curveBasis } from "d3-shape";
|
||||||
import { I18n } from "../i18n";
|
import { I18n } from "../i18n";
|
||||||
|
|
||||||
|
@ -66,6 +66,13 @@ export function renderHours(
|
||||||
const svg = select(svgElem);
|
const svg = select(svgElem);
|
||||||
const trans = svg.transition().duration(600) as any;
|
const trans = svg.transition().duration(600) as any;
|
||||||
|
|
||||||
|
if (!yMax) {
|
||||||
|
setDataAvailable(svg, false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setDataAvailable(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
const x = scaleBand()
|
const x = scaleBand()
|
||||||
.domain(data.map((d) => d.hour.toString()))
|
.domain(data.map((d) => d.hour.toString()))
|
||||||
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
.range([bounds.marginLeft, bounds.width - bounds.marginRight])
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import pb from "../backend/proto";
|
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 { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
import { CardQueue } from "../cards";
|
import { CardQueue } from "../cards";
|
||||||
import { HistogramData } from "./histogram-graph";
|
import { HistogramData } from "./histogram-graph";
|
||||||
|
@ -98,6 +98,11 @@ export function prepareIntervalData(
|
||||||
.domain(scale.domain() as any)
|
.domain(scale.domain() as any)
|
||||||
.thresholds(scale.ticks(desiredBars))(allIntervals);
|
.thresholds(scale.ticks(desiredBars))(allIntervals);
|
||||||
|
|
||||||
|
// empty graph?
|
||||||
|
if (!sum(bins, (bin) => bin.length)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// start slightly darker
|
// start slightly darker
|
||||||
const shiftedMin = xMin! - Math.round((xMax - xMin!) / 10);
|
const shiftedMin = xMin! - Math.round((xMax - xMin!) / 10);
|
||||||
const colourScale = scaleSequential(interpolateBlues).domain([shiftedMin, xMax]);
|
const colourScale = scaleSequential(interpolateBlues).domain([shiftedMin, xMax]);
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { select, mouse } from "d3-selection";
|
||||||
import { scaleLinear, scaleSequential } from "d3-scale";
|
import { scaleLinear, scaleSequential } from "d3-scale";
|
||||||
import { axisBottom, axisLeft } from "d3-axis";
|
import { axisBottom, axisLeft } from "d3-axis";
|
||||||
import { showTooltip, hideTooltip } from "./tooltip";
|
import { showTooltip, hideTooltip } from "./tooltip";
|
||||||
import { GraphBounds } from "./graphs";
|
import { GraphBounds, setDataAvailable } from "./graphs";
|
||||||
import { area, curveBasis } from "d3-shape";
|
import { area, curveBasis } from "d3-shape";
|
||||||
import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
|
import { min, histogram, sum, max, Bin, cumsum } from "d3-array";
|
||||||
import { timeSpan, dayLabel } from "../time";
|
import { timeSpan, dayLabel } from "../time";
|
||||||
|
@ -116,6 +116,9 @@ export function renderReviews(
|
||||||
showTime: boolean,
|
showTime: boolean,
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
): void {
|
): void {
|
||||||
|
const svg = select(svgElem);
|
||||||
|
const trans = svg.transition().duration(600) as any;
|
||||||
|
|
||||||
const xMax = 1;
|
const xMax = 1;
|
||||||
let xMin = 0;
|
let xMin = 0;
|
||||||
// cap max to selected range
|
// cap max to selected range
|
||||||
|
@ -144,8 +147,13 @@ export function renderReviews(
|
||||||
.domain(x.domain() as any)
|
.domain(x.domain() as any)
|
||||||
.thresholds(x.ticks(desiredBars))(sourceMap.entries() as any);
|
.thresholds(x.ticks(desiredBars))(sourceMap.entries() as any);
|
||||||
|
|
||||||
const svg = select(svgElem);
|
// empty graph?
|
||||||
const trans = svg.transition().duration(600) as any;
|
if (!sum(bins, (bin) => bin.length)) {
|
||||||
|
setDataAvailable(svg, false);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setDataAvailable(svg, true);
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
|
|
Loading…
Reference in a new issue