Move dispatch logic from Histogram to individual graphs

This commit is contained in:
Henrik Giesel 2021-01-25 16:33:18 +01:00
parent f767a5e6ca
commit 759ed17963
14 changed files with 98 additions and 71 deletions

View file

@ -65,7 +65,7 @@ class DeckBrowser:
def _linkHandler(self, url): def _linkHandler(self, url):
if ":" in url: if ":" in url:
(cmd, arg) = url.split(":") (cmd, arg) = url.split(":", 1)
else: else:
cmd = url cmd = url
if cmd == "open": if cmd == "open":

View file

@ -1092,7 +1092,6 @@ message GraphsOut {
uint32 scheduler_version = 5; uint32 scheduler_version = 5;
/// Seconds to add to UTC timestamps to get local time. /// Seconds to add to UTC timestamps to get local time.
int32 local_offset_secs = 7; int32 local_offset_secs = 7;
bool bridge_commands_supported = 8;
} }
message GraphPreferences { message GraphPreferences {
@ -1104,6 +1103,7 @@ message GraphPreferences {
} }
Weekday calendar_first_day_of_week = 1; Weekday calendar_first_day_of_week = 1;
bool card_counts_separate_inactive = 2; bool card_counts_separate_inactive = 2;
bool browser_links_supported = 3;
} }
message RevlogEntry { message RevlogEntry {

View file

@ -44,7 +44,6 @@ impl Collection {
next_day_at_secs: timing.next_day_at as u32, next_day_at_secs: timing.next_day_at as u32,
scheduler_version: self.sched_ver() as u32, scheduler_version: self.sched_ver() as u32,
local_offset_secs: local_offset_secs as i32, local_offset_secs: local_offset_secs as i32,
bridge_commands_supported: true,
}) })
} }
@ -52,6 +51,7 @@ impl Collection {
Ok(pb::GraphPreferences { Ok(pb::GraphPreferences {
calendar_first_day_of_week: self.get_first_day_of_week() as i32, calendar_first_day_of_week: self.get_first_day_of_week() as i32,
card_counts_separate_inactive: self.get_card_counts_separate_inactive(), card_counts_separate_inactive: self.get_card_counts_separate_inactive(),
browser_links_supported: true,
}) })
} }

View file

@ -9,6 +9,7 @@
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte"; import GraphRangeRadios from "./GraphRangeRadios.svelte";
import TableData from "./TableData.svelte"; import TableData from "./TableData.svelte";
import { createEventDispatcher } from "svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
@ -17,13 +18,15 @@
let tableData: TableDatum[] = []; let tableData: TableDatum[] = [];
let graphRange: GraphRange = GraphRange.Month; let graphRange: GraphRange = GraphRange.Month;
const dispatch = createEventDispatcher();
let addedData: GraphData | null = null; let addedData: GraphData | null = null;
$: if (sourceData) { $: if (sourceData) {
addedData = gatherData(sourceData); addedData = gatherData(sourceData);
} }
$: if (addedData) { $: if (addedData) {
[histogramData, tableData] = buildHistogram(addedData, graphRange, i18n); [histogramData, tableData] = buildHistogram(addedData, graphRange, i18n, dispatch);
} }
const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE);

View file

@ -11,7 +11,7 @@
export let i18n: I18n; export let i18n: I18n;
export let preferences: PreferenceStore; export let preferences: PreferenceStore;
let { cardCountsSeparateInactive } = preferences; let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let svg = null as HTMLElement | SVGElement | null; let svg = null as HTMLElement | SVGElement | null;
@ -89,7 +89,7 @@
<!-- prettier-ignore --> <!-- prettier-ignore -->
<td> <td>
<span style="color: {d.colour};">&nbsp;</span> <span style="color: {d.colour};">&nbsp;</span>
{#if sourceData.bridgeCommandsSupported} {#if browserLinksSupported}
<span class="search-link" on:click={() => dispatch('search', { query: d.query })}>{d.label}</span> <span class="search-link" on:click={() => dispatch('search', { query: d.query })}>{d.label}</span>
{:else} {:else}
<span>{d.label}</span> <span>{d.label}</span>

View file

@ -6,15 +6,18 @@
import type { I18n } from "anki/i18n"; import type { I18n } from "anki/i18n";
import type { TableDatum } from "./graph-helpers"; import type { TableDatum } from "./graph-helpers";
import TableData from "./TableData.svelte"; import TableData from "./TableData.svelte";
import { createEventDispatcher } from "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;
const dispatch = createEventDispatcher();
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
let tableData: TableDatum[] = []; let tableData: TableDatum[] = [];
$: if (sourceData) { $: if (sourceData) {
[histogramData, tableData] = prepareData(gatherData(sourceData), i18n); [histogramData, tableData] = prepareData(gatherData(sourceData), i18n, dispatch);
} }
const title = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_TITLE);

