tabalyst 0.1.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,500 @@
1
+ (() => {
2
+ const root = document.documentElement;
3
+ const themeToggle = document.getElementById("theme-toggle");
4
+
5
+ function setTheme(theme, persist = true) {
6
+ root.dataset.theme = theme;
7
+ root.dataset.bsTheme = theme;
8
+ const dark = theme === "dark";
9
+ const label = dark ? "Switch to light mode" : "Switch to dark mode";
10
+ themeToggle.setAttribute("aria-label", label);
11
+ themeToggle.setAttribute("aria-pressed", String(dark));
12
+ themeToggle.title = label;
13
+ themeToggle.querySelector("[data-theme-icon]").textContent = dark ? "\u2600" : "\u263e";
14
+ if (persist) {
15
+ try {
16
+ localStorage.setItem("tabalyst-theme", theme);
17
+ } catch (error) {
18
+ // Theme switching still works when local files cannot use storage.
19
+ }
20
+ }
21
+ }
22
+
23
+ setTheme(root.dataset.theme || "dark", false);
24
+ themeToggle.addEventListener("click", () => {
25
+ setTheme(root.dataset.theme === "dark" ? "light" : "dark");
26
+ });
27
+
28
+ function toggleDetails(details, open) {
29
+ if (details.dataset.animating || details.open === open) return;
30
+ const summary = details.querySelector(":scope > summary");
31
+ if (!summary || matchMedia("(prefers-reduced-motion: reduce)").matches) {
32
+ details.open = open;
33
+ return;
34
+ }
35
+
36
+ const startHeight = details.offsetHeight;
37
+ const content = [...details.children].filter(element => element !== summary);
38
+ if (open) {
39
+ details.open = true;
40
+ } else {
41
+ details.dataset.collapsing = "true";
42
+ }
43
+ const endHeight = open
44
+ ? details.offsetHeight
45
+ : summary.offsetHeight
46
+ + Number.parseFloat(getComputedStyle(details).paddingTop)
47
+ + Number.parseFloat(getComputedStyle(details).paddingBottom);
48
+
49
+ details.dataset.animating = "true";
50
+ details.style.height = `${startHeight}px`;
51
+ details.style.overflow = "hidden";
52
+ const animation = details.animate(
53
+ { height: [`${startHeight}px`, `${endHeight}px`] },
54
+ { duration: 240, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
55
+ );
56
+ for (const element of content) {
57
+ element.animate(
58
+ open
59
+ ? { opacity: [0.35, 1], transform: ["translateY(-4px)", "translateY(0)"] }
60
+ : { opacity: [1, 0.35], transform: ["translateY(0)", "translateY(-4px)"] },
61
+ { duration: 150, delay: open ? 25 : 0, easing: "ease-out" },
62
+ );
63
+ }
64
+ animation.onfinish = () => {
65
+ if (!open) details.open = false;
66
+ delete details.dataset.animating;
67
+ delete details.dataset.collapsing;
68
+ details.style.height = "";
69
+ details.style.overflow = "";
70
+ };
71
+ }
72
+
73
+ document.querySelectorAll(".report-details > summary").forEach(summary => {
74
+ summary.addEventListener("click", event => {
75
+ event.preventDefault();
76
+ toggleDetails(summary.parentElement, !summary.parentElement.open);
77
+ });
78
+ });
79
+
80
+ document.querySelectorAll(".sidebar nav a[href^='#']").forEach(link => {
81
+ link.addEventListener("click", () => {
82
+ const section = document.getElementById(link.hash.slice(1));
83
+ if (section instanceof HTMLDetailsElement) toggleDetails(section, true);
84
+ });
85
+ });
86
+
87
+ const nameSearch = document.getElementById("column-search");
88
+ const summary = document.getElementById("columns-table");
89
+
90
+ if (!window.DataTable || !DataTable.ColumnControl) {
91
+ const status = document.getElementById("filter-result");
92
+ status.hidden = false;
93
+ status.textContent = "Interactive filters unavailable. Check your internet connection.";
94
+ nameSearch.addEventListener("input", () => {
95
+ for (const row of summary.tBodies[0].rows) {
96
+ row.hidden = !row.dataset.columnName.includes(nameSearch.value.trim().toLowerCase());
97
+ }
98
+ });
99
+ return;
100
+ }
101
+
102
+ DataTable.ColumnControl.content.compactNumber = {
103
+ defaults: { title: "Numeric filter" },
104
+ init(config) {
105
+ const dt = this.dt();
106
+ const key = `${this.unique()}compactNumber`;
107
+ const root = document.createElement("div");
108
+ root.className = "number-filter";
109
+ const title = document.createElement("div");
110
+ title.className = "number-filter-title";
111
+ title.textContent = config.title;
112
+ const operators = document.createElement("div");
113
+ operators.className = "number-operators";
114
+ operators.setAttribute("role", "group");
115
+ operators.setAttribute("aria-label", "Comparison");
116
+ const fields = document.createElement("div");
117
+ fields.className = "number-fields";
118
+ const minimum = document.createElement("input");
119
+ const maximum = document.createElement("input");
120
+ for (const input of [minimum, maximum]) {
121
+ input.type = "number";
122
+ input.step = "any";
123
+ input.autocomplete = "off";
124
+ }
125
+ maximum.placeholder = "Maximum";
126
+ maximum.setAttribute("aria-label", "Maximum (inclusive)");
127
+ const error = document.createElement("div");
128
+ error.id = `number-error-${dt.table().node().id}-${this.idx()}`;
129
+ error.className = "number-filter-error";
130
+ error.setAttribute("aria-live", "polite");
131
+ for (const input of [minimum, maximum]) input.setAttribute("aria-describedby", error.id);
132
+ fields.append(minimum, maximum);
133
+ root.append(title, operators, fields, error);
134
+ let mode = "greater";
135
+
136
+ const update = (draw = true) => {
137
+ const between = mode === "between";
138
+ maximum.hidden = !between;
139
+ minimum.placeholder = between ? "Minimum" : "Value";
140
+ minimum.setAttribute("aria-label", between ? "Minimum (inclusive)" : "Value");
141
+ for (const button of operators.children) {
142
+ button.setAttribute("aria-pressed", String(button.dataset.operator === mode));
143
+ }
144
+ const low = minimum.valueAsNumber;
145
+ const high = maximum.valueAsNumber;
146
+ const reversed = between && Number.isFinite(low) && Number.isFinite(high) && low > high;
147
+ error.textContent = reversed ? "Minimum must not exceed maximum." : "";
148
+ error.hidden = !reversed;
149
+ minimum.setAttribute("aria-invalid", String(reversed));
150
+ maximum.setAttribute("aria-invalid", String(reversed));
151
+ const active = Number.isFinite(low) && (!between || Number.isFinite(high)) && !reversed;
152
+ const compare = value => {
153
+ if (String(value).trim() === "") return false;
154
+ const number = Number(value);
155
+ if (!Number.isFinite(number)) return false;
156
+ if (mode === "equal") return number === low;
157
+ if (mode === "greater") return number > low;
158
+ if (mode === "greaterOrEqual") return number >= low;
159
+ if (mode === "less") return number < low;
160
+ if (mode === "lessOrEqual") return number <= low;
161
+ return number >= low && number <= high;
162
+ };
163
+ dt.column(this.idx()).search.fixed("dtcc", active ? compare : "");
164
+ for (const parent of config._parents || []) parent.activeList(key, active);
165
+ if (draw) dt.draw();
166
+ };
167
+ for (const [value, symbol, label] of [
168
+ ["equal", "=", "Equal to"],
169
+ ["greater", ">", "Greater than"],
170
+ ["greaterOrEqual", "\u2265", "Greater than or equal"],
171
+ ["less", "<", "Less than"],
172
+ ["lessOrEqual", "\u2264", "Less than or equal"],
173
+ ["between", "[\u00b7\u00b7]", "Between (inclusive)"],
174
+ ]) {
175
+ const button = document.createElement("button");
176
+ button.type = "button";
177
+ button.dataset.operator = value;
178
+ button.textContent = symbol;
179
+ button.title = label;
180
+ button.setAttribute("aria-label", label);
181
+ button.addEventListener("click", () => { mode = value; update(); });
182
+ operators.append(button);
183
+ }
184
+ for (const input of [minimum, maximum]) input.addEventListener("input", () => update());
185
+ dt.on("cc-search-clear", (event, index) => {
186
+ if (index !== this.idx()) return;
187
+ minimum.value = maximum.value = "";
188
+ mode = "greater";
189
+ update(false);
190
+ });
191
+ update(false);
192
+ return root;
193
+ },
194
+ };
195
+
196
+ function numericFilter(title = "Numeric filter") {
197
+ return { extend: "compactNumber", title };
198
+ }
199
+
200
+ function checkboxFilter(element, index, formatLabel = value => DataTable.util.escapeHtml(value)) {
201
+ const counts = new Map();
202
+ for (const row of element.tBodies[0].rows) {
203
+ const value = row.cells[index].dataset.search;
204
+ counts.set(value, (counts.get(value) || 0) + 1);
205
+ }
206
+ return {
207
+ extend: "searchList",
208
+ search: true,
209
+ select: true,
210
+ // Library labels accept HTML; CSV content must stay escaped in dropdowns too.
211
+ options: [...counts].map(([value, count]) => ({ value, label: formatLabel(value, count) })),
212
+ };
213
+ }
214
+
215
+ function controls(filter) {
216
+ return ["order", [filter, "searchClear"]];
217
+ }
218
+
219
+ function positionMenus() {
220
+ for (const menu of document.querySelectorAll(".dtcc-dropdown")) {
221
+ if (menu.dataset.dragged === "true") {
222
+ const left = Math.max(12, Math.min(parseFloat(menu.style.left) || 12, innerWidth - menu.offsetWidth - 12));
223
+ const top = Math.max(12, Math.min(parseFloat(menu.style.top) || 12, innerHeight - menu.offsetHeight - 12));
224
+ menu.style.left = `${left}px`;
225
+ menu.style.top = `${top}px`;
226
+ continue;
227
+ }
228
+ const trigger = menu.closest(".dt-container").querySelector(".dtcc-button_dropdown[aria-expanded='true']");
229
+ if (!trigger) continue;
230
+ const anchor = trigger.getBoundingClientRect();
231
+ const left = Math.max(12, Math.min(anchor.right - menu.offsetWidth, innerWidth - menu.offsetWidth - 12));
232
+ const below = anchor.bottom + 6;
233
+ const top = below + menu.offsetHeight <= innerHeight - 12 ? below : anchor.top - menu.offsetHeight - 6;
234
+ menu.style.left = `${left}px`;
235
+ menu.style.top = `${Math.max(12, Math.min(top, innerHeight - menu.offsetHeight - 12))}px`;
236
+ menu.setAttribute("aria-label", trigger.getAttribute("aria-label"));
237
+ }
238
+ }
239
+
240
+ function makeMenuMovable(menu) {
241
+ if (menu.querySelector(":scope > .filter-drag-handle")) return;
242
+ const handle = document.createElement("div");
243
+ handle.className = "filter-drag-handle";
244
+ const handleLabel = document.createElement("span");
245
+ handleLabel.textContent = "Move filter";
246
+ const close = document.createElement("button");
247
+ close.type = "button";
248
+ close.className = "filter-close";
249
+ close.textContent = "\u00d7";
250
+ close.title = "Close filter";
251
+ close.setAttribute("aria-label", close.title);
252
+ close.addEventListener("pointerdown", event => event.stopPropagation());
253
+ close.addEventListener("click", event => {
254
+ event.stopPropagation();
255
+ if (typeof menu._close === "function") menu._close();
256
+ });
257
+ handle.append(handleLabel, close);
258
+ handle.title = "Drag to move this filter";
259
+ handle.addEventListener("pointerdown", event => {
260
+ if (event.button !== 0) return;
261
+ const rect = menu.getBoundingClientRect();
262
+ const offsetX = event.clientX - rect.left;
263
+ const offsetY = event.clientY - rect.top;
264
+ menu.dataset.dragged = "true";
265
+ handle.setPointerCapture(event.pointerId);
266
+ const move = pointer => {
267
+ const left = Math.max(12, Math.min(pointer.clientX - offsetX, innerWidth - menu.offsetWidth - 12));
268
+ const top = Math.max(12, Math.min(pointer.clientY - offsetY, innerHeight - menu.offsetHeight - 12));
269
+ menu.style.left = `${left}px`;
270
+ menu.style.top = `${top}px`;
271
+ };
272
+ const stop = pointer => {
273
+ move(pointer);
274
+ handle.releasePointerCapture(pointer.pointerId);
275
+ handle.removeEventListener("pointermove", move);
276
+ handle.removeEventListener("pointerup", stop);
277
+ handle.removeEventListener("pointercancel", stop);
278
+ };
279
+ handle.addEventListener("pointermove", move);
280
+ handle.addEventListener("pointerup", stop);
281
+ handle.addEventListener("pointercancel", stop);
282
+ event.preventDefault();
283
+ });
284
+ menu.prepend(handle);
285
+ }
286
+
287
+ function organizeMenuActions(menu) {
288
+ const clear = menu.querySelector(".dtcc-button_searchClear");
289
+ const liner = menu.querySelector(":scope > .dtcc-dropdown-liner");
290
+ if (!clear || !liner) return;
291
+ let row = liner.querySelector(":scope > .filter-action-row");
292
+ if (!row) {
293
+ row = document.createElement("div");
294
+ row.className = "filter-action-row";
295
+ const label = document.createElement("span");
296
+ label.className = "filter-action-label";
297
+ row.append(label);
298
+ liner.prepend(row);
299
+ }
300
+ clear.classList.add("filter-clear");
301
+ const text = clear.querySelector(".dtcc-button-text");
302
+ if (text && text.textContent !== "Clear") text.textContent = "Clear";
303
+ const trigger = menu.closest(".dt-container")
304
+ ?.querySelector(".dtcc-button_dropdown[aria-expanded='true']");
305
+ const title = trigger?.getAttribute("title") || "Filter";
306
+ const label = row.querySelector(".filter-action-label");
307
+ const labelText = title.replace(/^Filter\s+/, "");
308
+ if (label.textContent !== labelText) label.textContent = labelText;
309
+ clear.classList.toggle(
310
+ "filter-clear-active",
311
+ Boolean(trigger?.classList.contains("dtcc-button_active")),
312
+ );
313
+ }
314
+
315
+ // Preserve accessible checkbox states and viewport positioning in scrollable tables.
316
+ const menuObserverOptions = {
317
+ childList: true,
318
+ subtree: true,
319
+ attributes: true,
320
+ attributeFilter: ["class"],
321
+ };
322
+ const menuObserver = new MutationObserver(() => {
323
+ menuObserver.disconnect();
324
+ for (const menu of document.querySelectorAll(".dtcc-dropdown")) {
325
+ makeMenuMovable(menu);
326
+ organizeMenuActions(menu);
327
+ }
328
+ for (const button of document.querySelectorAll(".dtcc-list-buttons .dtcc-button")) {
329
+ button.setAttribute("role", "checkbox");
330
+ button.setAttribute("aria-checked", String(button.classList.contains("dtcc-button_active")));
331
+ const badges = [...button.querySelectorAll(".type-badge")].map(badge => badge.textContent);
332
+ const count = button.querySelector(".filter-option-count")?.textContent || "";
333
+ const label = badges.length > 1
334
+ ? `${badges.join(" · ")} ${count}`
335
+ : button.querySelector(".dtcc-button-text").textContent;
336
+ button.setAttribute("aria-label", label);
337
+ }
338
+ positionMenus();
339
+ menuObserver.observe(document.body, menuObserverOptions);
340
+ });
341
+ menuObserver.observe(document.body, menuObserverOptions);
342
+ document.addEventListener("scroll", positionMenus, { capture: true, passive: true });
343
+ window.addEventListener("resize", () => requestAnimationFrame(positionMenus));
344
+
345
+ function positionHoverTooltip(cell, selector) {
346
+ const tooltip = cell.querySelector(selector);
347
+ if (!tooltip) return;
348
+ const anchor = cell.getBoundingClientRect();
349
+ const overlap = Math.min(32, anchor.height * 0.65);
350
+ const left = Math.max(12, Math.min(anchor.left, innerWidth - tooltip.offsetWidth - 12));
351
+ const below = anchor.bottom - overlap;
352
+ const top = below + tooltip.offsetHeight <= innerHeight - 12
353
+ ? below
354
+ : anchor.top - tooltip.offsetHeight + overlap;
355
+ tooltip.style.left = `${left}px`;
356
+ tooltip.style.top = `${Math.max(12, top)}px`;
357
+ }
358
+
359
+ for (const [cellSelector, tooltipSelector] of [
360
+ [".examples", ".examples-tooltip"],
361
+ [".date-formats-cell", ".date-formats-tooltip"],
362
+ ]) {
363
+ for (const cell of document.querySelectorAll(cellSelector)) {
364
+ cell.addEventListener("pointerenter", () => positionHoverTooltip(cell, tooltipSelector));
365
+ cell.addEventListener("focusin", () => positionHoverTooltip(cell, tooltipSelector));
366
+ }
367
+ }
368
+
369
+ function attachTable(element, columns, noun) {
370
+ const table = new DataTable(element, {
371
+ autoWidth: false,
372
+ paging: false,
373
+ info: false,
374
+ order: [],
375
+ ordering: { indicators: false, handler: false },
376
+ layout: { topStart: null, topEnd: null, bottomStart: null, bottomEnd: null },
377
+ // DataTables decodes entities through HTML when building its search cache.
378
+ // Escape raw filter values first so that decoding yields inert text.
379
+ columns: columns.map(column => ({
380
+ ...column,
381
+ render: (value, purpose) => purpose === "filter" ? DataTable.util.escapeHtml(value) : value,
382
+ })),
383
+ language: { emptyTable: "No data available.", zeroRecords: "No matching results." },
384
+ });
385
+ const status = document.querySelector(`[data-table-status="${element.id}"]`);
386
+ const updateStatus = () => {
387
+ const total = table.rows().count();
388
+ const visible = table.rows({ search: "applied" }).count();
389
+ status.hidden = false;
390
+ status.textContent = `${visible} of ${total} ${noun}`;
391
+ };
392
+ for (const cell of element.tHead.rows[0].cells) {
393
+ const title = cell.querySelector(".dt-column-title").textContent;
394
+ for (const [selector, action] of [[".dtcc-button_order", "Sort by"], [".dtcc-button_dropdown", "Filter"]]) {
395
+ const button = cell.querySelector(selector);
396
+ if (button) {
397
+ button.title = `${action} ${title}`;
398
+ button.setAttribute("aria-label", button.title);
399
+ }
400
+ }
401
+ }
402
+
403
+ const reset = document.querySelector(`[data-reset-table="${element.id}"]`);
404
+ const updateResetVisibility = () => {
405
+ const hasActiveControl = Boolean(element.tHead.querySelector(".dtcc-button_active"));
406
+ let hasColumnSearch = false;
407
+ table.columns().every(function () {
408
+ hasColumnSearch ||= this.search() !== "";
409
+ });
410
+ const hasSearch = table.search() !== "" || hasColumnSearch;
411
+ reset.hidden = element !== summary
412
+ && !hasActiveControl
413
+ && !hasSearch
414
+ && table.order().length === 0;
415
+ };
416
+ table.on("draw", () => {
417
+ updateStatus();
418
+ updateResetVisibility();
419
+ });
420
+ updateStatus();
421
+ updateResetVisibility();
422
+ reset.addEventListener("click", () => {
423
+ table.columns().columnControl.searchClear();
424
+ table.search("").columns().search("");
425
+ if (element === summary) nameSearch.value = "";
426
+ table.order([]).draw();
427
+ });
428
+ element.dataset.interactive = "true";
429
+ return table;
430
+ }
431
+
432
+ const summaryTable = attachTable(summary, [
433
+ { type: "string", columnControl: controls(checkboxFilter(summary, 0)) },
434
+ { type: "num", columnControl: controls(numericFilter("Missing (%)")) },
435
+ { type: "num", columnControl: controls(numericFilter("Distinct values")) },
436
+ { type: "string", columnControl: controls(checkboxFilter(summary, 3, (value, count) => {
437
+ const type = DataTable.util.escapeHtml(value);
438
+ const badge = ["text", "integer", "number", "date", "boolean", "mixed", "empty"].includes(value)
439
+ ? `badge-${value}` : "badge-empty";
440
+ return `<span class="type-badge ${badge}">${type}</span> <span class="filter-option-count">(${count})</span>`;
441
+ })) },
442
+ { type: "string", columnControl: controls(checkboxFilter(summary, 4, (value, count) => value === "(none)"
443
+ ? `<span class="muted">(none)</span> <span class="filter-option-count">(${count})</span>`
444
+ : `<span class="type-badge semantic-badge semantic-${DataTable.util.escapeHtml(value)}">${DataTable.util.escapeHtml(value)}</span> <span class="filter-option-count">(${count})</span>`)) },
445
+ { type: "num", columnControl: controls(numericFilter("Type error (%)")) },
446
+ { orderable: false, columnControl: [] },
447
+ ], "columns");
448
+ nameSearch.addEventListener("input", () => {
449
+ summaryTable.column(0).search(nameSearch.value, { regex: false, smart: false }).draw();
450
+ });
451
+
452
+ const transformations = document.getElementById("transformations-table");
453
+ if (transformations) {
454
+ attachTable(transformations, [
455
+ { type: "string", columnControl: controls(checkboxFilter(transformations, 0)) },
456
+ ...Array.from({ length: 4 }, () => ({ type: "num", columnControl: controls(numericFilter()) })),
457
+ ], "columns");
458
+ }
459
+
460
+ const numeric = document.getElementById("numeric-table");
461
+ if (numeric) {
462
+ attachTable(numeric, [
463
+ { type: "string", columnControl: controls(checkboxFilter(numeric, 0)) },
464
+ ...Array.from({ length: 4 }, () => ({ type: "num", columnControl: controls(numericFilter()) })),
465
+ ], "columns");
466
+ }
467
+
468
+ const dates = document.getElementById("date-table");
469
+ if (dates) {
470
+ attachTable(dates, [
471
+ { type: "string", columnControl: controls(checkboxFilter(dates, 0)) },
472
+ { type: "string", columnControl: controls(checkboxFilter(dates, 1)) },
473
+ ...Array.from({ length: 4 }, () => ({ type: "num", columnControl: controls(numericFilter()) })),
474
+ { orderable: false, columnControl: [] },
475
+ ], "date columns");
476
+ }
477
+
478
+ const strings = document.getElementById("string-table");
479
+ if (strings) {
480
+ attachTable(strings, [
481
+ { type: "string", columnControl: controls(checkboxFilter(strings, 0)) },
482
+ { type: "string", columnControl: controls(checkboxFilter(strings, 1, (value, count) =>
483
+ `<span class="type-badge string-status string-${DataTable.util.escapeHtml(value)}">${DataTable.util.escapeHtml(value.replaceAll("_", " "))}</span> <span class="filter-option-count">(${count})</span>`)) },
484
+ ...Array.from({ length: 6 }, () => ({ type: "num", columnControl: controls(numericFilter()) })),
485
+ { orderable: false, columnControl: [] },
486
+ ], "string columns");
487
+ }
488
+
489
+ const sample = document.getElementById("sample-table");
490
+ if (sample) {
491
+ const columns = [...sample.tHead.rows[0].cells].map((cell, index) => {
492
+ const numeric = ["integer", "number"].includes(cell.dataset.valueType);
493
+ const filter = numeric ? numericFilter() : cell.dataset.valueType === "boolean"
494
+ ? checkboxFilter(sample, index)
495
+ : { extend: "searchText", titleAttr: "Filter values", placeholder: "Value" };
496
+ return { type: numeric ? "num" : "string", columnControl: controls(filter) };
497
+ });
498
+ attachTable(sample, columns, "sample rows");
499
+ }
500
+ })();