diff --git a/ts/routes/graphs/card-counts.ts b/ts/routes/graphs/card-counts.ts index 2ea7f9c5f..2207f78f5 100644 --- a/ts/routes/graphs/card-counts.ts +++ b/ts/routes/graphs/card-counts.ts @@ -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 { @@ -36,13 +36,13 @@ let barColours; if((window as any).colorBlindMode) { barColours = [ - "#88CCEE", /* new */ - "#44AA99", /* learn */ - "#117733", /* relearn */ - "#CC6677", /* young */ - "#882255", /* mature */ - "#DDCC77", /* suspended */ - "#332288", /* buried */ + colorBlindColors.new, /* new */ + colorBlindColors.learn, /* learn */ + colorBlindColors.relearn, /* relearn */ + colorBlindColors.young, /* young */ + colorBlindColors.mature, /* mature */ + colorBlindColors.suspended, /* suspended */ + colorBlindColors.buried, /* buried */ ]; } else diff --git a/ts/routes/graphs/graph-helpers.ts b/ts/routes/graphs/graph-helpers.ts index 47f0249b4..5e665f1c3 100644 --- a/ts/routes/graphs/graph-helpers.ts +++ b/ts/routes/graphs/graph-helpers.ts @@ -105,3 +105,17 @@ export function numericMap(obj: { [k: string]: T }): Map { export function getNumericMapBinValue(d: Bin, 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" +}; diff --git a/ts/routes/graphs/reviews.ts b/ts/routes/graphs/reviews.ts index 164fc4199..e6507f08b 100644 --- a/ts/routes/graphs/reviews.ts +++ b/ts/routes/graphs/reviews.ts @@ -22,16 +22,20 @@ import { interpolateGreens, interpolatePurples, interpolateReds, + interpolateRgb, max, min, pointer, scaleLinear, + scaleOrdinal, scaleSequential, select, sum, + color, + hsl } 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,21 +192,52 @@ 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 { 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 blues; + return colorBlindMode ? colorBlindScales.learn : blues; case BinIndex.Relearn: - return reds; + return colorBlindMode ? colorBlindScales.relearn : reds; case BinIndex.Filtered: - return purples; + return colorBlindMode ? colorBlindScales.filtered : purples; } } + function valueLabel(n: number): string { if (showTime) { return timeSpan(n / 1000, false, true, TimespanUnit.Hours);