View file

@ -9,10 +9,13 @@
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
import GraphRangeRadios from "./GraphRangeRadios.svelte"; import GraphRangeRadios from "./GraphRangeRadios.svelte";
import TableData from "./TableData.svelte"; import TableData from "./TableData.svelte";
import { createEventDispatcher } from "svelte";
export let sourceData: pb.BackendProto.GraphsOut | null = null; export let sourceData: pb.BackendProto.GraphsOut | null = null;
export let i18n: I18n; export let i18n: I18n;
const dispatch = createEventDispatcher();
let graphData = null as GraphData | null; let graphData = null as GraphData | null;
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
let tableData: TableDatum[] = [] as any; let tableData: TableDatum[] = [] as any;
@ -28,7 +31,8 @@
graphData, graphData,
graphRange, graphRange,
backlog, backlog,
i18n i18n,
dispatch,
)); ));
} }

View file

@ -12,10 +12,13 @@
import HistogramGraph from "./HistogramGraph.svelte"; import HistogramGraph from "./HistogramGraph.svelte";
import type { TableDatum } from "./graph-helpers"; import type { TableDatum } from "./graph-helpers";
import TableData from "./TableData.svelte"; import TableData from "./TableData.svelte";
import { createEventDispatcher } from "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;
const dispatch = createEventDispatcher();
let intervalData: IntervalGraphData | null = null; let intervalData: IntervalGraphData | null = null;
let histogramData = null as HistogramData | null; let histogramData = null as HistogramData | null;
let tableData: TableDatum[] = []; let tableData: TableDatum[] = [];
@ -26,7 +29,7 @@
} }
$: if (intervalData) { $: if (intervalData) {
[histogramData, tableData] = prepareIntervalData(intervalData, range, i18n); [histogramData, tableData] = prepareIntervalData(intervalData, range, i18n, dispatch);
} }
const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE); const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE);

View file

