Skip to content

Commit 0b688a5

Browse files
committed
Fix gallery viewer zoom and labels
1 parent e29f288 commit 0b688a5

5 files changed

Lines changed: 49 additions & 10 deletions

File tree

bookviz/gallery.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import shutil
77
from pathlib import Path
88

9-
from .metrics import ALL_METRICS
9+
from .metrics import ALL_METRICS, TOKEN_METRICS
1010
from .pipeline import RenderOptions, render_book, subtitle
1111
from .text import slugify
1212

@@ -35,7 +35,9 @@ def generate_gallery(
3535
for metric in metrics:
3636
if metric not in ALL_METRICS:
3737
raise ValueError(f"Unknown metric: {metric}")
38-
options = RenderOptions(metric=metric, color=color, window_size=window_size, window_step=window_step)
38+
metric_window_size = None if metric in TOKEN_METRICS else window_size
39+
metric_window_step = None if metric in TOKEN_METRICS else window_step
40+
options = RenderOptions(metric=metric, color=color, window_size=metric_window_size, window_step=metric_window_step)
3941
slug = f"{slugify(book.stem)}-{slugify(metric)}"
4042
png_path = assets_dir / f"{slug}.png"
4143
result = render_book(book, png_path, options, html=True)
@@ -98,4 +100,3 @@ def index_html(pages: list[dict[str, str]]) -> str:
98100
</body>
99101
</html>
100102
"""
101-

bookviz/html_viewer.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,9 @@ def viewer_html(image: Image.Image, *, title: str, subtitle: str, labels: list[s
6161
#stage.dragging {{ cursor: grabbing; }}
6262
#wrapper {{ position: absolute; transform-origin: 0 0; }}
6363
img {{ display: block; image-rendering: pixelated; image-rendering: crisp-edges; }}
64-
#tip {{ position: fixed; display: none; pointer-events: none; z-index: 3; max-width: 320px; padding: 8px 10px; background: rgba(0,0,0,.9); border: 1px solid #555; border-radius: 4px; font-size: 12px; }}
64+
#tip {{ position: fixed; display: none; pointer-events: none; z-index: 3; max-width: 360px; padding: 8px 10px; background: rgba(0,0,0,.9); border: 1px solid #555; border-radius: 4px; font-size: 12px; }}
6565
#tip strong {{ display: block; margin-bottom: 4px; font-size: 14px; }}
66+
#tip span {{ overflow-wrap: anywhere; }}
6667
</style>
6768
</head>
6869
<body>
@@ -94,7 +95,12 @@ def viewer_html(image: Image.Image, *, title: str, subtitle: str, labels: list[s
9495
let scale = 1, panX = 0, panY = 0, dragging = false, startX = 0, startY = 0, startPanX = 0, startPanY = 0;
9596
function update() {{ wrapper.style.transform = `translate(${{panX}}px, ${{panY}}px) scale(${{scale}})`; zoom.textContent = Math.round(scale * 100) + "%"; }}
9697
function center() {{ const r = stage.getBoundingClientRect(); panX = (r.width - image.width * scale) / 2; panY = (r.height - image.height * scale) / 2; update(); }}
97-
function resetView() {{ scale = 1; center(); }}
98+
function initialScale() {{
99+
const r = stage.getBoundingClientRect();
100+
const fit = Math.min(r.width / image.width, r.height / image.height) * 0.88;
101+
return Math.max(1, Math.min(64, fit));
102+
}}
103+
function resetView() {{ scale = initialScale(); center(); }}
98104
function zoomBy(factor) {{ scale = Math.max(.1, Math.min(200, scale * factor)); center(); }}
99105
stage.addEventListener("wheel", (event) => {{
100106
event.preventDefault();
@@ -118,9 +124,8 @@ def viewer_html(image: Image.Image, *, title: str, subtitle: str, labels: list[s
118124
tip.style.left = event.clientX + 14 + "px"; tip.style.top = event.clientY + 14 + "px"; tip.style.display = "block";
119125
}} else {{ tip.style.display = "none"; }}
120126
}});
121-
addEventListener("load", center); addEventListener("resize", center);
127+
addEventListener("load", resetView); addEventListener("resize", resetView);
122128
</script>
123129
</body>
124130
</html>
125131
"""
126-

bookviz/metrics.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def metric_values(tokens: list[str], metric: str, *, window_size: int | None = N
129129

130130
def window_metric_values(tokens: list[str], metric: str, *, window_size: int, window_step: int | None = None) -> MetricResult:
131131
chunks = windows(tokens, window_size, window_step)
132-
labels = [f"tokens {index * (window_step or window_size)}-{index * (window_step or window_size) + len(chunk) - 1}" for index, chunk in enumerate(chunks)]
132+
labels = [window_label(chunk, index, window_step or window_size) for index, chunk in enumerate(chunks)]
133133
if metric in WINDOW_METRICS:
134134
return [WINDOW_METRICS[metric](chunk) for chunk in chunks], labels
135135
if metric in TOKEN_METRICS:
@@ -141,11 +141,20 @@ def window_metric_values(tokens: list[str], metric: str, *, window_size: int, wi
141141
raise ValueError(f"Unknown metric: {metric}")
142142

143143

144+
def window_label(chunk: list[str], index: int, step: int) -> str:
145+
start = index * step
146+
end = start + len(chunk) - 1
147+
words = [token for token in chunk if not is_punctuation(token)]
148+
excerpt = " ".join(words[:14])
149+
if len(words) > 14:
150+
excerpt += " ..."
151+
return f"tokens {start}-{end}: {excerpt}" if excerpt else f"tokens {start}-{end}"
152+
153+
144154
def normalize(values: list[float], *, domain: tuple[float, float] | None = None) -> list[float]:
145155
if not values:
146156
return []
147157
low, high = domain if domain else (min(values), max(values))
148158
if high == low:
149159
return [0.0 for _value in values]
150160
return [(value - low) / (high - low) for value in values]
151-

tests/test_gallery.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from pathlib import Path
2+
3+
from bookviz.gallery import generate_gallery
4+
5+
6+
def test_gallery_keeps_token_metrics_at_word_level(tmp_path: Path):
7+
books = tmp_path / "books"
8+
site = tmp_path / "site"
9+
books.mkdir()
10+
(books / "sample.txt").write_text("one two one three", encoding="utf-8")
11+
12+
generate_gallery(
13+
books,
14+
site,
15+
metrics=["word-freq", "lexical-diversity"],
16+
color="red-blue",
17+
window_size=2,
18+
window_step=None,
19+
)
20+
21+
word_freq_html = (site / "sample-word-freq.html").read_text(encoding="utf-8")
22+
lexical_html = (site / "sample-lexical-diversity.html").read_text(encoding="utf-8")
23+
assert '"one"' in word_freq_html
24+
assert "tokens 0-1: one two" in lexical_html

tests/test_metrics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ def test_window_metric_values():
2222
values, labels = metric_values(["a", "b", "a", ".", "c"], "lexical-diversity", window_size=4)
2323
assert values == [2 / 3, 1.0]
2424
assert len(labels) == 2
25+
assert labels[0] == "tokens 0-3: a b a"
2526

2627

2728
def test_normalize_with_shared_domain():
2829
assert normalize([10, 15, 20], domain=(10, 20)) == [0.0, 0.5, 1.0]
29-

0 commit comments

Comments
 (0)