feat(types): add treemap for part-of-whole by area (#87)

Add Treemap as the 28th visual type with light, dark, and editorial variants; area, label, and information-marker verification; synchronized documentation and manifests; and plugin version 2.5.0.
This commit is contained in:
0xDarkMatter
2026-08-18 00:50:19 -07:00
committed by GitHub
parent 72f17665cb
commit de75c0adb5
21 changed files with 1072 additions and 36 deletions
+2 -2
View File
@@ -402,7 +402,7 @@ def main() -> int:
missing_guide = directory / "missing-guide.md"
missing_guide.write_text(
original_skill.read_text(encoding="utf-8").replace(
"### Visual-type guide (27)", "### Visual guide"
"### Visual-type guide (28)", "### Visual guide"
),
encoding="utf-8",
)
@@ -410,7 +410,7 @@ def main() -> int:
markdown_errors = semantic_module.verify_markdown()
finally:
semantic_module.SKILL = original_skill
if not any("must contain the 27-row visual-type guide" in error for error in markdown_errors):
if not any("must contain the 28-row visual-type guide" in error for error in markdown_errors):
raise AssertionError(f"missing visual-guide anchor was accepted: {markdown_errors}")
print("OK: missing visual-type guide anchor is rejected")
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
"""Adversarial tests for verify-treemap.py — both polarities.
Per ADR 0005, a geometric contract in this repo is a checker plus fixtures that
prove it fires when it should and stays quiet when it shouldn't. The mutations
below are the two defects that actually shipped in review: a sliver cell drawn a
third too small, and a label longer than the cell holding it.
Usage: python3 scripts/test-verify-treemap.py
Exit: 0 all pass, 1 a case failed.
"""
from __future__ import annotations
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
CHECKER = ROOT / "scripts/verify-treemap.py"
GOOD = ROOT / "skills/diagram-design/assets/example-treemap.html"
def run(path: Path) -> tuple[int, str]:
result = subprocess.run(
[sys.executable, str(CHECKER), str(path)],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
return result.returncode, (result.stdout or "") + (result.stderr or "")
def write(directory: Path, name: str, source: str) -> Path:
path = directory / name
path.write_text(source, encoding="utf-8")
return path
def main() -> int:
source = GOOD.read_text(encoding="utf-8")
failures: list[str] = []
with tempfile.TemporaryDirectory() as raw:
directory = Path(raw)
# 1. The shipped example must pass untouched.
code, output = run(write(directory, "clean.html", source))
if code != 0:
failures.append(f"clean example was rejected: {output.strip()}")
else:
print("OK: the shipped treemap passes")
# 2. Shrink a labelled cell below the share it prints. This is the
# defect that shipped in review — a cell drawn smaller than it claims
# — caught here on Africa, which carries a label to contradict.
shrunk = source.replace(
'<rect x="576" y="40" width="252" height="252"',
'<rect x="576" y="40" width="200" height="252"',
)
if shrunk == source:
failures.append("could not build the undersized-cell fixture (anchor moved)")
else:
code, output = run(write(directory, "undersized.html", shrunk))
if code == 0:
failures.append("cell drawn under its stated share was accepted")
elif "relative" not in output:
failures.append(f"undersized cell reported without an area finding: {output.strip()}")
else:
print("OK: a cell drawn smaller than its label is rejected")
# 3. Inflate Asia instead — the opposite direction of the same defect.
# Both rects move: a cell is painted as a paper mask then a body, and
# mutating only one leaves the original size still present, which the
# smallest-host rule would quietly measure instead.
inflated = source.replace(
'<rect x="40" y="40" width="532" height="380"',
'<rect x="40" y="40" width="700" height="380"',
)
if inflated == source:
failures.append("could not build the oversized-cell fixture (anchor moved)")
else:
code, _ = run(write(directory, "oversized.html", inflated))
if code == 0:
failures.append("oversized focal cell was accepted")
else:
print("OK: a cell drawn larger than its label is rejected")
# 4. Overrun a label past its own cell.
overflowing = source.replace(
">South America</text>",
">South America and the Caribbean Basin</text>",
)
if overflowing == source:
failures.append("could not build the overflowing-label fixture (anchor moved)")
else:
code, output = run(write(directory, "overflow.html", overflowing))
if code == 0:
failures.append("label overflowing its cell was accepted")
elif "overflows" not in output:
failures.append(f"overflow reported without a fit finding: {output.strip()}")
else:
print("OK: a label wider than its cell is rejected")
# 4b. Overflow off the LEFT edge, via an end-anchored label. Checking
# only the right and bottom edges would pass this silently, and the
# text would read as belonging to the cell next door.
left_over = source.replace(
'<text x="804" y="324" fill="#2d3142" font-size="12" font-weight="600"',
'<text x="804" y="324" text-anchor="end" fill="#2d3142" font-size="12" font-weight="600"',
)
if left_over == source:
failures.append("could not build the left-overflow fixture (anchor moved)")
else:
code, output = run(write(directory, "leftoverflow.html", left_over))
if code == 0:
failures.append("label hanging off the left of its cell was accepted")
elif "left" not in output:
failures.append(f"left overflow not named in the finding: {output.strip()}")
else:
print("OK: a label overflowing the left edge is rejected")
# 4c. Overflow off the TOP edge — the label's ascent clears the cell.
top_over = source.replace(
'<text x="592" y="68" fill="#2d3142" font-size="13"',
'<text x="592" y="44" fill="#2d3142" font-size="13"',
)
if top_over == source:
failures.append("could not build the top-overflow fixture (anchor moved)")
else:
code, output = run(write(directory, "topoverflow.html", top_over))
if code == 0:
failures.append("label overflowing the top of its cell was accepted")
elif "top" not in output:
failures.append(f"top overflow not named in the finding: {output.strip()}")
else:
print("OK: a label overflowing the top edge is rejected")
# 4d. A sliver can be narrower than the fixed 10px information-mark
# disc. The marker must never cross into the neighbouring cell.
marker_over = source.replace(
'<rect x="940" y="296" width="16" height="124"',
'<rect x="940" y="296" width="8" height="124"',
).replace('cx="948" cy="358"', 'cx="944" cy="358"').replace(
'<text x="948" y="361"', '<text x="944" y="361"'
)
if marker_over == source:
failures.append("could not build the overflowing-marker fixture (anchor moved)")
else:
code, output = run(write(directory, "markeroverflow.html", marker_over))
if code == 0:
failures.append("information mark overflowing a sliver cell was accepted")
elif "information marker" not in output:
failures.append(f"marker overflow not named in the finding: {output.strip()}")
else:
print("OK: an information mark overflowing its cell is rejected")
# 5. The shipped example leaves its smallest cell deliberately
# unlabelled. That must not switch the area check off for the cells
# that ARE labelled — the failure mode where a checker reports clean
# because it found nothing to compare.
unlabelled_shrunk = source.replace(
'<rect x="576" y="296" width="208" height="124"',
'<rect x="576" y="296" width="140" height="124"',
)
if unlabelled_shrunk == source:
failures.append("could not build the still-checks-labelled-cells fixture")
else:
code, _ = run(write(directory, "unlabelled.html", unlabelled_shrunk))
if code == 0:
failures.append(
"area check went silent when a cell was unlabelled — a labelled "
"cell was resized and no finding was reported"
)
else:
print("OK: an unlabelled sliver does not disable the area check")
for failure in failures:
print(f"FAIL: {failure}")
if failures:
print(f"\n{len(failures)} case(s) failed.")
return 1
print("\nOK verify-treemap: both polarities behave")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+61 -3
View File
@@ -11,10 +11,15 @@ Five drift classes, each of which has shipped before:
3. Every concrete file named in README.md's architecture tree must exist.
4. Every relative references/*.md link in SKILL.md must resolve.
5. Claude and Pi profile surfaces must both route to the profile reference.
6. The plugin manifests repeat the SKILL.md description verbatim. They are the
text a user reads *before installing*, so by ADR 0004's own argument they
need every type's lexical hook too - and nothing else notices when they
drift, because they are four separate copies of one sentence.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
@@ -67,8 +72,8 @@ def check_description(errors: list[str]) -> None:
errors.append("SKILL.md frontmatter description is missing")
return
types = selection_table_types(markdown)
if len(types) != 27:
errors.append(f"expected 27 visual types in the selection table; found {len(types)}")
if len(types) != 28:
errors.append(f"expected 28 visual types in the selection table; found {len(types)}")
for name in types:
key = normalized(name)
key = DESCRIPTION_ALIASES.get(key, key)
@@ -151,9 +156,62 @@ def check_profile_surfaces(errors: list[str], root: Path) -> None:
)
MANIFEST_DESCRIPTIONS = (
(Path(".claude-plugin/plugin.json"), ("description",)),
(Path(".claude-plugin/marketplace.json"), ("description",)),
(Path(".codex-plugin/plugin.json"), ("description", "longDescription")),
)
def find_key(node: object, key: str) -> str | None:
"""First value for *key* anywhere in a nested JSON document."""
if isinstance(node, dict):
if isinstance(node.get(key), str):
return node[key]
for value in node.values():
found = find_key(value, key)
if found is not None:
return found
elif isinstance(node, list):
for value in node:
found = find_key(value, key)
if found is not None:
return found
return None
def check_manifest_descriptions(errors: list[str], root: Path) -> None:
markdown = SKILL.read_text(encoding="utf-8")
description = normalized(frontmatter_description(markdown))
if not description:
return
types = selection_table_types(markdown)
for relative, keys in MANIFEST_DESCRIPTIONS:
path = root / relative
if not path.exists():
errors.append(f"missing plugin manifest: {relative.as_posix()}")
continue
document = json.loads(path.read_text(encoding="utf-8"))
for key in keys:
value = find_key(document, key)
if value is None:
errors.append(f"{relative.as_posix()} has no {key!r}")
continue
text = normalized(value)
for name in types:
hook = DESCRIPTION_ALIASES.get(normalized(name), normalized(name))
if hook not in text:
errors.append(
f"{relative.as_posix()} {key!r} lost the lexical hook for "
f"type {name!r} (expected {hook!r}) — it must name every "
f"type the SKILL.md description names"
)
def main() -> int:
errors: list[str] = []
check_description(errors)
check_manifest_descriptions(errors, ROOT)
check_gallery(errors)
check_readme_tree(errors)
check_skill_reference_links(
@@ -169,7 +227,7 @@ def main() -> int:
return 1
print(
"OK docs sync: description hooks, gallery reachability, README tree, "
"reference links, profile surfaces"
"reference links, profile surfaces, manifest descriptions"
)
return 0
+6 -6
View File
@@ -191,18 +191,18 @@ def verify_markdown() -> list[str]:
if "Selection: semantic pattern, then visual type" not in skill:
errors.append("SKILL.md must choose semantic pattern before visual type")
router_position = skill.find("semantic-patterns.md")
guide_position = skill.find("### Visual-type guide (27)")
guide_position = skill.find("### Visual-type guide (28)")
if router_position < 0:
errors.append("SKILL.md must link to semantic-patterns.md")
if guide_position < 0:
errors.append("SKILL.md must contain the 27-row visual-type guide")
errors.append("SKILL.md must contain the 28-row visual-type guide")
if router_position >= 0 and guide_position >= 0 and router_position > guide_position:
errors.append("semantic-pattern router must precede the visual-type guide")
visual_guide = section(skill, "### Visual-type guide (27)", "Rules of thumb:")
visual_guide = section(skill, "### Visual-type guide (28)", "Rules of thumb:")
visual_rows = re.findall(r"^\|.*\[type-[^)]+\.md\]", visual_guide, re.MULTILINE)
if len(visual_rows) != 27:
errors.append(f"visual-type guide must preserve 27 rows; found {len(visual_rows)}")
if len(visual_rows) != 28:
errors.append(f"visual-type guide must preserve 28 rows; found {len(visual_rows)}")
for index, name in enumerate(PATTERN_NAMES, 1):
if name not in skill:
@@ -419,7 +419,7 @@ def main() -> int:
print(f"- {error}")
return 1
if not args.example_only:
print("OK: 7 semantic patterns route independently to the preserved 27 visual types")
print("OK: 7 semantic patterns route independently to the preserved 28 visual types")
print("OK: animation modes, primitives, static fallback, and accessibility contract")
if not args.markdown_only:
print("OK: policy-trace example controls, structure, reduced motion, and unique IDs")
+356
View File
@@ -0,0 +1,356 @@
#!/usr/bin/env python3
"""Verify the two invariants a treemap can silently break.
A treemap's whole claim is that **area is the encoding**. Both ways of breaking
that claim render perfectly - nothing errors, nothing disappears, and neither
`lint-skin.py` (colors, fonts, a11y) nor `verify-geometry.py` (label masks vs
later-painted nodes) reads a cell against the number printed inside it.
1. AREA FIDELITY - a cell's share of the drawn area must match the share its
own label claims. This is checked as **relative** error, not percentage
points: a 0.13pp slip is noise on a 59% cell and a quarter of a 0.5% cell,
so an absolute threshold passes exactly the cell most likely to be wrong.
The failure is real - the smallest cell absorbs the whole 4px-grid residue,
because 4px is a third of a sliver and a rounding error on a giant.
2. LABEL AND MARKER FIT - a label or information marker must stay inside the
cell it names. Cells have no clip path, so overflow bleeds into a neighbour
or off the viewBox and reads as belonging to the wrong thing. A cell too
small for its annotation is not a nudge-the-coordinates problem: drop the
annotation (the cell keeps its honest area) or merge the tail into a named
"Other".
Text extent is estimated from font metrics rather than measured in a browser,
deliberately: every other gate here is pure Python and runs in CI with no
browser. The advances below are calibrated against Chromium renderings of the
shipped Geist / Geist Mono faces and rounded UP, so the estimate reports
overflow slightly before real overflow, never after.
Usage:
python3 scripts/verify-treemap.py --all
python3 scripts/verify-treemap.py skills/diagram-design/assets/example-treemap.html
Exit: 0 clean, 1 findings, 2 usage.
"""
from __future__ import annotations
import argparse
import html
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ASSET_DIR = ROOT / "skills/diagram-design/assets"
CELL_RE = re.compile(
r"<rect\b[^>]*?"
r'\bx="(?P<x>-?[\d.]+)"\s+y="(?P<y>-?[\d.]+)"\s+'
r'width="(?P<w>[\d.]+)"\s+height="(?P<h>[\d.]+)"[^>]*?'
r'rx="2"',
re.IGNORECASE,
)
TEXT_RE = re.compile(r"<text\b(?P<attrs>[^>]*)>(?P<body>.*?)</text>", re.IGNORECASE | re.DOTALL)
CIRCLE_RE = re.compile(r"<circle\b(?P<attrs>[^>]*)/?>", re.IGNORECASE)
ATTR_RE = re.compile(r'(?P<name>[\w:-]+)="(?P<value>[^"]*)"')
ROTATE_RE = re.compile(r"rotate\(\s*(?P<deg>-?[\d.]+)")
TAG_RE = re.compile(r"<[^>]+>")
# A cell states its size one of two ways, and the two must not be confused: a
# magnitude ("4.78B") is a value to be normalised against the other cells, while
# a percentage ("59%", "0.6%") is already the claimed share. Reading "0.6%" as
# the number 0.6 against cells measured in billions produces nonsense, so the
# percentage pattern is tried first and anchored on the sign.
PCT_RE = re.compile(r"(?P<num>\d[\d,]*(?:\.\d+)?)\s*%")
VALUE_RE = re.compile(r"(?P<num>\d[\d,]*(?:\.\d+)?)\s*(?P<unit>[BMK])\b")
MONO_ADVANCE = 0.62
SANS_ADVANCE = 0.60
ASCENT = 0.74
UNITS = {None: 1.0, "K": 1e3, "M": 1e6, "B": 1e9}
# A treemap cell is large enough to hold a label and small enough not to be a
# page background or diagram container.
CELL_MIN_AREA = 400.0
CELL_MAX_W = 900.0
CELL_MAX_H = 460.0
EPSILON = 1.0
# Relative area tolerance. Grid snapping on a 1000x500 viewBox cannot do better
# than a couple of percent on a sub-1% cell; 8% is loose enough never to fire on
# honest rounding and tight enough to have caught the 30%-undersized sliver this
# check exists for.
AREA_TOLERANCE = 8.0
MARKER_EPSILON = 0.01
class Box:
__slots__ = ("x", "y", "w", "h", "offset")
def __init__(self, x: float, y: float, w: float, h: float, offset: int = 0) -> None:
self.x, self.y, self.w, self.h, self.offset = x, y, w, h, offset
@property
def right(self) -> float:
return self.x + self.w
@property
def bottom(self) -> float:
return self.y + self.h
@property
def area(self) -> float:
return self.w * self.h
def contains_point(self, x: float, y: float) -> bool:
return (
self.x - EPSILON <= x <= self.right + EPSILON
and self.y - EPSILON <= y <= self.bottom + EPSILON
)
def line_of(source: str, offset: int) -> int:
return source.count("\n", 0, offset) + 1
def plain(body: str) -> str:
return html.unescape(TAG_RE.sub("", body)).strip()
def parse_cells(source: str) -> list[Box]:
"""Deduped cell rects. Each cell is painted twice: paper mask, then body."""
seen: list[Box] = []
for m in CELL_RE.finditer(source):
box = Box(
float(m.group("x")),
float(m.group("y")),
float(m.group("w")),
float(m.group("h")),
m.start(),
)
if box.area < CELL_MIN_AREA or box.w > CELL_MAX_W or box.h > CELL_MAX_H:
continue
if any(
abs(s.x - box.x) < 0.01
and abs(s.y - box.y) < 0.01
and abs(s.w - box.w) < 0.01
and abs(s.h - box.h) < 0.01
for s in seen
):
continue
seen.append(box)
return seen
def label_box(attrs: dict[str, str], body: str) -> Box | None:
"""Estimated rendered box of a <text>, in root user space."""
try:
x = float(attrs["x"])
y = float(attrs["y"])
size = float(attrs.get("font-size", "12"))
except (KeyError, ValueError):
return None
chars = len(plain(body))
if not chars:
return None
mono = "mono" in attrs.get("font-family", "").lower()
width = chars * size * (MONO_ADVANCE if mono else SANS_ADVANCE)
anchor = attrs.get("text-anchor", "start")
transform = attrs.get("transform", "")
if transform:
m = ROTATE_RE.search(transform)
# Only the quarter-turn cases are modelled; anything else is skipped
# rather than guessed at.
if m is None or abs(abs(float(m.group("deg"))) - 90.0) > 0.01:
return None
if anchor == "middle":
top = y - width / 2
elif anchor == "end":
top = y
else:
top = y - width
return Box(x - size * ASCENT, top, size, width)
if anchor == "middle":
left = x - width / 2
elif anchor == "end":
left = x - width
else:
left = x
return Box(left, y - size * ASCENT, width, size)
def _number(match: re.Match[str] | None) -> float | None:
if match is None:
return None
try:
return float(match.group("num").replace(",", ""))
except ValueError:
return None
def parse_claim(text: str) -> tuple[float | None, float | None]:
"""(percentage, value) claimed by a label. Either may be absent."""
percentage = _number(PCT_RE.search(text))
value_match = VALUE_RE.search(text)
value = _number(value_match)
if value is not None and value_match is not None:
value *= UNITS[value_match.group("unit")]
return percentage, value
def check(path: Path) -> list[str]:
source = path.read_text(encoding="utf-8")
cells = parse_cells(source)
if len(cells) < 3:
return []
findings: list[str] = []
labels: dict[int, list[str]] = {index: [] for index in range(len(cells))}
for m in TEXT_RE.finditer(source):
attrs = {a.group("name"): a.group("value") for a in ATTR_RE.finditer(m.group("attrs"))}
box = label_box(attrs, m.group("body"))
if box is None:
continue
# Host is resolved from the text's ANCHOR, not its box corner. An
# end- or middle-anchored label extends left of its anchor, so a box
# corner can land outside the very cell the label belongs to - which
# would silently attribute it to a neighbour, or to nothing at all.
anchor_x, anchor_y = float(attrs["x"]), float(attrs["y"])
host_index = None
for index, cell in enumerate(cells):
if not cell.contains_point(anchor_x, anchor_y):
continue
if host_index is None or cell.area < cells[host_index].area:
host_index = index
if host_index is None:
continue
host = cells[host_index]
labels[host_index].append(plain(m.group("body")))
# All four edges. Checking only right and bottom passes an end-anchored
# label hanging off the left, or a label whose ascent clears the top -
# both of which read as belonging to the neighbouring cell.
overflow = {
"left": host.x - box.x,
"top": host.y - box.y,
"right": box.right - host.right,
"bottom": box.bottom - host.bottom,
}
breached = {side: over for side, over in overflow.items() if over > EPSILON}
if breached:
detail = " / ".join(f"{over:.1f} {side}" for side, over in breached.items())
findings.append(
f"{path.name}:{line_of(source, m.start())}: label "
f"{plain(m.group('body'))[:38]!r} overflows its {host.w:g}x{host.h:g} cell "
f"by {detail} — drop the label or merge the cell into a named Other"
)
# Information marks are 8–12px discs placed inside sliver cells. Their
# one-letter text can fit while the surrounding disc still crosses a cell
# boundary, so circles need their own containment check.
for m in CIRCLE_RE.finditer(source):
attrs = {a.group("name"): a.group("value") for a in ATTR_RE.finditer(m.group("attrs"))}
try:
cx = float(attrs["cx"])
cy = float(attrs["cy"])
radius = float(attrs["r"])
except (KeyError, ValueError):
continue
if not 4.0 <= radius <= 6.0:
continue
host_index = None
for index, cell in enumerate(cells):
if not cell.contains_point(cx, cy):
continue
if host_index is None or cell.area < cells[host_index].area:
host_index = index
if host_index is None:
continue
host = cells[host_index]
overflow = {
"left": host.x - (cx - radius),
"top": host.y - (cy - radius),
"right": (cx + radius) - host.right,
"bottom": (cy + radius) - host.bottom,
}
breached = {
side: over for side, over in overflow.items() if over > MARKER_EPSILON
}
if breached:
detail = " / ".join(f"{over:.1f} {side}" for side, over in breached.items())
findings.append(
f"{path.name}:{line_of(source, m.start())}: information marker "
f"overflows its {host.w:g}x{host.h:g} cell by {detail} — "
"drop the marker and identify the sliver in the legend"
)
claims = {index: parse_claim(" ".join(texts)) for index, texts in labels.items()}
# Only compare on a basis every cell states, so the shares share a
# denominator. Percentages win when present: they are the claim itself.
percentages = {i: p for i, (p, _) in claims.items() if p}
magnitudes = {i: v for i, (_, v) in claims.items() if v}
known = percentages if len(percentages) >= len(magnitudes) else magnitudes
# Normalise both sides over the labelled cells only. A sliver too small to
# label is legal - it keeps its honest area and is named in the source line
# - and it must not switch this check off for the cells that do carry a
# claim, which is what comparing against the full drawn area would do.
if len(known) >= 3:
drawn_total = sum(cells[index].area for index in known)
value_total = sum(known.values())
for index, value in sorted(known.items()):
cell = cells[index]
area_share = cell.area / drawn_total * 100
value_share = value / value_total * 100
relative = (area_share - value_share) / value_share * 100
if abs(relative) > AREA_TOLERANCE:
findings.append(
f"{path.name}:{line_of(source, cell.offset)}: cell "
f"{cell.w:g}x{cell.h:g} labelled {' '.join(labels[index])[:30]!r} draws "
f"{area_share:.2f}% of the area for a {value_share:.2f}% value — "
f"{relative:+.1f}% relative. Area is the only encoding; resize the cell"
)
return findings
def targets(args: argparse.Namespace) -> list[Path]:
if args.all:
return sorted(ASSET_DIR.glob("example-treemap*.html"))
return [Path(p) for p in args.paths]
def main() -> int:
parser = argparse.ArgumentParser(
description="Verify treemap area fidelity and label/marker fit."
)
parser.add_argument("paths", nargs="*", help="HTML files to check")
parser.add_argument("--all", action="store_true", help="check every shipped treemap example")
args = parser.parse_args()
if not args.all and not args.paths:
parser.print_help()
return 2
findings: list[str] = []
checked = 0
for path in targets(args):
if not path.exists():
print(f"error: {path} does not exist", file=sys.stderr)
return 2
findings.extend(check(path))
checked += 1
for finding in findings:
print(finding)
if findings:
print(f"\n{len(findings)} treemap finding(s) across {checked} file(s).")
return 1
print(
f"OK treemap: {checked} file(s), area matches labels and every label/marker fits its cell"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())