@ -7,7 +7,7 @@
*/ */
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { extent, histogram, sum } from "d3-array"; import { extent, histogram, sum, Bin } from "d3-array";
import { scaleLinear, scaleSequential } from "d3-scale"; import { scaleLinear, scaleSequential } from "d3-scale";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
import { interpolateBlues } from "d3-scale-chromatic"; import { interpolateBlues } from "d3-scale-chromatic";
@ -28,10 +28,22 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
return { daysAdded }; return { daysAdded };
} }
function makeQuery(start: number, end: number): string {
const include = `"added:${start}"`;
if (start === 1) {
return include;
}
const exclude = `-"added:${end}"`;
return `${include} AND ${exclude}`;
}
export function buildHistogram( export function buildHistogram(
data: GraphData, data: GraphData,
range: GraphRange, range: GraphRange,
i18n: I18n i18n: I18n,
dispatch: any,
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
// get min/max // get min/max
const total = data.daysAdded.length; const total = data.daysAdded.length;
@ -102,17 +114,11 @@ export function buildHistogram(
return `${day}:<br>${cards}<br>${total}: ${totalCards}`; return `${day}:<br>${cards}<br>${total}: ${totalCards}`;
} }
function makeQuery(data: HistogramData, binIdx: number): string { function onClick(bin: Bin<number, number>): void {
const start = Math.abs(data.bins[binIdx].x0!) + 1; const start = Math.abs(bin.x0!) + 1;
const include = `"added:${start}"`; const end = Math.abs(bin.x1!) + 1;
const query = makeQuery(start, end);
if (start === 1) { dispatch("search", { query });
return include;
}
const end = Math.abs(data.bins[binIdx].x1!) + 1;
const exclude = `-"added:${end}"`;
return `${include} AND ${exclude}`;
} }
return [ return [
@ -121,7 +127,7 @@ export function buildHistogram(
bins, bins,
total: totalInPeriod, total: totalInPeriod,
hoverText, hoverText,
makeQuery, onClick,
colourScale, colourScale,
showArea: true, showArea: true,
}, },

View file

@ -114,7 +114,6 @@ export function renderCalendar(
maxCount = count; maxCount = count;
} }
} }
console.log("sourceData", sourceData, dayMap);
if (!maxCount) { if (!maxCount) {
setDataAvailable(svg, false); setDataAvailable(svg, false);

View file

@ -7,7 +7,7 @@
*/ */
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { extent, histogram, sum } from "d3-array"; import { extent, histogram, sum, Bin } from "d3-array";
import { scaleLinear, scaleSequential } from "d3-scale"; import { scaleLinear, scaleSequential } from "d3-scale";
import { CardType } from "anki/cards"; import { CardType } from "anki/cards";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
@ -26,9 +26,21 @@ export function gatherData(data: pb.BackendProto.GraphsOut): GraphData {
return { eases }; return { eases };
} }
function makeQuery(start: number, end: number): string {
if (start === end) {
return `"prop:ease=${start / 100}"`;
}
const fromQuery = `"prop:ease>=${start / 100}"`;
const tillQuery = `"prop:ease<${end / 100}"`;
return `${fromQuery} AND ${tillQuery}`;
}
export function prepareData( export function prepareData(
data: GraphData, data: GraphData,
i18n: I18n i18n: I18n,
dispatch: any,
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
// get min/max // get min/max
const allEases = data.eases; const allEases = data.eases;
@ -61,19 +73,11 @@ export function prepareData(
}); });
} }
function makeQuery(data: HistogramData, binIdx: number): string { function onClick(bin: Bin<number, number>): void {
const bin = data.bins[binIdx];
const start = bin.x0!; const start = bin.x0!;
const end = bin.x1! - 1; const end = bin.x1! - 1;
const query = makeQuery(start, end);
if (start === end) { dispatch("search", { query });
return `"prop:ease=${start / 100}"`;
}
const fromQuery = `"prop:ease>=${start / 100}"`;
const tillQuery = `"prop:ease<${end / 100}"`;
return `${fromQuery} AND ${tillQuery}`;
} }
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`; const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
@ -90,7 +94,7 @@ export function prepareData(
bins, bins,
total, total,
hoverText, hoverText,
makeQuery, onClick,
colourScale, colourScale,
showArea: false, showArea: false,
xTickFormat, xTickFormat,

View file

@ -72,11 +72,23 @@ export interface FutureDueOut {
tableData: TableDatum[]; tableData: TableDatum[];
} }
function makeQuery(start: number, end: number): string {
if (start === end) {
return `"prop:due=${start}"`;
}
else {
const fromQuery = `"prop:due>=${start}"`;
const tillQuery = `"prop:due<=${end}"`;
return `${fromQuery} AND ${tillQuery}`;
}
}
export function buildHistogram( export function buildHistogram(
sourceData: GraphData, sourceData: GraphData,
range: GraphRange, range: GraphRange,
backlog: boolean, backlog: boolean,
i18n: I18n i18n: I18n,
dispatch: any,
): FutureDueOut { ): FutureDueOut {
const output = { histogramData: null, tableData: [] }; const output = { histogramData: null, tableData: [] };
// get min/max // get min/max
@ -145,19 +157,11 @@ export function buildHistogram(
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`; return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
} }
function makeQuery(data: HistogramData, binIdx: number): string { function onClick(bin: Bin<number, number>): void {
const bin = data.bins[binIdx];
const start = bin.x0!; const start = bin.x0!;
const end = bin.x1! - 1; const end = bin.x1! - 1;
const query = makeQuery(start, end);
if (start === end) { dispatch("search", { query });
return `"prop:due=${start}"`;
}
const fromQuery = `"prop:due>=${start}"`;
const tillQuery = `"prop:due<=${end}"`;
return `${fromQuery} AND ${tillQuery}`;
} }
const periodDays = xMax! - xMin!; const periodDays = xMax! - xMin!;
@ -186,7 +190,7 @@ export function buildHistogram(
bins, bins,
total, total,
hoverText, hoverText,
makeQuery, onClick,
showArea: true, showArea: true,
colourScale, colourScale,
binValue, binValue,

View file

@ -25,7 +25,7 @@ export interface HistogramData {
cumulative: number, cumulative: number,
percent: number percent: number
) => string; ) => string;
makeQuery?: (data: HistogramData, binIdx: number) => string; onClick?: (data: Bin<number, number>) => void;
showArea: boolean; showArea: boolean;
colourScale: ScaleSequential<string>; colourScale: ScaleSequential<string>;
binValue?: (bin: Bin<any, any>) => number; binValue?: (bin: Bin<any, any>) => number;
@ -36,7 +36,6 @@ export function histogramGraph(
svgElem: SVGElement, svgElem: SVGElement,
bounds: GraphBounds, bounds: GraphBounds,
data: HistogramData | null, data: HistogramData | null,
dispatch: any
): void { ): void {
const svg = select(svgElem); const svg = select(svgElem);
const trans = svg.transition().duration(600) as any; const trans = svg.transition().duration(600) as any;
@ -162,11 +161,9 @@ export function histogramGraph(
}) })
.on("mouseout", hideTooltip); .on("mouseout", hideTooltip);
if (data.makeQuery) { if (data.onClick) {
hoverzone hoverzone
.attr("class", "clickable") .attr("class", "clickable")
.on("click", function (this: any, _d: any, idx: number) { .on("click", data.onClick);
dispatch("search", { query: data.makeQuery!(data, idx) });
});
} }
} }

