Skip to content

Commit 0638375

Browse files
author
committed
Deployed 1697c07 with MkDocs version: 1.6.1
0 parents  commit 0638375

File tree

1,579 files changed

+36068
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

1,579 files changed

+36068
-0
lines changed

.nojekyll

Whitespace-only changes.

404.html

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

__pycache__/_hooks.cpython-312.pyc

10.2 KB
Binary file not shown.

_gen_cmaps.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import json
2+
from pathlib import Path
3+
from typing import TYPE_CHECKING
4+
5+
import mkdocs_gen_files
6+
import natsort
7+
import numpy as np
8+
9+
if TYPE_CHECKING:
10+
from cmap import _catalog
11+
from cmap import Colormap
12+
from cmap._util import report_cvds
13+
14+
# TODO: convert to jinja
15+
TEMPLATE = """# {name}
16+
17+
{aliases}
18+
19+
{info}
20+
21+
| category | license | authors | source |
22+
| --- | --- | --- | --- |
23+
| {category} | {license} | {authors} | {source} |
24+
25+
```python
26+
from cmap import Colormap
27+
28+
cm = Colormap('{name}') # case insensitive
29+
```
30+
31+
{{{{ cmap: {name} 40 }}}}
32+
{{{{ cmap_gray: {name} 30 }}}}
33+
{{{{ cmap_sineramp: {name} }}}}
34+
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+
58+
## Perceptual Lightness
59+
60+
<canvas class="linearity-chart cmap-chart" data-cmap-name="{name}" width="800" height="350"></canvas>
61+
<p style="text-align: center;">
62+
<em style="font-size: small; color: gray;">
63+
L* measured in
64+
<a href="https://onlinelibrary.wiley.com/doi/10.1002/col.20227">CAM02 Uniform Color Space (CAM02-UCS)</a>
65+
</em>
66+
</p>
67+
68+
## RGB components
69+
70+
<canvas class="rgb-chart cmap-chart" data-cmap-name="{name}" width="800" height="350"></canvas>
71+
72+
## Hue & Saturation
73+
74+
<canvas class="hsl-chart cmap-chart" data-cmap-name="{name}" width="800" height="350"></canvas>
75+
76+
77+
<script>
78+
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+
92+
<!-- Note: this is here because of `navigation.instant` in the mkdocs settings -->
93+
typeof(initCharts) !== 'undefined' && initCharts();
94+
</script>
95+
"""
96+
97+
DOCS = Path(__file__).parent
98+
LICENSE_URL = {
99+
"CC0": "https://creativecommons.org/publicdomain/zero/1.0/",
100+
"CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/",
101+
"Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0",
102+
"MIT": "https://opensource.org/licenses/MIT",
103+
"BSD-3-Clause": "https://opensource.org/licenses/BSD-3-Clause",
104+
"BSD-2-Clause": "https://opensource.org/licenses/BSD-2-Clause",
105+
"PSF": "https://opensource.org/licenses/Python-2.0",
106+
}
107+
108+
109+
INCLUDE_DATA = (
110+
"x",
111+
"color",
112+
"R",
113+
"G",
114+
"B",
115+
"J",
116+
"lightness_derivs",
117+
"hue",
118+
"saturation",
119+
"chroma",
120+
)
121+
122+
123+
def build_catalog(catalog: "_catalog.Catalog") -> None:
124+
nav = mkdocs_gen_files.Nav()
125+
for name in natsort.natsorted(catalog, alg=natsort.ns.IGNORECASE):
126+
if ":" not in name:
127+
continue
128+
try:
129+
info = catalog[name]
130+
category = info.category
131+
license_: str = info.license
132+
except KeyError as e:
133+
raise KeyError(f"Missing info for {name}: {e}") from e
134+
135+
# FIXME: not a great way to determine aliases...
136+
# prone to false positives if normalization is not perfect/consistent
137+
if info.qualified_name.lower() != name.lower():
138+
# skip aliases
139+
continue
140+
141+
source = info.source
142+
source = f"[{source}]({source})" if source.startswith("http") else f"`{source}`"
143+
authors = ", ".join(info.authors)
144+
if license_ in LICENSE_URL:
145+
license_ = f"[{license_}]({LICENSE_URL[license_]})"
146+
147+
# write data used for charts
148+
cm = Colormap(name)
149+
cmap_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()
156+
}
157+
_aliases = [x for x in info.aliases if x != info.name]
158+
aliases = _make_aliases_md(_aliases) if _aliases else ""
159+
160+
# write the actual markdown file
161+
doc_path = f"{category}/{name.lower()}.md"
162+
nav[(category.title(), name)] = doc_path
163+
with mkdocs_gen_files.open(f"catalog/{doc_path}", "w") as f:
164+
f.write(
165+
TEMPLATE.format(
166+
name=name,
167+
category=category.title(),
168+
license=license_,
169+
authors=authors,
170+
source=source,
171+
aliases=aliases,
172+
info=info.info,
173+
data=json.dumps({name: cmap_data}, separators=(",", ":")),
174+
)
175+
)
176+
177+
# sort categories alphabetically
178+
nav._data = dict(sorted(nav._data.items(), key=lambda x: x[0][0]))
179+
with mkdocs_gen_files.open("catalog/SUMMARY.md", "w") as nav_file:
180+
nav_file.writelines(["# SUMMARY { data-search-exclude }\n"])
181+
nav_file.writelines(nav.build_literate_nav())
182+
183+
184+
def _make_aliases_md(aliases: list[str]) -> str:
185+
return "**Aliases**: " + ", ".join(f"`{a}`" for a in aliases)
186+
187+
188+
build_catalog(Colormap.catalog())

