This commit is contained in:
TheFefel 2025-09-17 15:23:46 +08:00 committed by GitHub
commit e541006989
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 270 additions and 63 deletions

View file

@ -234,6 +234,10 @@ Spiritual Father <https://github.com/spiritualfather>
Emmanuel Ferdman <https://github.com/emmanuel-ferdman>
Sunong2008 <https://github.com/Sunrongguo2008>
Marvin Kopf <marvinkopf@outlook.com>
David Brenn <davidbrenn@t-online.de>
Felix Kühne <flxkhn0602@gmail.com>
Matthis Ehrhardt <matthis-ehrhardt@gmx.de>
Billy Julian Lesmana <hunter140797@gmail.com>
Kevin Nakamura <grinkers@grinkers.net>
Bradley Szoke <bradleyszoke@gmail.com>
jcznk <https://github.com/jcznk>

View file

@ -60,6 +60,7 @@ preferences-full-screen-only = Full screen only
preferences-appearance = Appearance
preferences-general = General
preferences-style = Style
preferences-color-blind = Color Blind Mode
preferences-review = Review
preferences-answer-keys = Answer keys
preferences-distractions = Distractions

View file

@ -104,6 +104,23 @@
<string>preferences_user_interface</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QLabel" name="styleLabel">
<property name="text">
<string>preferences_style</string>
</property>
<property name="buddy">
<cstring>styleComboBox</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="styleComboBox">
<property name="currentText">
<string/>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="theme"/>
</item>
@ -123,23 +140,6 @@
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="styleComboBox">
<property name="currentText">
<string/>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="styleLabel">
<property name="text">
<string>preferences_style</string>
</property>
<property name="buddy">
<cstring>styleComboBox</cstring>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="themeLabel">
<property name="text">
@ -160,13 +160,26 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QPushButton" name="resetWindowSizes">
<property name="text">
<string>preferences_reset_window_sizes</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="color_blind">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>preferences_color_blind</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View file

@ -371,11 +371,24 @@ class Preferences(QDialog):
self.form.styleComboBox.setVisible(not is_win)
qconnect(self.form.resetWindowSizes.clicked, self.on_reset_window_sizes)
self.form.color_blind.setChecked(self.mw.pm.color_blind())
qconnect(
self.form.color_blind.stateChanged, self.on_color_blind_checkbox_changed
)
self.setup_language()
self.setup_video_driver()
self.setupOptions()
def on_color_blind_checkbox_changed(self, state: int) -> None:
if state == 2:
# checkbox is checked
self.mw.pm.set_color_blind(True)
else:
# checkbox is unchecked
self.mw.pm.set_color_blind(False)
def update_global(self) -> None:
restart_required = False

View file

@ -536,6 +536,12 @@ create table if not exists profiles
def setUiScale(self, scale: float) -> None:
self.meta["uiScale"] = scale
def set_color_blind(self, value: bool) -> None:
self.meta["color_blind"] = value
def color_blind(self) -> bool:
return self.meta.get("color_blind", False)
def reduce_motion(self) -> bool:
return self.meta.get("reduce_motion", True)

View file

@ -136,10 +136,18 @@ class NewDeckStats(QDialog):
_, query = cmd.split(":", 1)
browser = aqt.dialogs.open("Browser", self.mw)
browser.search_for(query)
return False
def refresh(self) -> None:
def on_load_finished(success: bool) -> None:
if success:
is_color_blind = self.mw.pm.color_blind()
js_code = f"window.colorBlindMode = {str(is_color_blind).lower()};"
self.form.web.eval(js_code)
# Disconnect after running once to avoid multiple triggers
self.form.web.page().loadFinished.disconnect(on_load_finished)
self.form.web.page().loadFinished.connect(on_load_finished)
self.form.web.load_sveltekit_page("graphs")

View file