View file

@ -7,7 +7,7 @@
*/ */
import type pb from "anki/backend_proto"; import type pb from "anki/backend_proto";
import { extent, histogram, quantile, sum, mean } from "d3-array"; import { extent, histogram, quantile, sum, mean, Bin } from "d3-array";
import { scaleLinear, scaleSequential } from "d3-scale"; import { scaleLinear, scaleSequential } from "d3-scale";
import { CardType } from "anki/cards"; import { CardType } from "anki/cards";
import type { HistogramData } from "./histogram-graph"; import type { HistogramData } from "./histogram-graph";
@ -56,10 +56,22 @@ export function intervalLabel(
} }
} }
function makeQuery(start: number, end: number): string {
if (start === end) {
return `"prop:ivl=${start}"`;
}
const fromQuery = `"prop:ivl>=${start}"`;
const tillQuery = `"prop:ivl<=${end}"`;
return `${fromQuery} AND ${tillQuery}`;
}
export function prepareIntervalData( export function prepareIntervalData(
data: IntervalGraphData, data: IntervalGraphData,
range: IntervalRange, range: IntervalRange,
i18n: I18n i18n: I18n,
dispatch: any,
): [HistogramData | null, TableDatum[]] { ): [HistogramData | null, TableDatum[]] {
// get min/max // get min/max
const allIntervals = data.intervals; const allIntervals = data.intervals;
@ -134,19 +146,11 @@ export function prepareIntervalData(
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`; return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
} }
function makeQuery(data: HistogramData, binIdx: number): string { function onClick(bin: Bin<number, number>): void {
const bin = data.bins[binIdx];
const start = bin.x0!; const start = bin.x0!;
const end = bin.x1! - 1; const end = bin.x1! - 1;
const query = makeQuery(start, end);
if (start === end) { dispatch("search", { query });
return `"prop:ivl=${start}"`;
}
const fromQuery = `"prop:ivl>=${start}"`;
const tillQuery = `"prop:ivl<=${end}"`;
return `${fromQuery} AND ${tillQuery}`;
} }
const meanInterval = Math.round(mean(allIntervals) ?? 0); const meanInterval = Math.round(mean(allIntervals) ?? 0);
@ -163,7 +167,7 @@ export function prepareIntervalData(
bins, bins,
total: totalInPeriod, total: totalInPeriod,
hoverText, hoverText,
makeQuery, onClick,
colourScale, colourScale,
showArea: true, showArea: true,
}, },