csv-grid 3.0.5__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.
- csv_grid/__init__.py +172 -0
- csv_grid/assets/csv-grid.css +160 -0
- csv_grid/assets/csv-grid.umd.js +4 -0
- csv_grid-3.0.5.dist-info/METADATA +83 -0
- csv_grid-3.0.5.dist-info/RECORD +7 -0
- csv_grid-3.0.5.dist-info/WHEEL +4 -0
- csv_grid-3.0.5.dist-info/licenses/LICENSE +21 -0
csv_grid/__init__.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""csv_grid — emit CsvGrid (csv-viewer's embeddable grid) from pandas DataFrames.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
show(df, **options) display in Jupyter / Quarto via IPython
|
|
5
|
+
to_html(df, **options) return an HTML fragment for static generation
|
|
6
|
+
payload(df) the {records, columns} dict the grid consumes
|
|
7
|
+
|
|
8
|
+
The grid re-infers column types from the emitted strings/numbers exactly
|
|
9
|
+
as the csv-viewer app does; this module only handles serialization
|
|
10
|
+
(dates -> ISO strings, NaN/None -> blank, integral floats -> ints) and
|
|
11
|
+
option plumbing. The built JS/CSS assets live in csv_grid/assets/,
|
|
12
|
+
refreshed by the repo's `npm run build`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import uuid
|
|
19
|
+
from importlib import resources
|
|
20
|
+
|
|
21
|
+
__version__ = "3.0.5"
|
|
22
|
+
__all__ = ["show", "to_html", "payload"]
|
|
23
|
+
|
|
24
|
+
# python snake_case -> CsvGrid option names (see src/grid/grid.js)
|
|
25
|
+
_OPTION_MAP = {
|
|
26
|
+
"global_search": "globalSearch",
|
|
27
|
+
"column_filters": "columnFilters",
|
|
28
|
+
"sortable": "sortable",
|
|
29
|
+
"status_bar": "statusBar",
|
|
30
|
+
"expand_buttons": "expandButtons",
|
|
31
|
+
"align": "align",
|
|
32
|
+
"formats": "formats",
|
|
33
|
+
"render_cap": "renderCap",
|
|
34
|
+
"eager_cells": "eagerCells",
|
|
35
|
+
"worker": "worker",
|
|
36
|
+
}
|
|
37
|
+
_CAMEL = set(_OPTION_MAP.values())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _map_options(options: dict) -> dict:
|
|
41
|
+
"""snake_case -> camelCase; `fmt` aliases `formats`; unknown keys raise."""
|
|
42
|
+
if "fmt" in options:
|
|
43
|
+
options["formats"] = options.pop("fmt")
|
|
44
|
+
out = {}
|
|
45
|
+
for k, v in options.items():
|
|
46
|
+
if k in _OPTION_MAP:
|
|
47
|
+
out[_OPTION_MAP[k]] = v
|
|
48
|
+
elif k in _CAMEL:
|
|
49
|
+
out[k] = v
|
|
50
|
+
else:
|
|
51
|
+
raise TypeError(f"csv_grid: unknown option {k!r} "
|
|
52
|
+
f"(known: {', '.join(sorted(_OPTION_MAP))})")
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _jsonable(o):
|
|
57
|
+
"""json.dumps default: numpy scalars, Timestamps, anything else -> str."""
|
|
58
|
+
item = getattr(o, "item", None) # numpy scalar -> python native
|
|
59
|
+
if callable(item):
|
|
60
|
+
try:
|
|
61
|
+
return item()
|
|
62
|
+
except (ValueError, TypeError):
|
|
63
|
+
pass
|
|
64
|
+
iso = getattr(o, "isoformat", None) # stray datetime/date
|
|
65
|
+
if callable(iso):
|
|
66
|
+
return iso()
|
|
67
|
+
return str(o)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def payload(df, name: str | None = None, index: bool = False) -> dict:
|
|
71
|
+
"""The {records, columns[, name]} dict CsvGrid consumes (records as
|
|
72
|
+
arrays in column order). Dates become ISO strings (hh:mm only when a
|
|
73
|
+
column has non-midnight times); NaN/None/NaT become None (blank cells
|
|
74
|
+
in the grid); integral float columns become ints so the grid's
|
|
75
|
+
integer/year formatting rules apply. Types are re-inferred grid-side.
|
|
76
|
+
"""
|
|
77
|
+
import pandas as pd # deferred so importing csv_grid stays cheap
|
|
78
|
+
|
|
79
|
+
if index:
|
|
80
|
+
df = df.reset_index()
|
|
81
|
+
# build plain-python column lists directly: assigning converted columns
|
|
82
|
+
# back into a DataFrame lets pandas re-coerce ints + None to float64
|
|
83
|
+
cols = []
|
|
84
|
+
for c in df.columns:
|
|
85
|
+
s = df[c]
|
|
86
|
+
if isinstance(s.dtype, pd.DatetimeTZDtype) or str(s.dtype).startswith("datetime64"):
|
|
87
|
+
midnight = (s.dt.hour.fillna(0).eq(0) & s.dt.minute.fillna(0).eq(0)
|
|
88
|
+
& s.dt.second.fillna(0).eq(0)).all()
|
|
89
|
+
fmt = "%Y-%m-%d" if midnight else "%Y-%m-%d %H:%M"
|
|
90
|
+
vals = [None if pd.isna(v) else v.strftime(fmt) for v in s]
|
|
91
|
+
elif str(s.dtype).startswith("float"):
|
|
92
|
+
nonnull = s.dropna()
|
|
93
|
+
integral = len(nonnull) > 0 and nonnull.mod(1).eq(0).all()
|
|
94
|
+
vals = [None if pd.isna(v)
|
|
95
|
+
else (int(v) if integral else float(v)) for v in s]
|
|
96
|
+
else:
|
|
97
|
+
vals = [None if pd.isna(v) else v for v in s.tolist()]
|
|
98
|
+
cols.append(vals)
|
|
99
|
+
d = {
|
|
100
|
+
"records": [list(row) for row in zip(*cols)],
|
|
101
|
+
"columns": [str(c) for c in df.columns],
|
|
102
|
+
}
|
|
103
|
+
if name:
|
|
104
|
+
d["name"] = name
|
|
105
|
+
return d
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _asset_text(fname: str) -> str:
|
|
109
|
+
return resources.files("csv_grid").joinpath("assets", fname).read_text(encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _assets_fragment(assets) -> str:
|
|
113
|
+
"""'inline' -> embed css+js; a string -> <link>/<script src> against
|
|
114
|
+
that base URL; False/None -> '' (already on the page)."""
|
|
115
|
+
if assets == "inline":
|
|
116
|
+
return (f"<style>\n{_asset_text('csv-grid.css')}\n</style>\n"
|
|
117
|
+
f"<script>\n{_asset_text('csv-grid.umd.js')}\n</script>")
|
|
118
|
+
if assets:
|
|
119
|
+
base = str(assets).rstrip("/")
|
|
120
|
+
return (f'<link rel="stylesheet" href="{base}/csv-grid.css">\n'
|
|
121
|
+
f'<script src="{base}/csv-grid.umd.js"></script>')
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _dump(obj) -> str:
|
|
126
|
+
# `<\/` keeps a literal '</script>' in the data from closing our tag
|
|
127
|
+
return json.dumps(obj, ensure_ascii=False, default=_jsonable).replace("</", "<\\/")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def to_html(df, *, name: str | None = None, assets="inline",
|
|
131
|
+
index: bool = False, **options) -> str:
|
|
132
|
+
"""HTML fragment rendering `df` as a CsvGrid. The first fragment on a
|
|
133
|
+
page should carry the assets (default 'inline'; or a base URL hosting
|
|
134
|
+
csv-grid.umd.js + csv-grid.css); pass assets=False for later tables.
|
|
135
|
+
Options are the grid's, in snake_case (see _OPTION_MAP); `fmt` is an
|
|
136
|
+
alias for `formats`; `worker` defaults to False (data is inlined).
|
|
137
|
+
"""
|
|
138
|
+
opts = _map_options(options)
|
|
139
|
+
opts.setdefault("worker", False)
|
|
140
|
+
div = f"csvgrid-{uuid.uuid4().hex[:12]}"
|
|
141
|
+
parts = []
|
|
142
|
+
head = _assets_fragment(assets)
|
|
143
|
+
if head:
|
|
144
|
+
parts.append(head)
|
|
145
|
+
parts.append(
|
|
146
|
+
f'<div id="{div}"></div>\n'
|
|
147
|
+
f'<script>new CsvGrid(document.getElementById("{div}"), '
|
|
148
|
+
f'{_dump(payload(df, name, index))}, {_dump(opts)});</script>'
|
|
149
|
+
)
|
|
150
|
+
return "\n".join(parts)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
_assets_emitted = False
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def show(df, *, name: str | None = None, assets=None,
|
|
157
|
+
index: bool = False, **options) -> None:
|
|
158
|
+
"""Display `df` as a CsvGrid in Jupyter / Quarto. Assets are emitted
|
|
159
|
+
once per kernel session (= once per rendered page); assets='inline'
|
|
160
|
+
forces re-emission (e.g. after restarting the browser page without
|
|
161
|
+
the kernel), a base-URL string loads them from there instead.
|
|
162
|
+
"""
|
|
163
|
+
global _assets_emitted
|
|
164
|
+
try:
|
|
165
|
+
from IPython.display import HTML, display
|
|
166
|
+
except ImportError as e: # pragma: no cover
|
|
167
|
+
raise ImportError("csv_grid.show() needs IPython; "
|
|
168
|
+
"use to_html() outside Jupyter/Quarto") from e
|
|
169
|
+
if assets is None:
|
|
170
|
+
assets = False if _assets_emitted else "inline"
|
|
171
|
+
_assets_emitted = True
|
|
172
|
+
display(HTML(to_html(df, name=name, assets=assets, index=index, **options)))
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/* csv-grid styles — self-contained, no framework. Namespaced .csvgrid-*
|
|
2
|
+
* (structural) with table-internal classes scoped under .csvgrid-table.
|
|
3
|
+
* Replicates the viewer's Bootstrap-era look so embed pages match. */
|
|
4
|
+
|
|
5
|
+
.csvgrid {
|
|
6
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
7
|
+
color: #212529;
|
|
8
|
+
}
|
|
9
|
+
.csvgrid-hidden { display: none !important; }
|
|
10
|
+
|
|
11
|
+
/* Toolbar (only generated when globalSearch / expandButtons are on) */
|
|
12
|
+
.csvgrid-toolbar {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: center;
|
|
15
|
+
gap: 8px;
|
|
16
|
+
margin-bottom: 8px;
|
|
17
|
+
}
|
|
18
|
+
.csvgrid-search {
|
|
19
|
+
flex: 1;
|
|
20
|
+
max-width: 320px;
|
|
21
|
+
font-size: 0.875rem;
|
|
22
|
+
padding: 0.25rem 0.5rem;
|
|
23
|
+
border: 1px solid #ced4da;
|
|
24
|
+
border-radius: 0.25rem;
|
|
25
|
+
}
|
|
26
|
+
.csvgrid-search:focus {
|
|
27
|
+
outline: 0;
|
|
28
|
+
border-color: #86b7fe;
|
|
29
|
+
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
|
30
|
+
}
|
|
31
|
+
.csvgrid-btn {
|
|
32
|
+
font-size: 0.875rem;
|
|
33
|
+
padding: 0.25rem 0.5rem;
|
|
34
|
+
border: 1px solid #6c757d;
|
|
35
|
+
border-radius: 0.25rem;
|
|
36
|
+
background: #fff;
|
|
37
|
+
color: #6c757d;
|
|
38
|
+
cursor: pointer;
|
|
39
|
+
}
|
|
40
|
+
.csvgrid-btn:hover {
|
|
41
|
+
background: #6c757d;
|
|
42
|
+
color: #fff;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Scrollable table region; hosts cap its height / add card chrome */
|
|
46
|
+
.csvgrid-scroll { overflow: auto; }
|
|
47
|
+
|
|
48
|
+
/* Widths are pinned per load via <colgroup> + table-layout: fixed (set from
|
|
49
|
+
* JS); tight when everything fits, equal-risk truncation when it doesn't. */
|
|
50
|
+
.csvgrid-table {
|
|
51
|
+
border-collapse: collapse;
|
|
52
|
+
width: auto;
|
|
53
|
+
font-size: 0.8rem;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
font-variant-numeric: tabular-nums;
|
|
56
|
+
background: #fff;
|
|
57
|
+
}
|
|
58
|
+
.csvgrid-table th,
|
|
59
|
+
.csvgrid-table td {
|
|
60
|
+
padding: 0.25rem;
|
|
61
|
+
border-bottom: 1px solid #dee2e6;
|
|
62
|
+
text-align: left;
|
|
63
|
+
}
|
|
64
|
+
.csvgrid-table tbody tr:hover td { background: rgba(0, 0, 0, 0.075); }
|
|
65
|
+
|
|
66
|
+
.csvgrid-table thead th {
|
|
67
|
+
position: sticky;
|
|
68
|
+
top: 0;
|
|
69
|
+
z-index: 2;
|
|
70
|
+
background: #fff;
|
|
71
|
+
box-shadow: inset 0 -2px 0 #dee2e6;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
user-select: none;
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
}
|
|
78
|
+
.csvgrid-table thead th.csvgrid-nosort { cursor: default; }
|
|
79
|
+
.csvgrid-table thead th .col-resizer {
|
|
80
|
+
/* th has overflow:hidden, so the grip must sit inside the edge */
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 0;
|
|
83
|
+
right: 0;
|
|
84
|
+
width: 7px;
|
|
85
|
+
height: 100%;
|
|
86
|
+
cursor: col-resize;
|
|
87
|
+
z-index: 3;
|
|
88
|
+
}
|
|
89
|
+
.csvgrid-table thead th .col-resizer:hover { background: rgba(13, 110, 253, 0.3); }
|
|
90
|
+
body.csvgrid-resizing { cursor: col-resize; user-select: none; }
|
|
91
|
+
|
|
92
|
+
.csvgrid-table thead th .sort-arrow {
|
|
93
|
+
display: inline-block;
|
|
94
|
+
width: 1em;
|
|
95
|
+
color: #0d6efd;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.csvgrid-table thead tr.filter-row th {
|
|
99
|
+
cursor: default;
|
|
100
|
+
box-shadow: inset 0 -2px 0 #dee2e6;
|
|
101
|
+
top: 28px; /* sits below the (0.8rem) header row */
|
|
102
|
+
padding: 2px 4px;
|
|
103
|
+
}
|
|
104
|
+
.csvgrid-filter {
|
|
105
|
+
width: 100%;
|
|
106
|
+
min-width: 4em;
|
|
107
|
+
font-size: 0.78rem;
|
|
108
|
+
padding: 1px 6px;
|
|
109
|
+
border: 1px solid #ced4da;
|
|
110
|
+
border-radius: 0.25rem;
|
|
111
|
+
color: #212529;
|
|
112
|
+
}
|
|
113
|
+
.csvgrid-filter:focus {
|
|
114
|
+
outline: 0;
|
|
115
|
+
border-color: #86b7fe;
|
|
116
|
+
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25);
|
|
117
|
+
}
|
|
118
|
+
.csvgrid-filter.active-filter {
|
|
119
|
+
background-color: #e7f1ff;
|
|
120
|
+
border-color: #0d6efd;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.csvgrid-table td {
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
overflow: hidden;
|
|
126
|
+
text-overflow: ellipsis;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.csvgrid-table .col-number { text-align: right; }
|
|
130
|
+
.csvgrid-table .col-date { text-align: center; } /* greater_tables convention */
|
|
131
|
+
.csvgrid-table .col-text { text-align: left; }
|
|
132
|
+
/* explicit alignment (align option / markdown spec) overrides type alignment */
|
|
133
|
+
.csvgrid-table .align-left { text-align: left; }
|
|
134
|
+
.csvgrid-table .align-center { text-align: center; }
|
|
135
|
+
.csvgrid-table .align-right { text-align: right; }
|
|
136
|
+
.csvgrid-table td.blank { color: #adb5bd; }
|
|
137
|
+
|
|
138
|
+
/* "Showing first N — show all" note below the table */
|
|
139
|
+
.csvgrid-capnote {
|
|
140
|
+
text-align: center;
|
|
141
|
+
margin: 1rem 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Row-counts line (only when statusBar: true generates it) */
|
|
145
|
+
.csvgrid-status {
|
|
146
|
+
font-size: 0.75rem;
|
|
147
|
+
color: #6c757d;
|
|
148
|
+
padding: 3px 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Load errors ({url} fetch failures, unusable input) */
|
|
152
|
+
.csvgrid-error {
|
|
153
|
+
margin: 0.5rem 0;
|
|
154
|
+
padding: 0.5rem 0.75rem;
|
|
155
|
+
font-size: 0.875rem;
|
|
156
|
+
color: #842029;
|
|
157
|
+
background: #f8d7da;
|
|
158
|
+
border: 1px solid #f5c2c7;
|
|
159
|
+
border-radius: 0.25rem;
|
|
160
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t():typeof define==`function`&&define.amd?define([],t):(e=typeof globalThis<`u`?globalThis:e||self,e.CsvGrid=t())})(this,function(){var e=typeof document<`u`&&document.currentScript&&document.currentScript.src||(typeof document<`u`?document.baseURI:``);function t(e){return(e??``).replace(/^\uFEFF/,``).replace(/^(?:[ \t]*(?:\r\n|\n|\r))+/,``)}function n(e){let t=[`,`,` `,`;`,`|`],n=e.split(/\r\n|\n|\r/,20).filter(e=>e.length),i=`,`,a=0;for(let e of t){let t=n.map(t=>r(t,e).length),o=t[0];if(o<2)continue;let s=o*(t.every(e=>e===o)?10:1);s>a&&(a=s,i=e)}return i}function r(e,t){let n=[],r=``,i=!1;for(let a=0;a<e.length;a++){let o=e[a];i?o===`"`?i=!1:r+=o:o===`"`?i=!0:o===t?(n.push(r),r=``):r+=o}return n.push(r),n}function i(e,t){let n=[],r=[],i=``,a=!1,o=0,s=e.length;for(;o<s;){let s=e[o];if(a){if(s===`"`){if(e[o+1]===`"`){i+=`"`,o+=2;continue}a=!1,o++;continue}i+=s,o++;continue}if(s===`"`){a=!0,o++;continue}if(s===t){r.push(i),i=``,o++;continue}if(s===`\r`||s===`
|
|
2
|
+
`){r.push(i),i=``,n.push(r),r=[],s===`\r`&&e[o+1]===`
|
|
3
|
+
`&&o++,o++;continue}i+=s,o++}for((i.length||r.length)&&(r.push(i),n.push(r));n.length&&n[n.length-1].every(e=>e.trim()===``);)n.pop();return n}function a(e){e=e.trim(),e.startsWith(`|`)&&(e=e.slice(1)),e.endsWith(`|`)&&!e.endsWith(`\\|`)&&(e=e.slice(0,-1));let t=[],n=``;for(let r=0;r<e.length;r++){let i=e[r];i===`\\`&&e[r+1]===`|`?(n+=`|`,r++):i===`|`?(t.push(n),n=``):n+=i}return t.push(n),t.map(e=>e.trim())}var o=/^:?-+:?$/;function s(e){let t=e.split(/\r\n|\n|\r/).filter(e=>e.trim()!==``);if(t.length<2||!t[0].includes(`|`))return!1;let n=a(t[1]);return n.length>0&&n.every(e=>o.test(e))}function c(e){let t=e.split(/\r\n|\n|\r/).filter(e=>e.trim()!==``),n=a(t[0]).map((e,t)=>e||`col${t+1}`),r=a(t[1]).map(e=>{let t=e.startsWith(`:`),n=e.endsWith(`:`);return t&&n?`center`:n?`right`:t?`left`:null});for(;r.length<n.length;)r.push(null);return{headers:n,rows:t.slice(2).filter(e=>e.includes(`|`)).map(e=>{let t=a(e).slice(0,n.length);for(;t.length<n.length;)t.push(``);return t}),aligns:r}}var l=/^\(?\$?-?(?:[0-9][0-9,]*(?:\.[0-9]+)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?%?\)?$/,u=/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2})(?:\.\d+)?)?Z?)?$/,d=/^(\d{1,4})([\/\-.])(\d{1,2})\2(\d{1,4})$/,f=/^(\d{1,2})[ \-]([A-Za-z]{3,9})\.?,?[ \-](\d{2,4})$/,p=/^([A-Za-z]{3,9})\.?,?[ \-](\d{1,2}),?[ \-](\d{2,4})$/,m=[`january`,`february`,`march`,`april`,`may`,`june`,`july`,`august`,`september`,`october`,`november`,`december`];function h(e){if(e=e.trim(),!l.test(e))return null;let t=!1;e.startsWith(`(`)&&e.endsWith(`)`)&&(t=!0,e=e.slice(1,-1));let n=!1;e.endsWith(`%`)&&(n=!0,e=e.slice(0,-1)),e=e.replace(/[$,]/g,``);let r=parseFloat(e);if(!isFinite(r))return null;t&&(r=-r),n&&(r/=100);let i=/^([^eE]*)[eE]([+-]?\d+)$/.exec(e),a=i?i[1]:e,o=i?+i[2]:0,s=a.indexOf(`.`),c=Math.max(0,(s<0?0:a.length-s-1)-o);return n&&(c+=2),{v:r,dec:c}}function g(e){let t=e.toLowerCase(),n=m.findIndex(e=>e.startsWith(t)||t===`sept`&&e===`september`);return n<0||t.length<3?null:n+1}function _(e){return e=+e,e<100?e<50?2e3+e:1900+e:e}function v(e,t,n,r=0,i=0,a=0,o=!1){let s=new Date(e,t-1,n,r,i,a);return s.getFullYear()!==e||s.getMonth()!==t-1||s.getDate()!==+n?null:{t:s.getTime(),hasTime:o}}function y(e,t=!1){e=e.trim();let n=u.exec(e);if(n){let[,e,t,r,i,a,o]=n;return v(+e,+t,+r,+(i||0),+(a||0),+(o||0),i!==void 0)}if(n=d.exec(e),n){let[,e,,r,i]=n;if(e.length===4&&i.length<=2)return v(+e,+r,+i);if(e.length<=2&&(i.length===4||i.length===2)){let n=_(i);return+e>12&&+r<=12?v(n,+r,+e):+r>12&&+e<=12?v(n,+e,+r):t?v(n,+r,+e):v(n,+e,+r)}return null}if(n=f.exec(e),n){let e=g(n[2]);return e?v(_(n[3]),e,+n[1]):null}if(n=p.exec(e),n){let e=g(n[1]);return e?v(_(n[3]),e,+n[2]):null}return null}function b(e){let t=d.exec(e.trim());return!!t&&t[1].length<=2&&+t[1]>12&&+t[3]<=12}function x(e){return e.some(e=>{let t=(e??``).trim();return t!==``&&(h(t)!==null||y(t)!==null)})}function S(e){let t=e=>e.type===`date`?`Date`:e.type===`number`?e.format===`year`?`Year`:`Amount`:`Description`,n={},r={};e.forEach(e=>{let r=t(e);n[r]=(n[r]||0)+1}),e.forEach(e=>{let i=t(e);r[i]=(r[i]||0)+1,e.name=n[i]>1?`${i} ${r[i]}`:i})}var C=/\b(year|yr|vintage|cohort)\b/i,w=/\b(amount|amt|balance|bal|price|cost|fee|fees|charge|paid|payment|debit|credit|total|premium|loss|salary|wage|income|expense|revenue|usd|gbp|eur|cad)\b|[$£€]/i;function T(e,t,n){let r=t.filter(e=>e!==null),i=r.every(e=>Number.isInteger(e));if(i&&r.length)return C.test(e)||r.every(e=>e>=1800&&e<=2100)?{format:`year`,dec:0}:w.test(e)?{format:`float`,dec:2}:{format:`int`,dec:0};if(!i&&w.test(e))return{format:`float`,dec:2};let a=0,o=0,s=1/0,c=0;for(let e of r){if(e===0)continue;let t=Math.abs(e);a++,t>o&&(o=t),t<s&&(s=t),c+=t}if(!a)return{format:`float`,dec:Math.min(n,6)};if(!i&&n<=2&&o<1e5)return{format:`float`,dec:2};if(o/s>1e6)return{format:`eng`,dec:0};let l=c/a;return{format:`float`,dec:Math.max(0,Math.min(n,3-Math.floor(Math.log10(l)),6))}}var E={"-9":`n`,"-6":`µ`,"-3":`m`,0:``,3:`k`,6:`M`,9:`G`,12:`T`};function D(e){if(e===0)return`0`;let t=Math.abs(e),n=Math.floor(Math.log10(t)/3)*3;n=Math.max(-9,Math.min(12,n));let r=t/10**n;return(e<0?`-`:``)+Number(r.toPrecision(3))+E[n]}function O(e,t){return e.map((e,n)=>{let r=!0,i=!0,a=0,o=0,s=!1,c=!1,l=Array(t.length).fill(null),u=Array(t.length).fill(null);for(let e=0;e<t.length;e++){let d=(t[e][n]??``).trim();if(d!==``){if(o++,r){let t=h(d);t?(l[e]=t.v,t.dec>a&&(a=t.dec)):r=!1}if(i){let t=y(d,!1);t?(u[e]=t.t,c||=t.hasTime,!s&&b(d)&&(s=!0)):i=!1}if(!r&&!i)break}}if(o===0)return{name:e,type:`text`,values:null};if(r){let t=T(e,l,a);return{name:e,type:`number`,format:t.format,dec:t.dec,values:l}}if(i){if(s){u=Array(t.length).fill(null);for(let e=0;e<t.length;e++){let r=(t[e][n]??``).trim();if(r===``)continue;let i=y(r,!0);i&&(u[e]=i.t)}}return{name:e,type:`date`,hasTime:c,values:u}}return{name:e,type:`text`,values:null}})}function k(e,t=null){let r,a,o=null,l;if(s(e)){if({headers:r,rows:a,aligns:o}=c(e),l=t===!1,l&&(a=[r,...a],r=r.map((e,t)=>`col${t+1}`)),!a.length)throw Error(`Markdown table has no data rows.`)}else{let o=i(e,n(e));if(o.length<2)throw Error(`Need a header row and at least one data row.`);l=t===null?x(o[0]):!t,r=l?o[0].map((e,t)=>`col${t+1}`):o[0].map((e,t)=>e.trim()||`col${t+1}`),a=(l?o:o.slice(1)).map(e=>{if(e.length===r.length)return e;let t=e.slice(0,r.length);for(;t.length<r.length;)t.push(``);return t})}let u=O(r,a);return l&&S(u),o&&u.forEach((e,t)=>{o[t]&&(e.align=o[t])}),{headers:u.map(e=>e.name),rows:a,cols:u,headerless:l}}var A=new Map;function j(e){let t=A.get(e);return t||(t=new Intl.NumberFormat(`en-US`,{minimumFractionDigits:e,maximumFractionDigits:e}),A.set(e,t)),t}function M(e){if(e==null||e===``)return null;if(e===`year`||e===`eng`)return{kind:e};let t=/^(,)?(?:\.(\d+))?([fd%es])$/.exec(e);if(!t)throw Error(`CsvGrid: unrecognized format spec '${e}'`);return{kind:t[3],comma:!!t[1],dec:t[2]===void 0?null:+t[2]}}var N=[[0xe8d4a51000,`T`],[1e9,`G`],[1e6,`M`],[1e3,`k`],[1,``],[.001,`m`],[1e-6,`µ`],[1e-9,`n`]];function P(e,t){switch(t.kind){case`year`:return String(e);case`eng`:return D(e);case`d`:{let n=Math.round(e);return t.comma?j(0).format(n):String(n)}case`f`:{let n=t.dec??2;return t.comma?j(n).format(e):e.toFixed(n)}case`%`:{let n=t.dec??0,r=e*100;return(t.comma?j(n).format(r):r.toFixed(n))+`%`}case`e`:return e.toExponential(t.dec??2);case`s`:{if(t.dec===null||t.dec===void 0)return D(e);if(e===0)return 0 .toFixed(t.dec);let n=Math.abs(e);for(let[r,i]of N)if(n>=r)return(e/r).toFixed(t.dec)+i;return(e/1e-9).toFixed(t.dec)+`n`}}}function F(e){return[...e].map(e=>({l:`left`,r:`right`,c:`center`})[e]??null)}function I(e,t,n){if(e=(e??``).trim(),e===``)return``;if(t.type===`number`){let r=t.values[n];return r===null?e:t.fmt?P(r,t.fmt):t.format===`year`?String(r):t.format===`eng`?D(r):j(t.dec).format(r)}if(t.type===`date`){let r=t.values[n];if(r===null)return e;let i=new Date(r),a=e=>String(e).padStart(2,`0`),o=`${i.getFullYear()}-${a(i.getMonth()+1)}-${a(i.getDate())}`;return t.hasTime&&(o+=` ${a(i.getHours())}:${a(i.getMinutes())}`),o}return e}function L(e,t){if(!Array.isArray(e))throw Error(`CsvGrid: records must be an array.`);let n=e=>e==null||typeof e==`number`&&Number.isNaN(e)?``:String(e),r,i;if(e.length&&Array.isArray(e[0])){if(!t)throw Error(`CsvGrid: columns are required with array-of-arrays records.`);r=t.map(String),i=e.map(e=>r.map((t,r)=>n(e[r])))}else r=(t??Object.keys(e[0]??{})).map(String),i=e.map(e=>r.map(t=>n(e[t])));let a=O(r,i);return{headers:r,rows:i,cols:a,headerless:!1}}function R(e){let t=[];for(let n of e.trim().split(/\s+/)){if(!n)continue;let e={kind:`fuzzy`,negate:!1};n.startsWith(`!`)&&(e.negate=!0,e.kind=`exact`,n=n.slice(1)),n.startsWith(`'`)&&(e.kind=`exact`,n=n.slice(1)),n.startsWith(`^`)&&(e.kind=`prefix`,n=n.slice(1)),n.endsWith(`$`)&&(e.kind=e.kind===`prefix`?`exact`:`suffix`,n=n.slice(0,-1)),n&&(e.cs=/[A-Z]/.test(n),e.str=e.cs?n:n.toLowerCase(),t.push(e))}return t}var z=/[\s_\-\/\\.,:;()[\]{}"']/;function B(e,t){let n=t.length,r=e.length;if(r===0)return 0;if(r>n)return-1;let i=0,a=-1;for(let o=0;o<n;o++)if(t[o]===e[i]&&(i++,i===r)){a=o;break}if(a<0)return-1;i=r-1;let o=a;for(let n=a;n>=0&&!(t[n]===e[i]&&(o=n,i--,i<0));n--);let s=100-3*(a-o+1-r)-Math.min(o,20);i=0;let c=!1;for(let n=o;n<=a&&i<r;n++)t[n]===e[i]?((n===0||z.test(t[n-1]))&&(s+=8),c&&(s+=4),c=!0,i++):c=!1;return s}function V(e,t,n){let r=e.cs?n:t,i,a=0;switch(e.kind){case`exact`:i=r.includes(e.str);break;case`prefix`:i=r.startsWith(e.str);break;case`suffix`:i=r.endsWith(e.str);break;default:{let t=B(e.str,r);i=t>=0,a=t}}return e.negate&&(i=!i),i?a:-1}function H(e,t,n){let r=(e,t)=>e.length?e[Math.floor(t*(e.length-1))]:0,i=n=>e.map((e,i)=>Math.max(t[i],r(e,n))),a=e=>e.reduce((e,t)=>e+t,0),o=i(1);if(a(o)<=n)return o;if(a(i(0))>=n)return i(0);let s=0,c=1;for(let e=0;e<32;e++){let e=(s+c)/2;a(i(e))<=n?s=e:c=e}return i(s)}function U(e,t){if(e<=t)return Array.from({length:e},(e,t)=>t);let n=Array(t),r=e/t;for(let e=0;e<t;e++)n[e]=Math.floor(e*r);return n}function W(e,t){let n=e.trim();if(!n)return null;if(t.type===`number`||t.type===`date`){let e=t.type===`number`?e=>{let t=h(e);return t?t.v:NaN}:e=>{let t=y(e);return t?t.t:NaN},r=/^(>=|<=|>|<|=)\s*(.+)$/.exec(n);if(r){let n=e(r[2]);if(!isNaN(n)){let e=r[1];return(r,i)=>{let a=t.values[i];if(a===null)return!1;switch(e){case`>`:return a>n;case`>=`:return a>=n;case`<`:return a<n;case`<=`:return a<=n;default:return a===n}}}}if(r=/^(.+?)\.\.(.+)$/.exec(n),r){let n=e(r[1]),i=e(r[2]);if(!isNaN(n)&&!isNaN(i))return(e,r)=>{let a=t.values[r];return a!==null&&a>=n&&a<=i}}}let r=n.toLowerCase();return(e,t)=>e.toLowerCase().includes(r)}function G(e){return e.align?`col-${e.type} align-${e.align}`:`col-${e.type}`}function K(e){return e.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`)}var q=1e6,J=2e3,Y=1e4;function X(e,t){let n=document.createElement(e);return t&&(n.className=t),n}return class n{constructor(e,t,n={}){let r=typeof e==`string`?document.querySelector(e):e;if(!r)throw Error(`CsvGrid: target element not found.`);this.root=r,this.opts={globalSearch:!0,columnFilters:!0,sortable:!0,statusBar:!0,expandButtons:!0,align:null,formats:null,renderCap:2e3,eagerCells:2e5,worker:!0,headerMode:`auto`,...n},this.fileName=``,this.headers=[],this.rows=[],this.cols=[],this.formatted=[],this.searchRaw=null,this.searchLow=null,this.searchReady=!1,this.indexing=null,this.loadGen=0,this.scores=[],this.layout=null,this.expandAll=!1,this.manualWidths=new Map,this.guessedHeaders=!1,this.view=[],this.sortCol=null,this.sortDir=1,this.globalFilter=``,this.colFilters=[],this.showAll=!1,this._worker=void 0,this._pending=new Map,this._buildScaffold(),t&&this.setData(t)}_buildScaffold(){let e=this.opts,t=this.root;if(t.classList.add(`csvgrid`),t.replaceChildren(),this.els={},e.globalSearch||e.expandButtons){let n=X(`div`,`csvgrid-toolbar`);if(e.globalSearch){let e=X(`input`,`csvgrid-search`);e.type=`text`,e.placeholder=`fzf search: term 'exact !not ^pre fix$`,e.title=`Space-separated terms AND together. Fuzzy by default; 'exact, !exclude, ^prefix, suffix$. Uppercase = case-sensitive.`,e.addEventListener(`input`,()=>this.setGlobalFilter(e.value)),e.addEventListener(`keydown`,t=>{t.key===`Escape`&&(t.preventDefault(),e.value=``,e.blur(),this.setGlobalFilter(``))}),n.appendChild(e),this.els.search=e}if(e.expandButtons){let e=X(`button`,`csvgrid-btn`);e.type=`button`,e.textContent=`Expand`,e.title=`Expand all columns to their full natural width (table scrolls horizontally)`,e.addEventListener(`click`,()=>this.expand());let t=X(`button`,`csvgrid-btn`);t.type=`button`,t.textContent=`Contract`,t.title=`Back to fitted widths (equal-risk squeeze); also clears any dragged widths`,t.addEventListener(`click`,()=>this.contract()),n.append(e,t)}t.appendChild(n)}let n=X(`div`,`csvgrid-scroll`),r=X(`table`,`csvgrid-table`),i=X(`thead`),a=X(`tbody`);r.append(i,a),n.appendChild(r),t.appendChild(n);let o=X(`div`,`csvgrid-capnote csvgrid-hidden`),s=X(`button`,`csvgrid-btn`);s.type=`button`,s.addEventListener(`click`,()=>{this.showAll=!0,this.renderBody(),this.renderStatus()}),o.appendChild(s),t.appendChild(o);let c=X(`div`,`csvgrid-error csvgrid-hidden`);t.appendChild(c);let l=null;e.statusBar===!0?(l=X(`div`,`csvgrid-status`),t.appendChild(l)):e.statusBar&&(l=e.statusBar),Object.assign(this.els,{table:r,head:i,body:a,scroll:n,capNote:o,showAllBtn:s,error:c,status:l}),r.addEventListener(`mouseover`,e=>{let t=e.target.closest(`td, th`);t&&!t.title&&t.scrollWidth>t.clientWidth&&(t.title=t.textContent)})}setData(e){let t=++this.loadGen,n=new Promise((n,r)=>{this._resolveData(e,t).then(({d:e,name:r})=>{t===this.loadGen&&(this._install(e,r),n())},e=>{t===this.loadGen&&(this._showError(e.message||String(e)),r(e))})});return n.catch(()=>{}),n}async _resolveData(e,t){if(!e||typeof e!=`object`)throw Error(`CsvGrid: data must be {csv}, {records[, columns]}, or {url}.`);if(this._headerMode=e.headerMode??this.opts.headerMode,e.url!==void 0){let n=String(e.url),r=e.name??decodeURIComponent(n.split(`/`).pop()||n),i=await fetch(n);if(!i.ok)throw Error(`HTTP ${i.status}`);return{d:await this._parse(await i.text(),t,r),name:r}}if(e.csv!==void 0){let n=e.name??``;return{d:await this._parse(e.csv,t,n),name:n}}if(e.records!==void 0)return{d:L(e.records,e.columns),name:e.name??``};throw Error(`CsvGrid: data must be {csv}, {records[, columns]}, or {url}.`)}_parse(e,n,r){if(e=t(e),!e.trim())throw Error(`No data found.`);let i=this._headerMode===`first-row`?!0:this._headerMode===`headerless`?!1:null,a=this.opts.worker!==!1&&e.length>=q?this._getWorker():null;return a?(this._setStatus(`parsing ${r||`data`} (${(e.length/1e6).toFixed(1)} MB)…`),new Promise((t,r)=>{this._pending.set(n,{resolve:t,reject:r}),a.postMessage({gen:n,text:e,headerOverride:i})})):k(e,i)}_getWorker(){if(this._worker===void 0){this._worker=null;try{let t=typeof this.opts.worker==`string`?new Worker(this.opts.worker):new Worker(new URL(``+new URL(`csv-grid.worker.js`,e).href,``+e),{type:`module`});t.onmessage=e=>{let{gen:t,result:n,error:r}=e.data,i=this._pending.get(t);i&&(this._pending.delete(t),r?i.reject(Error(r)):i.resolve(n))},t.onerror=()=>{let e=[...this._pending.values()];this._pending.clear();for(let t of e)t.reject(Error(`Background parse failed.`))},this._worker=t}catch{}}return this._worker}_install(e,t){let{rows:n,cols:r}=e;if(this.opts.align){let e=F(this.opts.align);r.forEach((t,n)=>{e[n]&&(t.align=e[n])})}if(this.opts.formats&&r.forEach((e,t)=>{e.fmt=M(this.opts.formats[t])}),this.fileName=t||``,this.guessedHeaders=e.headerless,this.headers=e.headers,this.rows=n,this.cols=r,this.formatted=Array(n.length),this.searchRaw=null,this.searchLow=null,this.searchReady=!1,this.indexing=null,n.length*r.length<=this.opts.eagerCells){for(let e=0;e<n.length;e++)this.getFormattedRow(e);this.searchRaw=this.formatted.map((e,t)=>e.join(` `)+` `+n[t].join(` `)),this.searchLow=this.searchRaw.map(e=>e.toLowerCase()),this.searchReady=!0}this.sortCol=null,this.sortDir=1,this.globalFilter=``,this.colFilters=Array(r.length).fill(``),this.manualWidths=new Map,this.showAll=!1,this.els.search&&(this.els.search.value=``),this.els.error.classList.add(`csvgrid-hidden`),this.renderHead(),this.layout=this.measureLayout(),this.applyLayout(),this.refresh()}_showError(e){this.els.error.textContent=e,this.els.error.classList.remove(`csvgrid-hidden`)}_setStatus(e){this.els.status&&(this.els.status.textContent=e)}destroy(){this.loadGen++,this._pending.clear(),this._worker&&=(this._worker.terminate(),null),this.root.classList.remove(`csvgrid`),this.root.replaceChildren()}setGlobalFilter(e){this.globalFilter=e,this.refresh()}clearFilters(){this.globalFilter=``,this.colFilters=this.colFilters.map(()=>``),this.els.search&&(this.els.search.value=``),this.renderHead(),this.refresh()}expand(){this.expandAll=!0,this.applyLayout()}contract(){this.expandAll=!1,this.manualWidths.clear(),this.applyLayout()}measureLayout(){let e=(n._canvas||=document.createElement(`canvas`)).getContext(`2d`),t=getComputedStyle(this.els.table),r=`${t.fontSize} ${t.fontFamily}`,i=U(this.rows.length,J),a=[],o=[];for(let t=0;t<this.cols.length;t++){e.font=`bold ${r}`,o.push(Math.max(50,Math.ceil(e.measureText(this.cols[t].name).width)+14+18)),e.font=r;let n=[];for(let r of i){let i=this.getFormattedRow(r)[t];i!==``&&n.push(Math.ceil(e.measureText(i).width)+18)}n.sort((e,t)=>e-t),a.push(n)}return{arrays:a,floors:o}}startColResize(e,t){e.preventDefault(),e.stopPropagation();let n=this.els.table,r=n.querySelectorAll(`colgroup col`)[t];if(!r)return;let i=e.clientX,a=parseFloat(r.style.width);document.body.classList.add(`csvgrid-resizing`);let o=()=>{let e=0;n.querySelectorAll(`colgroup col`).forEach(t=>{e+=parseFloat(t.style.width)}),n.style.width=e+`px`},s=e=>{let n=Math.max(24,Math.round(a+e.clientX-i));this.manualWidths.set(t,n),r.style.width=n+`px`,o()},c=()=>{document.body.classList.remove(`csvgrid-resizing`),document.removeEventListener(`mousemove`,s),document.removeEventListener(`mouseup`,c)};document.addEventListener(`mousemove`,s),document.addEventListener(`mouseup`,c)}fitColumn(e){let{arrays:t,floors:n}=this.layout,r=Math.max(n[e],t[e].length?t[e][t[e].length-1]:0);this.manualWidths.set(e,r),this.applyLayout()}applyLayout(){if(!this.layout)return;let e=this.els.table,t=this.expandAll?1/0:e.parentElement.clientWidth;if(!t)return;let n=H(this.layout.arrays,this.layout.floors,t);for(let[e,t]of this.manualWidths)e<n.length&&(n[e]=t);let r=e.querySelector(`colgroup`);r&&r.remove(),r=document.createElement(`colgroup`);for(let e of n){let t=document.createElement(`col`);t.style.width=e+`px`,r.appendChild(t)}e.prepend(r),e.style.tableLayout=`fixed`,e.style.width=n.reduce((e,t)=>e+t,0)+`px`}getFormattedRow(e){let t=this.formatted[e];return t||(t=this.cols.map((t,n)=>I(this.rows[e][n],t,e)),this.formatted[e]=t),t}buildSearchIndexChunked(){let e=this.loadGen,t=this.rows.length,n=Array(t),r=Array(t),i=0;this.indexing=0;let a=()=>{if(e!==this.loadGen)return;let o=Math.min(t,i+Y);for(;i<o;i++){let e=this.getFormattedRow(i).join(` `)+` `+this.rows[i].join(` `);n[i]=e,r[i]=e.toLowerCase()}i<t?(this.indexing=i/t,this.renderStatus(),setTimeout(a,0)):(this.searchRaw=n,this.searchLow=r,this.searchReady=!0,this.indexing=null,this.refresh())};a()}rebuildView(){let{rows:e,cols:t}=this,n=R(this.globalFilter);n.length&&!this.searchReady&&(this.indexing===null&&this.buildSearchIndexChunked(),n=[]);let r=n.some(e=>e.kind===`fuzzy`&&!e.negate),i=this.colFilters.map((e,n)=>W(e||``,t[n])),a=i.some(e=>e)||n.length,o=[];this.scores=[];for(let t=0;t<e.length;t++){let r=0;if(a){let a=!0;for(let e of n){let n=V(e,this.searchLow[t],this.searchRaw[t]);if(n<0){a=!1;break}r+=n}if(a){for(let n=0;n<i.length;n++)if(i[n]&&!i[n](e[t][n]??``,t)){a=!1;break}}if(!a)continue}this.scores[t]=r,o.push(t)}let s=this.sortCol;if(s===null&&r)o.sort((e,t)=>this.scores[t]-this.scores[e]||e-t);else if(s!==null){let e=this.cols[s],t=this.sortDir;if(e.type===`text`){let e=new Intl.Collator(`en`,{sensitivity:`base`,numeric:!0});o.sort((n,r)=>{let i=(this.rows[n][s]??``).trim(),a=(this.rows[r][s]??``).trim();return i===``||a===``?i===a?0:i===``?1:-1:t*e.compare(i,a)})}else o.sort((n,r)=>{let i=e.values[n],a=e.values[r];return i===null||a===null?i===a?0:i===null?1:-1:t*(i-a)})}this.view=o}renderHead(){let{cols:e}=this,t=this.els.head;t.innerHTML=``;let n=document.createElement(`tr`);if(e.forEach((e,t)=>{let r=document.createElement(`th`);r.className=G(e),this.opts.sortable?(r.innerHTML=`<span class="sort-arrow">${this.sortCol===t?this.sortDir===1?`▲`:`▼`:``}</span>${K(e.name)}`,r.title=`${e.name} (${e.type}) — click to sort`,r.addEventListener(`click`,()=>this.onSort(t))):(r.innerHTML=`<span class="sort-arrow"></span>${K(e.name)}`,r.title=`${e.name} (${e.type})`,r.classList.add(`csvgrid-nosort`));let i=document.createElement(`span`);i.className=`col-resizer`,i.title=`Drag to resize — double-click to fit content`,i.addEventListener(`mousedown`,e=>this.startColResize(e,t)),i.addEventListener(`dblclick`,e=>{e.stopPropagation(),this.fitColumn(t)}),i.addEventListener(`click`,e=>e.stopPropagation()),r.appendChild(i),n.appendChild(r)}),t.appendChild(n),!this.opts.columnFilters)return;let r=document.createElement(`tr`);r.className=`filter-row`,e.forEach((e,t)=>{let n=document.createElement(`th`),i=document.createElement(`input`);i.type=`text`,i.className=`csvgrid-filter`,i.placeholder=e.type===`text`?`filter`:`filter, >, .. `,i.value=this.colFilters[t]||``,i.addEventListener(`input`,()=>{this.colFilters[t]=i.value,i.classList.toggle(`active-filter`,i.value.trim()!==``),this.refresh()}),i.addEventListener(`keydown`,e=>{e.key===`Escape`&&(e.preventDefault(),i.value=``,this.colFilters[t]=``,i.classList.remove(`active-filter`),i.blur(),this.refresh())}),n.appendChild(i),r.appendChild(n)}),t.appendChild(r)}renderBody(){let{cols:e,view:t}=this,n=this.showAll?t.length:Math.min(t.length,this.opts.renderCap),r=[];for(let i=0;i<n;i++){let n=t[i],a=this.getFormattedRow(n),o=e.map((e,t)=>{let n=a[t];return n===``?`<td class="${G(e)} blank">·</td>`:`<td class="${G(e)}">${K(n)}</td>`});r.push(`<tr>${o.join(``)}</tr>`)}this.els.body.innerHTML=r.join(``);let i=this.els.capNote;t.length>n?(i.classList.remove(`csvgrid-hidden`),this.els.showAllBtn.textContent=`Showing first ${n.toLocaleString()} of ${t.length.toLocaleString()} rows — show all`):i.classList.add(`csvgrid-hidden`)}renderStatus(){if(!this.els.status)return;let e=e=>e.toLocaleString(),t=this.rows.length,n=this.view.length,r=this.showAll?n:Math.min(n,this.opts.renderCap),i=this.fileName?this.fileName+` — `:``;i+=n===t?`${e(t)} rows`:`${e(n)} of ${e(t)} rows`,i+=` × ${this.cols.length} cols`,r<n&&(i+=` — showing rows 1–${e(r)}`),this.guessedHeaders&&(i+=` (headers guessed)`),this.indexing!==null&&(i+=` — indexing search ${Math.round(this.indexing*100)}%`),this.els.status.textContent=i}refresh(){this.rebuildView(),this.renderBody(),this.renderStatus()}onSort(e){this.sortCol===e?this.sortDir===1?this.sortDir=-1:(this.sortCol=null,this.sortDir=1):(this.sortCol=e,this.sortDir=1),this.renderHead(),this.refresh()}}});
|
|
4
|
+
//# sourceMappingURL=csv-grid.umd.js.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: csv-grid
|
|
3
|
+
Version: 3.0.5
|
|
4
|
+
Summary: Emit csv-viewer's CsvGrid interactive tables from pandas DataFrames (Jupyter / Quarto / static HTML).
|
|
5
|
+
Project-URL: Homepage, https://github.com/mynl/CSV_Viewer
|
|
6
|
+
Project-URL: Live viewer, https://mynl.github.io/CSV_Viewer/
|
|
7
|
+
Project-URL: Repository, https://github.com/mynl/CSV_Viewer
|
|
8
|
+
Project-URL: Changelog, https://github.com/mynl/CSV_Viewer/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: "Stephen J. Mildenhall" <stephen.j.mildenhall@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: csv,dataframe,grid,jupyter,pandas,quarto,table
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Jupyter
|
|
16
|
+
Classifier: Intended Audience :: Science/Research
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: pandas>=2.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# csv_grid
|
|
28
|
+
|
|
29
|
+
Python emitter for **CsvGrid**, the embeddable interactive table of the
|
|
30
|
+
[csv-viewer](https://github.com/mynl/CSV_Viewer) project: render a pandas
|
|
31
|
+
DataFrame as a sortable, filterable, type-aware grid in Jupyter, Quarto
|
|
32
|
+
(`.qmd`), or any static HTML you generate.
|
|
33
|
+
|
|
34
|
+
The grid re-infers column types from the data exactly as the viewer app
|
|
35
|
+
does (numbers right with greater_tables-style formatting, dates ISO and
|
|
36
|
+
centered, fzf search, equal-risk column widths). NaN / None become blank
|
|
37
|
+
cells.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
uv add csv-grid # or: pip install csv-grid
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
or local path install from a clone of this repo:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
uv add --editable path/to/csv-viewer/python
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The grid's built JS/CSS assets ship inside the package (refreshed by the
|
|
52
|
+
repo's `npm run build`).
|
|
53
|
+
|
|
54
|
+
## Use
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from csv_grid import show, to_html
|
|
58
|
+
|
|
59
|
+
show(df) # Jupyter / qmd cell: display the grid
|
|
60
|
+
show(df, align="llrcr", fmt=[None, None, ",d", "year", ",.2f"])
|
|
61
|
+
|
|
62
|
+
html = to_html(df, name="results.df", assets="inline") # fragment string
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `show(df, **options)` displays via IPython. The JS + CSS assets are
|
|
66
|
+
emitted once per kernel session (so once per rendered page); pass
|
|
67
|
+
`assets="inline"` to force re-emission, or `assets="https://…/base"`
|
|
68
|
+
to load them from a URL instead of inlining.
|
|
69
|
+
- `to_html(df, **options)` returns an HTML fragment. The first fragment
|
|
70
|
+
on a page should carry the assets (`assets="inline"` — the default —
|
|
71
|
+
or a base URL); pass `assets=False` for subsequent tables.
|
|
72
|
+
- `payload(df)` returns the `{records, columns}` dict the grid consumes,
|
|
73
|
+
if you want to ship data yourself.
|
|
74
|
+
- Options mirror the JS API in snake_case: `global_search`,
|
|
75
|
+
`column_filters`, `sortable`, `status_bar`, `expand_buttons`, `align`
|
|
76
|
+
(`'llrcr…'`), `formats`/`fmt` (per-column `[,][.N](f|d|%|e|s)`,
|
|
77
|
+
`'year'`, `'eng'`, None = auto), `render_cap`, `eager_cells`,
|
|
78
|
+
`worker` (default False — data is inlined), plus `name` (status line)
|
|
79
|
+
and `index` (include the DataFrame index as leading columns).
|
|
80
|
+
|
|
81
|
+
Dates are emitted ISO (`yyyy-mm-dd`, with `hh:mm` only when a column has
|
|
82
|
+
non-midnight times); integral float columns are emitted as integers so
|
|
83
|
+
the grid's integer/year rules apply.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
csv_grid/__init__.py,sha256=znD48_ITsb667TuieX6nBzzKFHuAIgU0Nbg7J9OfWak,6505
|
|
2
|
+
csv_grid/assets/csv-grid.css,sha256=Z2D4CAb-uotuBIZf7V5ZtuKsf8Q8-cOSOFJ5yijrQMs,4247
|
|
3
|
+
csv_grid/assets/csv-grid.umd.js,sha256=aqZLDtxnM9D6m-vprV6kQrDUkMTrS1HiJYxOQS7tURg,23268
|
|
4
|
+
csv_grid-3.0.5.dist-info/METADATA,sha256=TIlRhVscn80cucdx1Dmrg8yWQZQfnf6tJORFNYRs24k,3382
|
|
5
|
+
csv_grid-3.0.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
csv_grid-3.0.5.dist-info/licenses/LICENSE,sha256=-uJvm77cnQ2sb8EVnjGMwdI0i13qk8axczilaSZ1ihY,1078
|
|
7
|
+
csv_grid-3.0.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stephen J. Mildenhall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|