Merge pull request #897 from hgiesel/statssearch
Triggering searches from the stats screen.
This commit is contained in:
commit
644cc46dea
@ -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":
|
||||||
|
@ -51,6 +51,7 @@ class NewDeckStats(QDialog):
|
|||||||
gui_hooks.stats_dialog_will_show(self)
|
gui_hooks.stats_dialog_will_show(self)
|
||||||
self.show()
|
self.show()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
self.form.web.set_bridge_command(self._on_bridge_cmd, self)
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
@ -89,6 +90,14 @@ class NewDeckStats(QDialog):
|
|||||||
def changeScope(self, type):
|
def changeScope(self, type):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _on_bridge_cmd(self, cmd: str) -> bool:
|
||||||
|
if cmd.startswith("browserSearch"):
|
||||||
|
_, query = cmd.split(":", 1)
|
||||||
|
browser = aqt.dialogs.open("Browser", self.mw)
|
||||||
|
browser.search_for(query)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
self.form.web.load_ts_page("graphs")
|
self.form.web.load_ts_page("graphs")
|
||||||
|
|
||||||
|
@ -1103,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 {
|
||||||
|
@ -51,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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,13 +9,19 @@
|
|||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
export let preferences: PreferenceStore;
|
||||||
|
|
||||||
let histogramData = null as HistogramData | null;
|
let histogramData = null as HistogramData | null;
|
||||||
let tableData: TableDatum[] = [];
|
let tableData: TableDatum[] = [];
|
||||||
let graphRange: GraphRange = GraphRange.Month;
|
let graphRange: GraphRange = GraphRange.Month;
|
||||||
|
let { browserLinksSupported } = preferences;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let addedData: GraphData | null = null;
|
let addedData: GraphData | null = null;
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
@ -23,7 +29,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: if (addedData) {
|
$: if (addedData) {
|
||||||
[histogramData, tableData] = buildHistogram(addedData, graphRange, i18n);
|
[histogramData, tableData] = buildHistogram(
|
||||||
|
addedData,
|
||||||
|
graphRange,
|
||||||
|
i18n,
|
||||||
|
dispatch,
|
||||||
|
$browserLinksSupported
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE);
|
const title = i18n.tr(i18n.TR.STATISTICS_ADDED_TITLE);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
import NoDataOverlay from "./NoDataOverlay.svelte";
|
import NoDataOverlay from "./NoDataOverlay.svelte";
|
||||||
import AxisTicks from "./AxisTicks.svelte";
|
import AxisTicks from "./AxisTicks.svelte";
|
||||||
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
||||||
@ -15,6 +16,8 @@
|
|||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
|
||||||
let { calendarFirstDayOfWeek } = preferences;
|
let { calendarFirstDayOfWeek } = preferences;
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let graphData: GraphData | null = null;
|
let graphData: GraphData | null = null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
@ -49,6 +52,7 @@
|
|||||||
svg as SVGElement,
|
svg as SVGElement,
|
||||||
bounds,
|
bounds,
|
||||||
graphData,
|
graphData,
|
||||||
|
dispatch,
|
||||||
targetYear,
|
targetYear,
|
||||||
i18n,
|
i18n,
|
||||||
nightMode,
|
nightMode,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
import { defaultGraphBounds } from "./graph-helpers";
|
import { defaultGraphBounds } from "./graph-helpers";
|
||||||
import { gatherData, renderCards } from "./card-counts";
|
import { gatherData, renderCards } from "./card-counts";
|
||||||
import type { GraphData, TableDatum } from "./card-counts";
|
import type { GraphData, TableDatum } from "./card-counts";
|
||||||
@ -10,7 +11,9 @@
|
|||||||
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();
|
||||||
|
|
||||||
let svg = null as HTMLElement | SVGElement | null;
|
let svg = null as HTMLElement | SVGElement | null;
|
||||||
|
|
||||||
let bounds = defaultGraphBounds();
|
let bounds = defaultGraphBounds();
|
||||||
@ -52,6 +55,12 @@
|
|||||||
.right {
|
.right {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-link:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--link);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="graph" id="graph-card-counts">
|
<div class="graph" id="graph-card-counts">
|
||||||
@ -79,7 +88,12 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<td>
|
<td>
|
||||||
<span style="color: {d.colour};">■ </span>{d.label}
|
<span style="color: {d.colour};">■ </span>
|
||||||
|
{#if browserLinksSupported}
|
||||||
|
<span class="search-link" on:click={() => dispatch('search', { query: d.query })}>{d.label}</span>
|
||||||
|
{:else}
|
||||||
|
<span>{d.label}</span>
|
||||||
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="right">{d.count}</td>
|
<td class="right">{d.count}</td>
|
||||||
<td class="right">{d.percent}</td>
|
<td class="right">{d.percent}</td>
|
||||||
|
@ -6,15 +6,26 @@
|
|||||||
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";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
export let preferences: PreferenceStore;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let histogramData = null as HistogramData | null;
|
let histogramData = null as HistogramData | null;
|
||||||
let tableData: TableDatum[] = [];
|
let tableData: TableDatum[] = [];
|
||||||
|
let { browserLinksSupported } = preferences;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
[histogramData, tableData] = prepareData(gatherData(sourceData), i18n);
|
[histogramData, tableData] = prepareData(
|
||||||
|
gatherData(sourceData),
|
||||||
|
i18n,
|
||||||
|
dispatch,
|
||||||
|
$browserLinksSupported
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_TITLE);
|
const title = i18n.tr(i18n.TR.STATISTICS_CARD_EASE_TITLE);
|
||||||
|
@ -9,15 +9,21 @@
|
|||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
export let preferences: PreferenceStore;
|
||||||
|
|
||||||
|
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;
|
||||||
let backlog: boolean = true;
|
let backlog: boolean = true;
|
||||||
let graphRange: GraphRange = GraphRange.Month;
|
let graphRange: GraphRange = GraphRange.Month;
|
||||||
|
let { browserLinksSupported } = preferences;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
graphData = gatherData(sourceData);
|
graphData = gatherData(sourceData);
|
||||||
@ -28,7 +34,9 @@
|
|||||||
graphData,
|
graphData,
|
||||||
graphRange,
|
graphRange,
|
||||||
backlog,
|
backlog,
|
||||||
i18n
|
i18n,
|
||||||
|
dispatch,
|
||||||
|
$browserLinksSupported
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import { getGraphData, RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
import { getGraphData, RevlogRange, daysToRevlogRange } from "./graph-helpers";
|
||||||
import { getPreferences } from "./preferences";
|
import { getPreferences } from "./preferences";
|
||||||
|
import { bridgeCommand } from "anki/bridgecommand";
|
||||||
|
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
@ -24,7 +25,9 @@
|
|||||||
|
|
||||||
const preferencesPromise = getPreferences();
|
const preferencesPromise = getPreferences();
|
||||||
|
|
||||||
const refreshWith = async (search: string, days: number) => {
|
const refreshWith = async (searchNew: string, days: number) => {
|
||||||
|
search = searchNew;
|
||||||
|
|
||||||
active = true;
|
active = true;
|
||||||
try {
|
try {
|
||||||
[sourceData, preferences] = await Promise.all([
|
[sourceData, preferences] = await Promise.all([
|
||||||
@ -44,6 +47,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
refreshWith(search, days);
|
refreshWith(search, days);
|
||||||
|
|
||||||
|
const browserSearch = (event: CustomEvent) => {
|
||||||
|
const query = `${search} ${event.detail.query}`;
|
||||||
|
bridgeCommand(`browserSearch:${query}`);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if controller}
|
{#if controller}
|
||||||
@ -65,7 +73,8 @@
|
|||||||
{preferences}
|
{preferences}
|
||||||
{revlogRange}
|
{revlogRange}
|
||||||
{i18n}
|
{i18n}
|
||||||
{nightMode} />
|
{nightMode}
|
||||||
|
on:search={browserSearch} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -12,21 +12,33 @@
|
|||||||
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";
|
||||||
|
import type { PreferenceStore } from "./preferences";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
export let i18n: I18n;
|
export let i18n: I18n;
|
||||||
|
export let preferences: PreferenceStore;
|
||||||
|
|
||||||
|
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[] = [];
|
||||||
let range = IntervalRange.Percentile95;
|
let range = IntervalRange.Percentile95;
|
||||||
|
let { browserLinksSupported } = preferences;
|
||||||
|
|
||||||
$: if (sourceData) {
|
$: if (sourceData) {
|
||||||
intervalData = gatherIntervalData(sourceData);
|
intervalData = gatherIntervalData(sourceData);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (intervalData) {
|
$: if (intervalData) {
|
||||||
[histogramData, tableData] = prepareIntervalData(intervalData, range, i18n);
|
[histogramData, tableData] = prepareIntervalData(
|
||||||
|
intervalData,
|
||||||
|
range,
|
||||||
|
i18n,
|
||||||
|
dispatch,
|
||||||
|
$browserLinksSupported
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE);
|
const title = i18n.tr(i18n.TR.STATISTICS_INTERVALS_TITLE);
|
||||||
|
@ -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,23 @@ 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,
|
||||||
|
browserLinksSupported: boolean
|
||||||
): [HistogramData | null, TableDatum[]] {
|
): [HistogramData | null, TableDatum[]] {
|
||||||
// get min/max
|
// get min/max
|
||||||
const total = data.daysAdded.length;
|
const total = data.daysAdded.length;
|
||||||
@ -102,8 +115,23 @@ export function buildHistogram(
|
|||||||
return `${day}:<br>${cards}<br>${total}: ${totalCards}`;
|
return `${day}:<br>${cards}<br>${total}: ${totalCards}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
const start = Math.abs(bin.x0!) + 1;
|
||||||
|
const end = Math.abs(bin.x1!) + 1;
|
||||||
|
const query = makeQuery(start, end);
|
||||||
|
dispatch("search", { query });
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ scale, bins, total: totalInPeriod, hoverText, colourScale, showArea: true },
|
{
|
||||||
|
scale,
|
||||||
|
bins,
|
||||||
|
total: totalInPeriod,
|
||||||
|
hoverText,
|
||||||
|
onClick: browserLinksSupported ? onClick : null,
|
||||||
|
colourScale,
|
||||||
|
showArea: true,
|
||||||
|
},
|
||||||
tableData,
|
tableData,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,7 @@ export function renderCalendar(
|
|||||||
svgElem: SVGElement,
|
svgElem: SVGElement,
|
||||||
bounds: GraphBounds,
|
bounds: GraphBounds,
|
||||||
sourceData: GraphData,
|
sourceData: GraphData,
|
||||||
|
dispatch: any,
|
||||||
targetYear: number,
|
targetYear: number,
|
||||||
i18n: I18n,
|
i18n: I18n,
|
||||||
nightMode: boolean,
|
nightMode: boolean,
|
||||||
@ -205,6 +206,14 @@ export function renderCalendar(
|
|||||||
showTooltip(tooltipText(d), x, y);
|
showTooltip(tooltipText(d), x, y);
|
||||||
})
|
})
|
||||||
.on("mouseout", hideTooltip)
|
.on("mouseout", hideTooltip)
|
||||||
|
.attr("class", (d: any): string => {
|
||||||
|
return d.count > 0 ? "clickable" : "";
|
||||||
|
})
|
||||||
|
.on("click", function (this: any, d: any) {
|
||||||
|
if (d.count > 0) {
|
||||||
|
dispatch("search", { query: `"prop:rated=${d.day}"` });
|
||||||
|
}
|
||||||
|
})
|
||||||
.transition()
|
.transition()
|
||||||
.duration(800)
|
.duration(800)
|
||||||
.attr("fill", (d) => (d.count === 0 ? emptyColour : blues(d.count)!));
|
.attr("fill", (d) => (d.count === 0 ? emptyColour : blues(d.count)!));
|
||||||
|
@ -23,7 +23,7 @@ import type { GraphBounds } from "./graph-helpers";
|
|||||||
import { cumsum } from "d3-array";
|
import { cumsum } from "d3-array";
|
||||||
import type { I18n } from "anki/i18n";
|
import type { I18n } from "anki/i18n";
|
||||||
|
|
||||||
type Count = [string, number, boolean];
|
type Count = [string, number, boolean, string];
|
||||||
export interface GraphData {
|
export interface GraphData {
|
||||||
title: string;
|
title: string;
|
||||||
counts: Count[];
|
counts: Count[];
|
||||||
@ -86,18 +86,51 @@ function countCards(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extraQuery = separateInactive ? 'AND -("is:buried" OR "is:suspended")' : "";
|
||||||
|
|
||||||
const counts: Count[] = [
|
const counts: Count[] = [
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS), newCards, true],
|
[
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS), learn, true],
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_NEW_CARDS),
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS), relearn, true],
|
newCards,
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS), young, true],
|
true,
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS), mature, true],
|
`"is:new"${extraQuery}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_LEARNING_CARDS),
|
||||||
|
learn,
|
||||||
|
true,
|
||||||
|
`(-"is:review" AND "is:learn")${extraQuery}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_RELEARNING_CARDS),
|
||||||
|
relearn,
|
||||||
|
true,
|
||||||
|
`("is:review" AND "is:learn")${extraQuery}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_YOUNG_CARDS),
|
||||||
|
young,
|
||||||
|
true,
|
||||||
|
`("is:review" AND -"is:learn") AND "prop:ivl<21"${extraQuery}`,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_MATURE_CARDS),
|
||||||
|
mature,
|
||||||
|
true,
|
||||||
|
`("is:review" -"is:learn") AND "prop:ivl>=21"${extraQuery}`,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS),
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_SUSPENDED_CARDS),
|
||||||
suspended,
|
suspended,
|
||||||
separateInactive,
|
separateInactive,
|
||||||
|
'"is:suspended"',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS),
|
||||||
|
buried,
|
||||||
|
separateInactive,
|
||||||
|
'"is:buried"',
|
||||||
],
|
],
|
||||||
[i18n.tr(i18n.TR.STATISTICS_COUNTS_BURIED_CARDS), buried, separateInactive],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return counts;
|
return counts;
|
||||||
@ -132,6 +165,7 @@ export interface SummedDatum {
|
|||||||
count: number;
|
count: number;
|
||||||
// show up in the table
|
// show up in the table
|
||||||
show: boolean;
|
show: boolean;
|
||||||
|
query: string;
|
||||||
// running total
|
// running total
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
@ -139,6 +173,7 @@ export interface SummedDatum {
|
|||||||
export interface TableDatum {
|
export interface TableDatum {
|
||||||
label: string;
|
label: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
query: string;
|
||||||
percent: string;
|
percent: string;
|
||||||
colour: string;
|
colour: string;
|
||||||
}
|
}
|
||||||
@ -155,6 +190,7 @@ export function renderCards(
|
|||||||
label: count[0],
|
label: count[0],
|
||||||
count: count[1],
|
count: count[1],
|
||||||
show: count[2],
|
show: count[2],
|
||||||
|
query: count[3],
|
||||||
idx,
|
idx,
|
||||||
total: n,
|
total: n,
|
||||||
} as SummedDatum;
|
} as SummedDatum;
|
||||||
@ -205,6 +241,7 @@ export function renderCards(
|
|||||||
count: d.count,
|
count: d.count,
|
||||||
percent: `${percent}%`,
|
percent: `${percent}%`,
|
||||||
colour: barColours[idx],
|
colour: barColours[idx],
|
||||||
|
query: d.query,
|
||||||
} as TableDatum)
|
} as TableDatum)
|
||||||
: [];
|
: [];
|
||||||
});
|
});
|
||||||
|
@ -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,22 @@ 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,
|
||||||
|
browserLinksSupported: boolean
|
||||||
): [HistogramData | null, TableDatum[]] {
|
): [HistogramData | null, TableDatum[]] {
|
||||||
// get min/max
|
// get min/max
|
||||||
const allEases = data.eases;
|
const allEases = data.eases;
|
||||||
@ -61,6 +74,13 @@ export function prepareData(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
const start = bin.x0!;
|
||||||
|
const end = bin.x1! - 1;
|
||||||
|
const query = makeQuery(start, end);
|
||||||
|
dispatch("search", { query });
|
||||||
|
}
|
||||||
|
|
||||||
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
|
const xTickFormat = (num: number): string => `${num.toFixed(0)}%`;
|
||||||
const tableData = [
|
const tableData = [
|
||||||
{
|
{
|
||||||
@ -70,7 +90,16 @@ export function prepareData(
|
|||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ scale, bins, total, hoverText, colourScale, showArea: false, xTickFormat },
|
{
|
||||||
|
scale,
|
||||||
|
bins,
|
||||||
|
total,
|
||||||
|
hoverText,
|
||||||
|
onClick: browserLinksSupported ? onClick : null,
|
||||||
|
colourScale,
|
||||||
|
showArea: false,
|
||||||
|
xTickFormat,
|
||||||
|
},
|
||||||
tableData,
|
tableData,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
browserLinksSupported: boolean
|
||||||
): FutureDueOut {
|
): FutureDueOut {
|
||||||
const output = { histogramData: null, tableData: [] };
|
const output = { histogramData: null, tableData: [] };
|
||||||
// get min/max
|
// get min/max
|
||||||
@ -145,6 +157,13 @@ export function buildHistogram(
|
|||||||
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
|
return `${days}:<br>${cards}<br>${totalLabel}: ${cumulative}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
const start = bin.x0!;
|
||||||
|
const end = bin.x1! - 1;
|
||||||
|
const query = makeQuery(start, end);
|
||||||
|
dispatch("search", { query });
|
||||||
|
}
|
||||||
|
|
||||||
const periodDays = xMax! - xMin!;
|
const periodDays = xMax! - xMin!;
|
||||||
const tableData = [
|
const tableData = [
|
||||||
{
|
{
|
||||||
@ -171,6 +190,7 @@ export function buildHistogram(
|
|||||||
bins,
|
bins,
|
||||||
total,
|
total,
|
||||||
hoverText,
|
hoverText,
|
||||||
|
onClick: browserLinksSupported ? onClick : null,
|
||||||
showArea: true,
|
showArea: true,
|
||||||
colourScale,
|
colourScale,
|
||||||
binValue,
|
binValue,
|
||||||
|
@ -197,3 +197,7 @@
|
|||||||
.no-focus-outline:focus {
|
.no-focus-outline:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -25,6 +25,7 @@ export interface HistogramData {
|
|||||||
cumulative: number,
|
cumulative: number,
|
||||||
percent: number
|
percent: number
|
||||||
) => string;
|
) => string;
|
||||||
|
onClick: ((data: Bin<number, number>) => void) | null;
|
||||||
showArea: boolean;
|
showArea: boolean;
|
||||||
colourScale: ScaleSequential<string>;
|
colourScale: ScaleSequential<string>;
|
||||||
binValue?: (bin: Bin<any, any>) => number;
|
binValue?: (bin: Bin<any, any>) => number;
|
||||||
@ -131,7 +132,7 @@ export function histogramGraph(
|
|||||||
"d",
|
"d",
|
||||||
area()
|
area()
|
||||||
.curve(curveBasis)
|
.curve(curveBasis)
|
||||||
.x((d, idx) => {
|
.x((_d, idx) => {
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
return x(data.bins[0].x0!)!;
|
return x(data.bins[0].x0!)!;
|
||||||
} else {
|
} else {
|
||||||
@ -144,7 +145,8 @@ export function histogramGraph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hover/tooltip
|
// hover/tooltip
|
||||||
svg.select("g.hoverzone")
|
const hoverzone = svg
|
||||||
|
.select("g.hoverzone")
|
||||||
.selectAll("rect")
|
.selectAll("rect")
|
||||||
.data(data.bins)
|
.data(data.bins)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
@ -152,10 +154,14 @@ export function histogramGraph(
|
|||||||
.attr("y", () => y(yMax!)!)
|
.attr("y", () => y(yMax!)!)
|
||||||
.attr("width", barWidth)
|
.attr("width", barWidth)
|
||||||
.attr("height", () => y(0)! - y(yMax!)!)
|
.attr("height", () => y(0)! - y(yMax!)!)
|
||||||
.on("mousemove", function (this: any, d: any, idx) {
|
.on("mousemove", function (this: any, _d: any, idx: number) {
|
||||||
const [x, y] = mouse(document.body);
|
const [x, y] = mouse(document.body);
|
||||||
const pct = data.showArea ? (areaData[idx + 1] / data.total) * 100 : 0;
|
const pct = data.showArea ? (areaData[idx + 1] / data.total) * 100 : 0;
|
||||||
showTooltip(data.hoverText(data, idx, areaData[idx + 1], pct), x, y);
|
showTooltip(data.hoverText(data, idx, areaData[idx + 1], pct), x, y);
|
||||||
})
|
})
|
||||||
.on("mouseout", hideTooltip);
|
.on("mouseout", hideTooltip);
|
||||||
|
|
||||||
|
if (data.onClick) {
|
||||||
|
hoverzone.attr("class", "clickable").on("click", data.onClick);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,23 @@ 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,
|
||||||
|
browserLinksSupported: boolean
|
||||||
): [HistogramData | null, TableDatum[]] {
|
): [HistogramData | null, TableDatum[]] {
|
||||||
// get min/max
|
// get min/max
|
||||||
const allIntervals = data.intervals;
|
const allIntervals = data.intervals;
|
||||||
@ -134,6 +147,13 @@ export function prepareIntervalData(
|
|||||||
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
|
return `${interval}<br>${total}: \u200e${percent.toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClick(bin: Bin<number, number>): void {
|
||||||
|
const start = bin.x0!;
|
||||||
|
const end = bin.x1! - 1;
|
||||||
|
const query = makeQuery(start, end);
|
||||||
|
dispatch("search", { query });
|
||||||
|
}
|
||||||
|
|
||||||
const meanInterval = Math.round(mean(allIntervals) ?? 0);
|
const meanInterval = Math.round(mean(allIntervals) ?? 0);
|
||||||
const meanIntervalString = timeSpan(i18n, meanInterval * 86400, false);
|
const meanIntervalString = timeSpan(i18n, meanInterval * 86400, false);
|
||||||
const tableData = [
|
const tableData = [
|
||||||
@ -142,8 +162,17 @@ export function prepareIntervalData(
|
|||||||
value: meanIntervalString,
|
value: meanIntervalString,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ scale, bins, total: totalInPeriod, hoverText, colourScale, showArea: true },
|
{
|
||||||
|
scale,
|
||||||
|
bins,
|
||||||
|
total: totalInPeriod,
|
||||||
|
hoverText,
|
||||||
|
onClick: browserLinksSupported ? onClick : null,
|
||||||
|
colourScale,
|
||||||
|
showArea: true,
|
||||||
|
},
|
||||||
tableData,
|
tableData,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
bridgeCommand<T>(command: string, callback?: (value: T) => void): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTML <a> tag pointing to a bridge command.
|
/// HTML <a> tag pointing to a bridge command.
|
||||||
export function bridgeLink(command: string, label: string): string {
|
export function bridgeLink(command: string, label: string): string {
|
||||||
return `<a href="javascript:bridgeCommand('${command}')">${label}</a>`;
|
return `<a href="javascript:bridgeCommand('${command}')">${label}</a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bridgeCommand<T>(command: string, callback?: (value: T) => void): void {
|
||||||
|
window.bridgeCommand<T>(command, callback);
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user