viseda 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from viseda.report.html_report import generate_html_report
2
+
3
+ __all__ = ["generate_html_report"]
@@ -0,0 +1,213 @@
1
+ """
2
+ viseda.report.html_report
3
+ -------------------------
4
+ Generates a self-contained HTML EDA report from a summary dict.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+
13
+
14
+ _TEMPLATE = """<!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8"/>
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
19
+ <title>{title}</title>
20
+ <style>
21
+ :root {{
22
+ --bg: #0d1117; --surface: #161b22; --border: #30363d;
23
+ --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff;
24
+ --green: #3fb950; --red: #f78166; --yellow: #e3b341;
25
+ }}
26
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
27
+ body {{ background: var(--bg); color: var(--text);
28
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
29
+ padding: 2rem; }}
30
+ h1 {{ font-size: 1.8rem; margin-bottom: 0.25rem; }}
31
+ h2 {{ font-size: 1.1rem; color: var(--accent); margin: 1.5rem 0 0.5rem; }}
32
+ h3 {{ font-size: 0.9rem; color: var(--muted); margin: 1rem 0 0.3rem; }}
33
+ .subtitle {{ color: var(--muted); margin-bottom: 2rem; font-size: 0.9rem; }}
34
+ .grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
35
+ gap: 1rem; }}
36
+ .card {{ background: var(--surface); border: 1px solid var(--border);
37
+ border-radius: 8px; padding: 1rem; }}
38
+ .card h3 {{ font-size: 0.8rem; color: var(--muted); margin-bottom: 0.5rem;
39
+ text-transform: uppercase; letter-spacing: 0.05em; }}
40
+ .stat {{ display: flex; justify-content: space-between; font-size: 0.85rem;
41
+ padding: 0.2rem 0; border-bottom: 1px solid var(--border); }}
42
+ .stat:last-child {{ border-bottom: none; }}
43
+ .val {{ font-variant-numeric: tabular-nums; color: var(--accent); }}
44
+ .badge {{ display: inline-block; padding: 0.15rem 0.5rem; border-radius: 12px;
45
+ font-size: 0.75rem; font-weight: 600; margin: 0.15rem; }}
46
+ .badge-blue {{ background: rgba(88,166,255,0.15); color: var(--accent); }}
47
+ .badge-green {{ background: rgba(63,185,80,0.15); color: var(--green); }}
48
+ .badge-red {{ background: rgba(247,129,102,0.15); color: var(--red); }}
49
+ .badge-yellow {{ background: rgba(227,179,65,0.15); color: var(--yellow); }}
50
+ .bar-wrap {{ margin-top: 0.5rem; }}
51
+ .bar-row {{ display: flex; align-items: center; gap: 0.5rem; margin: 0.2rem 0; font-size: 0.78rem; }}
52
+ .bar-label {{ width: 110px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }}
53
+ .bar {{ flex: 1; background: var(--border); border-radius: 4px; height: 10px; }}
54
+ .bar-fill {{ height: 100%; border-radius: 4px; background: var(--accent); }}
55
+ .bar-count {{ width: 60px; text-align: right; color: var(--accent); }}
56
+ .corrupt {{ color: var(--red); font-size: 0.8rem; margin-top: 0.5rem;
57
+ max-height: 200px; overflow-y: auto; }}
58
+ footer {{ margin-top: 3rem; color: var(--muted); font-size: 0.75rem;
59
+ border-top: 1px solid var(--border); padding-top: 1rem; }}
60
+ </style>
61
+ </head>
62
+ <body>
63
+ <h1>πŸ”¬ {title}</h1>
64
+ <p class="subtitle">Generated by <strong>VisEDA</strong></p>
65
+
66
+ {body}
67
+
68
+ <footer>Generated by VisEDA &mdash; Visual Exploratory Data Analysis</footer>
69
+ </body>
70
+ </html>
71
+ """
72
+
73
+
74
+ def _stat_card(title: str, stats: Dict) -> str:
75
+ if not stats:
76
+ return ""
77
+ rows = "".join(
78
+ f'<div class="stat"><span>{k}</span>'
79
+ f'<span class="val">{_fmt(v)}</span></div>'
80
+ for k, v in stats.items()
81
+ )
82
+ return f'<div class="card"><h3>{title}</h3>{rows}</div>'
83
+
84
+
85
+ def _fmt(v) -> str:
86
+ if isinstance(v, float):
87
+ return f"{v:,.3f}"
88
+ if isinstance(v, int):
89
+ return f"{v:,}"
90
+ return str(v)
91
+
92
+
93
+ def _bar_chart(title: str, dist: Dict, color_class: str = "badge-blue") -> str:
94
+ if not dist:
95
+ return ""
96
+ total = sum(dist.values()) or 1
97
+ max_val = max(dist.values()) or 1
98
+ bars = ""
99
+ for label, count in sorted(dist.items(), key=lambda x: -x[1])[:30]:
100
+ pct = count / max_val * 100
101
+ bars += (
102
+ f'<div class="bar-row">'
103
+ f'<span class="bar-label" title="{label}">{label}</span>'
104
+ f'<div class="bar"><div class="bar-fill" style="width:{pct:.1f}%"></div></div>'
105
+ f'<span class="bar-count">{count:,}</span>'
106
+ f"</div>"
107
+ )
108
+ return f'<div class="card" style="grid-column: span 2;"><h3>{title}</h3><div class="bar-wrap">{bars}</div></div>'
109
+
110
+
111
+ def generate_html_report(
112
+ summary: Dict[str, Any],
113
+ output_path: str,
114
+ title: str = "VisEDA Report",
115
+ ) -> None:
116
+ sections = []
117
+
118
+ # ── Overview badges ──────────────────────────────────────────────
119
+ badges = ""
120
+ if "total_images" in summary:
121
+ badges += f'<span class="badge badge-blue">{summary["total_images"]:,} images</span>'
122
+ if "valid_images" in summary:
123
+ badges += f'<span class="badge badge-green">{summary["valid_images"]:,} valid</span>'
124
+ if summary.get("corrupt_images"):
125
+ badges += f'<span class="badge badge-red">{summary["corrupt_images"]:,} corrupt</span>'
126
+ if "n_points" in summary:
127
+ badges += f'<span class="badge badge-blue">{summary["n_points"]:,} points</span>'
128
+ if summary.get("n_duplicate_groups"):
129
+ badges += (
130
+ f'<span class="badge badge-yellow">'
131
+ f'{summary["n_duplicate_groups"]:,} duplicate groups</span>'
132
+ )
133
+
134
+ if badges:
135
+ sections.append(f"<h2>Overview</h2><p>{badges}</p>")
136
+
137
+ # ── Grid of stat cards ───────────────────────────────────────────
138
+ stat_keys = [
139
+ "height", "width", "aspect_ratio", "file_size_kb",
140
+ "brightness", "contrast", "sharpness",
141
+ "x", "y", "z",
142
+ "intensity",
143
+ ]
144
+ cards = ""
145
+ for key in stat_keys:
146
+ if key in summary and summary[key]:
147
+ cards += _stat_card(key.replace("_", " ").title(), summary[key])
148
+
149
+ if "bounding_box" in summary:
150
+ bb = summary["bounding_box"]
151
+ cards += _stat_card("Bounding Box", {
152
+ "X extent": bb["extent"][0],
153
+ "Y extent": bb["extent"][1],
154
+ "Z extent": bb["extent"][2],
155
+ "Volume": summary.get("volume_bounding_box", 0),
156
+ "Density (pts/unitΒ³)": summary.get("density_pts_per_unit3", 0),
157
+ })
158
+
159
+ if cards:
160
+ sections.append(f'<h2>Statistics</h2><div class="grid">{cards}</div>')
161
+
162
+ # ── Channel / RGB ─────────────────────────────────────────────────
163
+ if "mean_pixel_per_channel" in summary and summary["mean_pixel_per_channel"]:
164
+ ch_stats = {}
165
+ for i, (mn, st) in enumerate(zip(
166
+ summary["mean_pixel_per_channel"],
167
+ summary.get("std_pixel_per_channel", [0] * 3),
168
+ )):
169
+ ch_stats[f"Channel {i} mean"] = mn
170
+ ch_stats[f"Channel {i} std"] = st
171
+ cards2 = _stat_card("Channel Statistics", ch_stats)
172
+ sections.append(f'<h2>Pixel Statistics</h2><div class="grid">{cards2}</div>')
173
+
174
+ # ── Label distribution ────────────────────────────────────────────
175
+ if summary.get("label_distribution"):
176
+ sections.append(
177
+ f'<h2>Label Distribution</h2>'
178
+ f'<div class="grid">'
179
+ f'{_bar_chart("Labels", summary["label_distribution"])}'
180
+ f'</div>'
181
+ )
182
+
183
+ # ── Channel distribution ──────────────────────────────────────────
184
+ if summary.get("channel_distribution"):
185
+ sections.append(
186
+ f'<div class="grid">'
187
+ f'{_bar_chart("Channel Distribution", {str(k)+"-ch": v for k, v in summary["channel_distribution"].items()})}'
188
+ f'</div>'
189
+ )
190
+
191
+ # ── Corrupt files ─────────────────────────────────────────────────
192
+ if summary.get("corrupt_paths"):
193
+ items = "".join(f"<li>{p}</li>" for p in summary["corrupt_paths"][:50])
194
+ sections.append(
195
+ f'<h2>Corrupt Files ({len(summary["corrupt_paths"])})</h2>'
196
+ f'<div class="corrupt"><ul>{items}</ul></div>'
197
+ )
198
+
199
+ # ── Duplicate groups ──────────────────────────────────────────────
200
+ if summary.get("duplicate_groups"):
201
+ groups_html = ""
202
+ for grp in summary["duplicate_groups"][:10]:
203
+ items = "".join(f"<li>{p}</li>" for p in grp)
204
+ groups_html += f"<ul>{items}</ul><hr/>"
205
+ sections.append(
206
+ f'<h2>Duplicate Groups (showing first 10)</h2>'
207
+ f'<div class="card">{groups_html}</div>'
208
+ )
209
+
210
+ body = "\n".join(sections)
211
+ html = _TEMPLATE.format(title=title, body=body)
212
+
213
+ Path(output_path).write_text(html, encoding="utf-8")
@@ -0,0 +1,3 @@
1
+ from viseda.text.eda import TextEDA, TextRecord
2
+
3
+ __all__ = ["TextEDA", "TextRecord"]