Skip to content

Commit af9c925

Browse files
authored
Implements switchable color vision deficiency simulation to the docs (#109)
1 parent 66da1b1 commit af9c925

File tree

5 files changed

+155
-7
lines changed

5 files changed

+155
-7
lines changed

docs/_gen_cmaps.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if TYPE_CHECKING:
1010
from cmap import _catalog
1111
from cmap import Colormap
12-
from cmap._util import report
12+
from cmap._util import report_cvds
1313

1414
# TODO: convert to jinja
1515
TEMPLATE = """# {name}
@@ -32,6 +32,29 @@
3232
{{{{ cmap_gray: {name} 30 }}}}
3333
{{{{ cmap_sineramp: {name} }}}}
3434
35+
<form id="cvd" class="radio-group">
36+
<label class="radio-option" title="Full Vision">
37+
<input type="radio" name="cvd_button" value="normal" checked>
38+
<span class="material-icons">visibility</span>
39+
<span class="radio-label"> Normal Vision</span>
40+
</label>
41+
<label class="radio-option" title="Protanopic">
42+
<input type="radio" name="cvd_button" value="protan">
43+
<span class="material-icons">filter_1</span>
44+
<span class="radio-label"> Protanopic</span>
45+
</label>
46+
<label class="radio-option" title="Deuteranopic">
47+
<input type="radio" name="cvd_button" value="deutan">
48+
<span class="material-icons">filter_2</span>
49+
<span class="radio-label"> Deuteranopic</span>
50+
</label>
51+
<label class="radio-option" title="Tritananopic">
52+
<input type="radio" name="cvd_button" value="tritan">
53+
<span class="material-icons">filter_3</span>
54+
<span class="radio-label"> Tritanopic</span>
55+
</label>
56+
</form>
57+
3558
## Perceptual Lightness
3659
3760
<canvas class="linearity-chart cmap-chart" data-cmap-name="{name}" width="800" height="350"></canvas>
@@ -53,6 +76,19 @@
5376
5477
<script>
5578
window.cmap_data = {data};
79+
80+
cvd?.addEventListener("change", (e) => {{
81+
const selected = cvd.querySelector('input[name="cvd_button"]:checked')?.value;
82+
//window.cmap_data = {data}[selected];
83+
console.log("CVD type selected:", selected);
84+
// re-render the charts
85+
initCharts();
86+
87+
console.log("Selected variant:", selected);
88+
}});
89+
90+
91+
5692
<!-- Note: this is here because of `navigation.instant` in the mkdocs settings -->
5793
typeof(initCharts) !== 'undefined' && initCharts();
5894
</script>
@@ -111,11 +147,13 @@ def build_catalog(catalog: "_catalog.Catalog") -> None:
111147
# write data used for charts
112148
cm = Colormap(name)
113149
cmap_data = {
114-
k: np.around(v, 4).tolist() if isinstance(v, np.ndarray) else v
115-
for k, v in report(cm).items()
116-
if k in INCLUDE_DATA
150+
cvd_type: {
151+
k: np.around(v, 4).tolist() if isinstance(v, np.ndarray) else v
152+
for k, v in report.items()
153+
if k in INCLUDE_DATA
154+
}
155+
for cvd_type, report in report_cvds(cm).items()
117156
}
118-
119157
_aliases = [x for x in info.aliases if x != info.name]
120158
aliases = _make_aliases_md(_aliases) if _aliases else ""
121159

docs/javascripts/cmap_charts.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,20 @@ async function initCharts() {
2222
chartElems[cmap_name] = [charts[i]];
2323
}
2424
}
25+
var cvd_type = document.getElementById("cvd").querySelector('input[name="cvd_button"]:checked')?.value;
2526
// Make all charts for each cmap name
2627
for (var cmap_name in chartElems) {
2728
// NOTE: we're using a global window variable here that will be
2829
// injected into the _gen_cmaps page... because it's much faster
2930
// on readthedocs than making an additional fetch request
30-
var cmap_data = window.cmap_data[cmap_name];
31+
var cmap_data = window.cmap_data[cmap_name][cvd_type] || window.cmap_data[cmap_name]["normal"];
3132
for (var i = 0; i < chartElems[cmap_name].length; i++) {
3233
var canv = chartElems[cmap_name][i];
34+
const chart = Chart.getChart(canv);
35+
if (chart) {
36+
chart.destroy();
37+
}
38+
3339
if (canv.classList.contains("rgb-chart")) {
3440
makeRGBChart(canv, cmap_data);
3541
} else if (canv.classList.contains("hsl-chart")) {

docs/stylesheets/extra.css

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,49 @@ body[data-md-color-scheme="slate"] .btn.active {
176176
#readthedocs-ea, .ethical-alabaster {
177177
display: none;
178178
}
179+
180+
.radio-group {
181+
display: flex;
182+
gap: 1rem;
183+
margin-bottom: 1rem;
184+
}
185+
186+
.radio-option {
187+
position: relative;
188+
display: inline-flex;
189+
align-items: center;
190+
justify-content: center;
191+
padding: 0.5em;
192+
border-radius: 0.5em;
193+
cursor: pointer;
194+
transition: background-color 0.2s, box-shadow 0.2s;
195+
}
196+
197+
.radio-option input[type="radio"] {
198+
appearance: none;
199+
position: absolute;
200+
opacity: 0;
201+
pointer-events: none;
202+
}
203+
204+
.radio-option .material-icons {
205+
font-size: 1rem;
206+
color: var(--md-default-fg-color);
207+
transition: color 0.2s;
208+
}
209+
210+
.radio-option input[type="radio"]:checked ~ .material-icons,
211+
.radio-option input[type="radio"]:checked ~ .radio-label {
212+
color: var(--md-accent-fg-color);
213+
}
214+
215+
216+
.radio-option:hover {
217+
background-color: var(--md-default-bg-color--lighter);
218+
box-shadow: 0 0 0 2px var(--md-accent-fg-color);
219+
}
220+
221+
.radio-option .radio-label {
222+
font-size: 0.65rem;
223+
color: var(--md-default-fg-color);
224+
}

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ plugins:
117117
summary: true
118118

119119
extra_css:
120+
- https://fonts.googleapis.com/icon?family=Material+Icons
120121
- stylesheets/extra.css
121122

122123
extra_javascript:

src/cmap/_util.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,27 @@ class ReportDict(TypedDict):
446446
lightness_derivs: np.ndarray
447447

448448

449-
def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> ReportDict:
449+
class CVDReportDict(TypedDict):
450+
normal: ReportDict
451+
protan: ReportDict
452+
deutan: ReportDict
453+
tritan: ReportDict
454+
455+
456+
def report(
457+
cm: Colormap,
458+
n: int = 256,
459+
uniform_space: str = "CAM02-UCS",
460+
initial_space: dict | None = None,
461+
) -> ReportDict:
450462
"""Generate a report of data describing a colormap.
451463
452464
This is primarily used for generating charts in the documentation
453465
"""
454466
from colorspacious import cspace_convert
455467

468+
if initial_space is None:
469+
initial_space = {"name": "sRGB1"}
456470
if len(cm.color_stops) >= 100:
457471
RGBA = np.asarray(cm.color_stops.color_array)
458472
n = RGBA.shape[0]
@@ -462,6 +476,7 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> Repo
462476
RGBA = cm(x)
463477
RGB = RGBA[:, :3]
464478

479+
RGB = np.clip(cast("np.ndarray", cspace_convert(RGB, initial_space, "sRGB1")), 0, 1)
465480
Jab = cast("np.ndarray", cspace_convert(RGB, "sRGB1", uniform_space))
466481

467482
local_deltas = np.sqrt(np.sum((Jab[:-1, :] - Jab[1:, :]) ** 2, axis=-1))
@@ -504,6 +519,48 @@ def report(cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS") -> Repo
504519
# H -> hue composition
505520

506521

522+
def report_cvds(
523+
cm: Colormap, n: int = 256, uniform_space: str = "CAM02-UCS"
524+
) -> CVDReportDict:
525+
"""Generate a report of data describing a colormap for different CVD types.
526+
527+
This is primarily used for generating charts in the documentation
528+
"""
529+
return {
530+
"normal": report(cm, n=n, uniform_space=uniform_space),
531+
"protan": report(
532+
cm,
533+
n=n,
534+
uniform_space=uniform_space,
535+
initial_space={
536+
"name": "sRGB1+CVD",
537+
"cvd_type": "protanomaly",
538+
"severity": 100,
539+
},
540+
),
541+
"deutan": report(
542+
cm,
543+
n=n,
544+
uniform_space=uniform_space,
545+
initial_space={
546+
"name": "sRGB1+CVD",
547+
"cvd_type": "deuteranomaly",
548+
"severity": 100,
549+
},
550+
),
551+
"tritan": report(
552+
cm,
553+
n=n,
554+
uniform_space=uniform_space,
555+
initial_space={
556+
"name": "sRGB1+CVD",
557+
"cvd_type": "tritanomaly",
558+
"severity": 100,
559+
},
560+
),
561+
}
562+
563+
507564
def to_mpl(
508565
value: ColorStopsLike,
509566
/,

0 commit comments

Comments
 (0)