_hooks.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import base64
2+
import re
3+
import sys
4+
from collections.abc import Sequence
5+
from functools import partial
6+
from pathlib import Path
7+
from typing import TYPE_CHECKING, Any, cast
8+
9+
import natsort
10+
import numpy as np
11+
12+
from cmap import Colormap, _util
13+
14+
if TYPE_CHECKING:
15+
from cmap._catalog import CatalogDict
16+
from cmap._color import NAME_TO_RGB
17+
18+
CATALOG = cast("CatalogDict", Colormap.catalog()._data) # type: ignore
19+
20+
# the template for a single colormap
21+
CMAP_DIV = """
22+
<div class="cmap {class_list}" id="cmap-{name}">
23+
<div class="cmap-name">{name}</div>
24+
{img}
25+
</div>
26+
"""
27+
CMAP_LINK = '<a href="{url}">' + CMAP_DIV + "</a>"
28+
DEV_MODE = "serve" in sys.argv
29+
SINERAMP = _util.sineramp((96, 826))[:, ::-1]
30+
31+
32+
def _to_img_tag(
33+
cm: Colormap,
34+
height: str = "32px",
35+
width: str = "100%",
36+
img: np.ndarray | None = None,
37+
) -> str:
38+
"""Return a base64-encoded <img> tag for the given colormap."""
39+
_img = cm._repr_png_(width=826, height=1, img=img)
40+
data = base64.b64encode(_img).decode("ascii")
41+
return (
42+
f'<img style="height: {height}" width="{width}" src="data:image/png;base64,'
43+
f'{data}" alt="{cm.name} colormap" />'
44+
)
45+
46+
47+
def _cm_url(cm: Colormap) -> str:
48+
name = cm.name.lower()
49+
if name.endswith("_r"):
50+
name = name[:-2]
51+
return f"/catalog/{cm.category}/{name}/"
52+
53+
54+
def _cmap_div(match: re.Match | str, class_list: Sequence[str] = ()) -> str:
55+
"""Convert a `cmap` tag to a div with the colormap.
56+
57+
{{ cmap: name }} -> <div class="cmap">
58+
"""
59+
map_name = match if isinstance(match, str) else match[1].strip()
60+
cm = Colormap(map_name)
61+
height = (match[2] if isinstance(match, re.Match) else None) or 32
62+
img = _to_img_tag(cm, height=f"{height}px")
63+
return CMAP_LINK.format(
64+
name=map_name, img=img, class_list=" ".join(class_list), url=_cm_url(cm)
65+
)
66+
67+
68+
def _cmap_expr(match: re.Match) -> str:
69+
"""Convert a `cmap_expr` tag to a div with the colormap.
70+
71+
{{ cmap_expr: {0: 'blue', 0.5: 'yellow', 1: 'red'} }} -> <div class="cmap">...
72+
"""
73+
cm = Colormap(eval(match[1].strip())) # noqa: S307
74+
return CMAP_DIV.format(name="", img=_to_img_tag(cm), class_list="cmap-expr")
75+
76+
77+
def _cmap_sineramp(match: re.Match) -> str:
78+
"""Convert a `cmap_sineramp` tag to an img element with sineramp applied.
79+
80+
{{ cmap_sineramp: viridis }} -> <img class="cmap">...
81+
"""
82+
# if DEV_MODE:
83+
# return "[sineramp slow -- disabled in `mkdocs serve`]"
84+
85+
map_name = match[1].strip()
86+
return _to_img_tag(Colormap(map_name), f"{SINERAMP.shape[0]}px", img=SINERAMP)
87+
88+
89+
def _cmap_catalog() -> str:
90+
"""Return the HTML for the colormap catalog page.
91+
92+
this works in conjunction with the `javascripts/extra.js` script.
93+
"""
94+
categories = set()
95+
lines = []
96+
for cmap_name, details in natsort.natsorted(
97+
CATALOG.items(), key=lambda x: x[0].lower()
98+
):
99+
if "alias" in details:
100+
continue
101+
category = details.get("category") or "Uncategorized"
102+
categories.add(category)
103+
classes = ["filterDiv", category.lower()]
104+
lines.append(_cmap_div(cmap_name, classes))
105+
106+
btns = [
107+
'<div id="cmapFilterButtons">',
108+
"""<button class="btn active" onclick="filterSelection('all')">All</button>""",
109+
]
110+
btns.extend(
111+
f'<button class="btn" onclick="filterSelection({c.lower()!r})">{c}</button>'
112+
for c in sorted(categories)
113+
)
114+
btns.append("</div>")
115+
lines = btns + lines
116+
117+
return "\n".join(lines)
118+
119+
120+
COLORBOX = """<div class="colorbox" style="background-color: {hex}; color: {text_color}">
121+
<strong class="colornamespan">{name}</strong><br />
122+
<span class="colorhexspan">{hex}</span><br />
123+
<span class="colortuple">{rgb}</span><br />
124+
</div>
125+
"""
126+
127+
128+
def _color_list() -> str:
129+
"""Return the HTML for the color list page."""
130+
colors = []
131+
for name, c in NAME_TO_RGB.items():
132+
if (hsl := c.to_hsl()).l == 0.5:
133+
text_color = "#000" if hsl.h <= 0.5 else "#FFF"
134+
else:
135+
text_color = "#000" if hsl.l > 0.50 or not c.a else "#FFF"
136+
cbox = COLORBOX.format(
137+
name=name,
138+
hex=c.to_hex(),
139+
text_color=text_color,
140+
rgb=c.rgba_string(),
141+
)
142+
colors.append(cbox)
143+
return '<div class="colorlist">{}</div>'.format("\n".join(colors))
144+
145+
146+
# -----------------------------------------------------------------------------
147+
# mkdocs hooks
148+
# -----------------------------------------------------------------------------
149+
150+
# markdown tag for a single colormap: {{ cmap: name }}
151+
CSS_CMAP = re.compile(r"{{\s?cmap:\s?([^}^\s]+)\s?(\d+)?\s?}}")
152+
CSS_CMAP_GRAY = re.compile(r"{{\s?cmap_gray:\s?([^}^\s]+)\s?(\d+)?\s?}}")
153+
CMAP_SINERAMP = re.compile(r"{{\s?cmap_sineramp:\s?([^}]+)\s?}}")
154+
CMAP_EXPR = re.compile(r"{{\s?cmap_expr:\s(.+)\s}}")
155+
CMAP_CATALOG = r"{{ CMAP_CATALOG }}"
156+
COLOR_LIST = r"{{ COLOR_LIST }}"
157+
158+
159+
def on_page_content(html: str, **kwargs: Any) -> str:
160+
html = CSS_CMAP.sub(_cmap_div, html)
161+
html = CSS_CMAP_GRAY.sub(partial(_cmap_div, class_list=["grayscale"]), html)
162+
html = CMAP_EXPR.sub(_cmap_expr, html)
163+
html = CMAP_SINERAMP.sub(_cmap_sineramp, html)
164+
if CMAP_CATALOG in html:
165+
html = html.replace(CMAP_CATALOG, _cmap_catalog())
166+
if COLOR_LIST in html:
167+
html = html.replace(COLOR_LIST, _color_list())
168+
return html
169+
170+
171+
REDIRECT_TEMPLATE = """<!doctype html>
172+
<html lang="en">
173+
<head>
174+
<meta charset="utf-8">
175+
<title>Redirecting...</title>
176+
<link rel="canonical" href="{url}">
177+
<meta name="robots" content="noindex">
178+
<script>
179+
var anchor=window.location.hash.substr(1);location.href="{url}"+(anchor?"#"+anchor:"")
180+
</script>
181+
<meta http-equiv="refresh" content="0; url={url}">
182+
</head>
183+
<body>
184+
Redirecting...
185+
</body>
186+
</html>
187+
"""
188+
189+
190+
def _write_cmap_redirects(site_dir: str) -> None:
191+
sd = Path(site_dir)
192+
for cmap_name, details in natsort.natsorted(
193+
CATALOG.items(), key=lambda x: x[0].lower()
194+
):
195+
if "alias" in details:
196+
cmap_name = cmap_name.replace(":", "-")
197+
real = Colormap(details["alias"]) # type: ignore
198+
old_path_abs = (
199+
sd / "catalog" / str(real.category) / cmap_name / "index.html"
200+
)
201+
old_path_abs.parent.mkdir(parents=True, exist_ok=True)
202+
content = REDIRECT_TEMPLATE.format(url=_cm_url(real))
203+
with open(old_path_abs, "w", encoding="utf-8") as f:
204+
f.write(content)
205+
206+
207+
def on_post_build(config: dict, **kwargs: Any) -> None:
208+
"""Copy the extra javascripts to the output directory."""
209+
_write_cmap_redirects(config["site_dir"])

0 commit comments

Comments
 (0)