@ -3,9 +3,9 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { colorBlindColors } from "./graph-helpers";
import * as tr from "@generated/ftl";
import { localizedNumber } from "@tslib/i18n";
import { type RevlogRange } from "./graph-helpers";
import {
calculateRetentionPercentageString,
@ -13,6 +13,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
type PeriodTrueRetentionData,
type RowData,
} from "./true-retention";
import { onMount } from "svelte";
interface Props {
revlogRange: RevlogRange;
@ -22,6 +23,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const { revlogRange, data }: Props = $props();
const rowData: RowData[] = $derived(getRowData(data, revlogRange));
// Default (non-colorblind) colors
let youngColor = "#64c476";
let matureColor = "#31a354";
onMount(() => {
const isColorBlindMode = (window as any).colorBlindMode;
if (isColorBlindMode) {
youngColor = colorBlindColors.young;
matureColor = colorBlindColors.mature;
}
// Set globally so scoped SCSS can use them
document.documentElement.style.setProperty("--young-color", youngColor);
document.documentElement.style.setProperty("--mature-color", matureColor);
});
</script>
<table>
@ -84,11 +100,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
.young {
color: #64c476;
color: var(--young-color);
}
.mature {
color: #31a354;
color: var(--mature-color);
}
.total {

View file

@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { colorBlindColors } from "./graph-helpers";
import * as tr from "@generated/ftl";
import { type RevlogRange } from "./graph-helpers";
import {
@ -15,6 +16,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
type Scope,
} from "./true-retention";
import { localizedNumber } from "@tslib/i18n";
import { onMount } from "svelte";
interface Props {
revlogRange: RevlogRange;
@ -25,6 +27,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
const { revlogRange, data, scope }: Props = $props();
const rowData: RowData[] = $derived(getRowData(data, revlogRange));
let passColor = "#3bc464";
let failColor = "#c43b3b";
onMount(() => {
const isColorBlindMode = (window as any).colorBlindMode;
if (isColorBlindMode) {
passColor = colorBlindColors.relearn;
failColor = colorBlindColors.filtered;
}
// Apply them to document root so SCSS can see them
document.documentElement.style.setProperty("--pass-color", passColor);
document.documentElement.style.setProperty("--fail-color", failColor);
});
</script>
<table>
@ -71,11 +87,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
.pass {
color: #3bc464;
color: var(--pass-color);
}
.fail {
color: #c43b3b;
color: var(--fail-color);
}
.retention {

View file

@ -8,7 +8,7 @@
import type { GraphsResponse } from "@generated/anki/stats_pb";
import * as tr from "@generated/ftl";
import { dayLabel } from "@tslib/time";
import type { Bin } from "d3";
import { type Bin, interpolateViridis } from "d3";
import { bin, interpolateBlues, min, scaleLinear, scaleSequential, sum } from "d3";
import type { SearchDispatch, TableDatum } from "./graph-helpers";
@ -80,8 +80,18 @@ export function buildHistogram(
return [null, []];
}
const adjustedRange = scaleLinear().range([0.7, 0.3]);
const colourScale = scaleSequential((n) => interpolateBlues(adjustedRange(n)!)).domain([xMax!, xMin!]);
let adjustedRange;
let colourScale;
const isColourBlindMode = (window as any).colorBlindMode;
// Changing color based on mode
if (isColourBlindMode) {
adjustedRange = scaleLinear().range([0.3, 0.7]);
colourScale = scaleSequential((n) => interpolateViridis(adjustedRange(n)!)).domain([xMax!, xMin!]);
} else {
adjustedRange = scaleLinear().range([0.7, 0.3]);
colourScale = scaleSequential((n) => interpolateBlues(adjustedRange(n)!)).domain([xMax!, xMin!]);
}
const totalInPeriod = sum(bins, accessor);
const periodDays = Math.abs(xMin!);

View file

@ -12,6 +12,7 @@ import {
axisBottom,
axisLeft,
interpolateRdYlGn,
interpolateViridis,
pointer,
scaleBand,
scaleLinear,
@ -20,7 +21,7 @@ import {
sum,
} from "d3";
import type { GraphBounds } from "./graph-helpers";
import { type GraphBounds } from "./graph-helpers";
import { GraphRange } from "./graph-helpers";
import { setDataAvailable } from "./graph-helpers";
import { hideTooltip, showTooltip } from "./tooltip-utils.svelte";
@ -162,7 +163,15 @@ export function renderButtons(
.paddingOuter(1)
.paddingInner(0.1);
const colour = scaleSequential(interpolateRdYlGn).domain([1, 4]);
const isColourBlindMode = (window as any).colorBlindMode;
let colour;
// Changing color based on mode
if (isColourBlindMode) {
colour = scaleSequential(interpolateViridis).domain([1, 4]);
} else {
colour = scaleSequential(interpolateRdYlGn).domain([1, 4]);
}
// y scale
const yTickFormat = (n: number): string => localizedNumber(n);

View file

@ -10,6 +10,7 @@ import type { CountableTimeInterval } from "d3";
import { timeHour } from "d3";
import {
interpolateBlues,
interpolateMagma,
pointer,
scaleLinear,
scaleSequentialSqrt,
@ -136,10 +137,21 @@ export function renderCalendar(
}
}
const data = Array.from(dayMap.values());
const cappedRange = scaleLinear().range([0.2, nightMode ? 0.8 : 1]);
const blues = scaleSequentialSqrt()
.domain([0, maxCount])
.interpolator((n) => interpolateBlues(cappedRange(n)!));
const cappedRange = scaleLinear().range([1, 0]);
const isColorBlindMode = (window as any).colorBlindMode;
let gradient;
if (isColorBlindMode) {
gradient = scaleSequentialSqrt()
.domain([0, maxCount])
.interpolator((n) => interpolateMagma(cappedRange(n)!));
} else {
gradient = scaleSequentialSqrt()
.domain([0, maxCount])
.interpolator((n) => interpolateBlues(cappedRange(n)!));
}
function tooltipText(d: DayDatum): string {
const date = localizedDate(d.date, {
@ -203,7 +215,7 @@ export function renderCalendar(
})
.transition()
.duration(800)
.attr("fill", (d: DayDatum) => (d.count === 0 ? emptyColour : blues(d.count)!));
.attr("fill", (d: DayDatum) => (d.count === 0 ? emptyColour : gradient(d.count)!));
}
function timeFunctionForDay(firstDayOfWeek: Weekday): CountableTimeInterval {

View file

@ -22,7 +22,7 @@ import {
sum,
} from "d3";
import type { GraphBounds } from "./graph-helpers";
import { colorBlindColors, type GraphBounds } from "./graph-helpers";
type Count = [string, number, boolean, string];
export interface GraphData {
@ -31,15 +31,29 @@ export interface GraphData {
totalCards: string;
}
const barColours = [
schemeBlues[5][2], /* new */
schemeOranges[5][2], /* learn */
schemeReds[5][2], /* relearn */
schemeGreens[5][2], /* young */
schemeGreens[5][3], /* mature */
"#FFDC41", /* suspended */
"grey", /* buried */
];
let barColours;
if ((window as any).colorBlindMode) {
barColours = [
colorBlindColors.new, /* new */
colorBlindColors.learn, /* learn */
colorBlindColors.relearn, /* relearn */
colorBlindColors.young, /* young */
colorBlindColors.mature, /* mature */
colorBlindColors.suspended, /* suspended */
colorBlindColors.buried, /* buried */
];
} else {
barColours = [
schemeBlues[5][2], /* new */
schemeOranges[5][2], /* learn */
schemeReds[5][2], /* relearn */
schemeGreens[5][2], /* young */
schemeGreens[5][3], /* mature */
"#FFDC41", /* suspended */
"#grey", /* buried */
];
}
function countCards(data: GraphsResponse, separateInactive: boolean): Count[] {
const countData = separateInactive ? data.cardCounts!.excludingInactive! : data.cardCounts!.includingInactive!;

View file

@ -9,7 +9,7 @@ import type { GraphsResponse } from "@generated/anki/stats_pb";
import * as tr from "@generated/ftl";
import { localizedNumber } from "@tslib/i18n";
import type { Bin, ScaleLinear } from "d3";
import { bin, extent, interpolateRdYlGn, scaleLinear, scaleSequential, sum } from "d3";
import { bin, extent, interpolateRdYlGn, interpolateViridis, scaleLinear, scaleSequential, sum } from "d3";
import type { SearchDispatch, TableDatum } from "./graph-helpers";
import { getNumericMapBinValue, numericMap } from "./graph-helpers";
@ -84,7 +84,13 @@ export function prepareData(
.thresholds(ticks)(allEases.entries() as any);
const total = sum(bins as any, getNumericMapBinValue);
const colourScale = scaleSequential(interpolateRdYlGn).domain([xMin, 300]);
let colourScale;
if ((window as any).colorBlindMode) {
colourScale = scaleSequential(interpolateViridis).domain([xMin, 300]);
} else {
colourScale = scaleSequential(interpolateRdYlGn).domain([xMin, 300]);
}
function hoverText(bin: Bin<number, number>, _percent: number): string {
const minPct = Math.floor(bin.x0!);

View file

@ -10,7 +10,7 @@ import * as tr from "@generated/ftl";
import { localizedNumber } from "@tslib/i18n";
import { dayLabel } from "@tslib/time";
import type { Bin } from "d3";
import { bin, extent, interpolateGreens, scaleLinear, scaleSequential, sum } from "d3";
import { bin, extent, interpolateGreens, interpolateViridis, scaleLinear, scaleSequential, sum } from "d3";
import type { SearchDispatch, TableDatum } from "./graph-helpers";
import { getNumericMapBinValue, GraphRange, numericMap } from "./graph-helpers";
@ -108,8 +108,20 @@ export function buildHistogram(
}
const xTickFormat = (n: number): string => localizedNumber(n);
const adjustedRange = scaleLinear().range([0.7, 0.3]);
const colourScale = scaleSequential((n) => interpolateGreens(adjustedRange(n)!)).domain([xMin!, xMax!]);
let adjustedRange = scaleLinear().range([0.0, 1]);
const isColorBlindMode = (window as any).colorBlindMode;
let colourScale;
if (isColorBlindMode) {
colourScale = scaleSequential((n) => interpolateViridis(adjustedRange(n)!)).domain([xMin!, xMax!]);
adjustedRange = scaleLinear().range([0.0, 1]);
} else {
colourScale = scaleSequential((n) => interpolateGreens(adjustedRange(n)!)).domain([xMin!, xMax!]);
adjustedRange = scaleLinear().range([0.7, 0.3]);
}
const total = sum(bins as any, getNumericMapBinValue);

View file

@ -105,3 +105,17 @@ export function numericMap<T>(obj: { [k: string]: T }): Map<number, T> {
export function getNumericMapBinValue(d: Bin<Map<number, number>, number>): number {
return sum(d, (d) => d[1]);
}
/**
* Colorblind-friendly colors from https://davidmathlogic.com/colorblind/
*/
export const colorBlindColors = {
new: "#88CCEE",
learn: "#44AA99",
relearn: "#117733",
young: "#CC6677",
mature: "#882255",
suspended: "#DDCC77",
buried: "#332288",
filtered: "#AA4499",
};

View file

@ -16,6 +16,7 @@ import {
axisRight,
curveBasis,
interpolateBlues,
interpolateViridis,
pointer,
scaleBand,
scaleLinear,
@ -83,11 +84,23 @@ export function renderHours(
.classed(oddTickClass, (d: any): boolean => d % 2 != 0)
.attr("direction", "ltr");
const cappedRange = scaleLinear().range([0.1, 0.8]);
const colour = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain([
0,
yMax,
]);
let cappedRange;
let colour;
const isColorBlindMode = (window as any).colorBlindMode;
if (isColorBlindMode) {
cappedRange = scaleLinear().range([0.0, 1]);
colour = scaleSequential((n) => interpolateViridis(cappedRange(n)!)).domain([
0,
yMax,
]);
} else {
cappedRange = scaleLinear().range([0.1, 0.8]);
colour = scaleSequential((n) => interpolateBlues(cappedRange(n)!)).domain([
0,
yMax,
]);
}
// y scale
const yTickFormat = (n: number): string => localizedNumber(n);

View file

@ -10,7 +10,7 @@ import * as tr from "@generated/ftl";
import { localizedNumber } from "@tslib/i18n";
import { timeSpan } from "@tslib/time";
import type { Bin } from "d3";
import { bin, extent, interpolateBlues, quantile, scaleLinear, scaleSequential, sum } from "d3";
import { bin, extent, interpolateBlues, interpolateViridis, quantile, scaleLinear, scaleSequential, sum } from "d3";
import type { SearchDispatch, TableDatum } from "./graph-helpers";
import { numericMap } from "./graph-helpers";
@ -147,8 +147,16 @@ export function prepareIntervalData(
return [null, []];
}
const adjustedRange = scaleLinear().range([0.7, 0.3]);
const colourScale = scaleSequential((n) => interpolateBlues(adjustedRange(n)!)).domain([xMax!, xMin!]);
let adjustedRange;
let colourScale;
if ((window as any).colorBlindMode) {
adjustedRange = scaleLinear().range([0.3, 0.7]);
colourScale = scaleSequential((n) => interpolateViridis(adjustedRange(n)!)).domain([xMax!, xMin!]);
} else {
adjustedRange = scaleLinear().range([0.7, 0.3]);
colourScale = scaleSequential((n) => interpolateBlues(adjustedRange(n)!)).domain([xMax!, xMin!]);
}
function hoverText(
bin: Bin<number, number>,

View file

@ -16,12 +16,15 @@ import {
axisLeft,
axisRight,
bin,
color,
cumsum,
curveBasis,
hsl,
interpolateGreens,
interpolateOranges,
interpolatePurples,
interpolateReds,
interpolateRgb,
max,
min,
pointer,
@ -31,7 +34,7 @@ import {
sum,
} from "d3";
import type { GraphBounds, TableDatum } from "./graph-helpers";
import { colorBlindColors, type GraphBounds, type TableDatum } from "./graph-helpers";
import { GraphRange, numericMap, setDataAvailable } from "./graph-helpers";
import { hideTooltip, showTooltip } from "./tooltip-utils.svelte";
@ -188,18 +191,47 @@ export function renderReviews(
x.domain() as any,
);
const colorBlindMode = (window as any).colorBlindMode;
function makeColorBlindGradient(baseHex: string, satAdjust = 0.02, lightAdjust = 0.02) {
const base = color(baseHex);
if (!base) { throw new Error(`Invalid color: ${baseHex}`); }
const lighter = hsl(base);
lighter.s = Math.min(1, lighter.s + satAdjust);
lighter.l = Math.min(1, lighter.l + lightAdjust);
const darker = hsl(base);
darker.s = Math.max(0, darker.s - satAdjust);
darker.l = Math.max(0, darker.l - lightAdjust);
return scaleSequential(interpolateRgb(darker.toString(), lighter.toString()));
}
const colorBlindScales = {
mature: makeColorBlindGradient(colorBlindColors.mature),
learn: makeColorBlindGradient(colorBlindColors.learn),
relearn: makeColorBlindGradient(colorBlindColors.relearn),
young: makeColorBlindGradient(colorBlindColors.young),
suspended: makeColorBlindGradient(colorBlindColors.suspended),
buried: makeColorBlindGradient(colorBlindColors.buried),
filtered: makeColorBlindGradient(colorBlindColors.filtered),
};
Object.values(colorBlindScales).forEach(scale => scale.domain(x.domain() as any));
function binColor(idx: BinIndex): ScaleSequential<string> {
switch (idx) {
case BinIndex.Mature:
return darkerGreens;
return colorBlindMode ? colorBlindScales.mature : darkerGreens;
case BinIndex.Young:
return lighterGreens;
return colorBlindMode ? colorBlindScales.young : lighterGreens;
case BinIndex.Learn:
return oranges;
return colorBlindMode ? colorBlindScales.learn : oranges;
case BinIndex.Relearn:
return reds;
return colorBlindMode ? colorBlindScales.relearn : reds;
case BinIndex.Filtered:
return purples;
return colorBlindMode ? colorBlindScales.filtered : purples;
}
}