coeftable 0.2.1__tar.gz → 0.3.1__tar.gz

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.
Files changed (41) hide show
  1. {coeftable-0.2.1 → coeftable-0.3.1}/.github/workflows/publish.yml +18 -11
  2. {coeftable-0.2.1 → coeftable-0.3.1}/.github/workflows/release.yml +37 -5
  3. coeftable-0.3.1/CHANGELOG.md +101 -0
  4. {coeftable-0.2.1 → coeftable-0.3.1}/PKG-INFO +83 -17
  5. {coeftable-0.2.1 → coeftable-0.3.1}/README.md +82 -16
  6. {coeftable-0.2.1 → coeftable-0.3.1}/pyproject.toml +6 -0
  7. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/_version.py +2 -2
  8. coeftable-0.3.1/src/coeftable/collapsible.py +166 -0
  9. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/frame.py +54 -4
  10. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/grid.py +74 -48
  11. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/render.py +13 -0
  12. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/series.py +105 -27
  13. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/spec.py +416 -72
  14. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/svg.py +311 -78
  15. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/theme.py +83 -5
  16. coeftable-0.3.1/tests/test_collapsible.py +260 -0
  17. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_frame.py +140 -0
  18. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_public_api.py +21 -0
  19. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_render.py +55 -0
  20. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_series.py +117 -22
  21. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_sparkline.py +667 -0
  22. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_svg.py +398 -0
  23. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_theme.py +42 -0
  24. coeftable-0.2.1/CHANGELOG.md +0 -42
  25. {coeftable-0.2.1 → coeftable-0.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {coeftable-0.2.1 → coeftable-0.3.1}/.github/release.yml +0 -0
  27. {coeftable-0.2.1 → coeftable-0.3.1}/.github/workflows/ci.yml +0 -0
  28. {coeftable-0.2.1 → coeftable-0.3.1}/.github/workflows/post-release.yml +0 -0
  29. {coeftable-0.2.1 → coeftable-0.3.1}/.gitignore +0 -0
  30. {coeftable-0.2.1 → coeftable-0.3.1}/.pre-commit-config.yaml +0 -0
  31. {coeftable-0.2.1 → coeftable-0.3.1}/LICENSE +0 -0
  32. {coeftable-0.2.1 → coeftable-0.3.1}/Makefile +0 -0
  33. {coeftable-0.2.1 → coeftable-0.3.1}/docs/images/example.png +0 -0
  34. {coeftable-0.2.1 → coeftable-0.3.1}/docs/images/trend-example.png +0 -0
  35. {coeftable-0.2.1 → coeftable-0.3.1}/noxfile.py +0 -0
  36. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/__init__.py +0 -0
  37. {coeftable-0.2.1 → coeftable-0.3.1}/src/coeftable/format.py +0 -0
  38. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_format.py +0 -0
  39. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_package.py +0 -0
  40. {coeftable-0.2.1 → coeftable-0.3.1}/tests/test_spec.py +0 -0
  41. {coeftable-0.2.1 → coeftable-0.3.1}/uv.lock +0 -0
@@ -11,11 +11,6 @@ on:
11
11
  description: "Git ref to build. Defaults to the triggering ref."
12
12
  required: false
13
13
  type: string
14
- publish:
15
- description: "Upload the built package to PyPI."
16
- required: false
17
- default: false
18
- type: boolean
19
14
  workflow_dispatch:
20
15
 
21
16
  jobs:
@@ -40,21 +35,33 @@ jobs:
40
35
 
41
36
  publish:
42
37
  runs-on: ubuntu-latest
43
- # A tag pushed by a workflow using the default token does not raise a
44
- # push event, so a release cut by automation asks for the upload
45
- # explicitly. Any other run sitting on a tag -- a hand-pushed tag, or a
46
- # manual run selecting one, which is how a stranded release gets
47
- # published -- is also a release build. A run on a branch never is.
48
- if: inputs.publish || startsWith(github.ref, 'refs/tags')
38
+ # PyPI Trusted Publishing does not support reusable workflows
39
+ # (https://github.com/pypa/gh-action-pypi-publish/issues/166): a
40
+ # `workflow_call` invocation of this job would present the *caller's*
41
+ # workflow identity, not this one, and fail to match the Trusted
42
+ # Publisher entry registered for this workflow. So this job never
43
+ # publishes when called reusably -- release.yml calls this workflow
44
+ # for `build-package` only and does its own top-level publish. This
45
+ # job runs only for a direct trigger of this workflow sitting on a
46
+ # tag: a tag push, or a manual run selecting a tag ref (how a
47
+ # stranded release gets republished). A run on a branch never does.
48
+ if: startsWith(github.ref, 'refs/tags')
49
49
  environment:
50
50
  name: publish
51
51
  url: https://pypi.org/p/coeftable
52
52
  needs: build-package
53
53
  permissions:
54
+ contents: read
54
55
  id-token: write
55
56
  steps:
56
57
  - uses: actions/download-artifact@v8
57
58
  with:
58
59
  name: Packages
59
60
  path: dist
61
+ # Must be invoked straight from the job, never via a local composite
62
+ # action. This action builds a Docker trampoline whose image it names
63
+ # from `github.action_repository`/`github.action_ref`, and both are
64
+ # empty for an action nested inside another composite action
65
+ # (actions/runner#2473). It then falls back to this repository and its
66
+ # branch, and tries to pull an image that was never published.
60
67
  - uses: pypa/gh-action-pypi-publish@release/v1
@@ -90,10 +90,12 @@ jobs:
90
90
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
91
91
  run: gh release create "${{ steps.version.outputs.tag }}" --generate-notes
92
92
 
93
- publish:
94
- # Called directly rather than left to the tag-push trigger on the publish
95
- # workflow: the tag above is pushed with the workflow token, and GitHub
96
- # does not raise workflow-triggering events for it.
93
+ build:
94
+ # Called reusably for the build step only: `publish.yml`'s own
95
+ # `publish` job is gated on `startsWith(github.ref, 'refs/tags')`,
96
+ # which is false here (`github.ref` reflects this run's own
97
+ # branch/dispatch ref, not the `ref:` input below), so it no-ops --
98
+ # only `build-package` executes.
97
99
  needs: tag
98
100
  uses: ./.github/workflows/publish.yml
99
101
  permissions:
@@ -102,7 +104,37 @@ jobs:
102
104
  id-token: write
103
105
  with:
104
106
  ref: ${{ needs.tag.outputs.tag }}
105
- publish: true
107
+
108
+ publish:
109
+ # A real top-level job, not delegated via `uses:`: PyPI Trusted
110
+ # Publishing does not support reusable workflows -- the OIDC
111
+ # certificate's Build Config URI reflects the top-level caller
112
+ # (release.yml) for a `uses:`-invoked job, which then fails to match
113
+ # a Trusted Publisher entry registered for the *called* workflow.
114
+ # See https://github.com/pypa/gh-action-pypi-publish/issues/166 and
115
+ # the "Non-goals" section of that action's README: "keep the job
116
+ # calling pypi-publish in a top-level [workflow]". `build-package`
117
+ # itself has no such restriction and stays reusable above.
118
+ needs: build
119
+ runs-on: ubuntu-latest
120
+ environment:
121
+ name: publish
122
+ url: https://pypi.org/p/coeftable
123
+ permissions:
124
+ contents: read
125
+ id-token: write
126
+ steps:
127
+ - uses: actions/download-artifact@v8
128
+ with:
129
+ name: Packages
130
+ path: dist
131
+ # Must be invoked straight from the job, never via a local composite
132
+ # action. This action builds a Docker trampoline whose image it names
133
+ # from `github.action_repository`/`github.action_ref`, and both are
134
+ # empty for an action nested inside another composite action
135
+ # (actions/runner#2473). It then falls back to this repository and its
136
+ # branch, and tries to pull an image that was never published.
137
+ - uses: pypa/gh-action-pypi-publish@release/v1
106
138
 
107
139
  changelog:
108
140
  # Same reason as the publish job above: the release is created with the
@@ -0,0 +1,101 @@
1
+ <a id="v0.3.1"></a>
2
+ # [v0.3.1](https://github.com/kylejcaron/coeftable/releases/tag/v0.3.1) - 2026-08-14
3
+
4
+ <!-- Release notes generated using configuration in .github/release.yml at v0.3.1 -->
5
+
6
+ ## What's Changed
7
+ ### Bug Fixes
8
+ * Fix row-group handling: labels spanning groups, row striping, and layout key collisions by [@kylejcaron](https://github.com/kylejcaron) in [#26](https://github.com/kylejcaron/coeftable/pull/26)
9
+ * Build core metadata 2.4 so releases pass validation by [@kylejcaron](https://github.com/kylejcaron) in [#27](https://github.com/kylejcaron/coeftable/pull/27)
10
+ ### Maintenance
11
+ * Update changelog for v0.3.0 by [@github-actions](https://github.com/github-actions)[bot] in [#21](https://github.com/kylejcaron/coeftable/pull/21)
12
+ * Fix automated releases: PyPI Trusted Publishing rejects reusable workflows by [@kylejcaron](https://github.com/kylejcaron) in [#22](https://github.com/kylejcaron/coeftable/pull/22)
13
+
14
+
15
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.3.0...v0.3.1
16
+
17
+ [Changes][v0.3.1]
18
+
19
+
20
+ <a id="v0.3.0"></a>
21
+ # [v0.3.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.3.0) - 2026-08-05
22
+
23
+ <!-- Release notes generated using configuration in .github/release.yml at v0.3.0 -->
24
+
25
+ ## What's Changed
26
+ ### Maintenance
27
+ * Update changelog for v0.2.1 by [@github-actions](https://github.com/github-actions)[bot] in [#13](https://github.com/kylejcaron/coeftable/pull/13)
28
+ * Document the PyPI trusted-publisher requirement for both entry points by [@kylejcaron](https://github.com/kylejcaron) in [#14](https://github.com/kylejcaron/coeftable/pull/14)
29
+ * Add collapsible group sections by [@kylejcaron](https://github.com/kylejcaron) in [#18](https://github.com/kylejcaron/coeftable/pull/18)
30
+ * Allow ref=None on Sparkline to drop the reference line and its domain constraint by [@kylejcaron](https://github.com/kylejcaron) in [#19](https://github.com/kylejcaron/coeftable/pull/19)
31
+ * Support multiple overlaid series in one sparkline cell by [@kylejcaron](https://github.com/kylejcaron) in [#20](https://github.com/kylejcaron/coeftable/pull/20)
32
+
33
+
34
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.2.1...v0.3.0
35
+
36
+ [Changes][v0.3.0]
37
+
38
+
39
+ <a id="v0.2.1"></a>
40
+ # [v0.2.1](https://github.com/kylejcaron/coeftable/releases/tag/v0.2.1) - 2026-08-05
41
+
42
+ <!-- Release notes generated using configuration in .github/release.yml at v0.2.1 -->
43
+
44
+ ## What's Changed
45
+ ### Maintenance
46
+ * Update changelog for v0.2.0 by [@github-actions](https://github.com/github-actions)[bot] in [#11](https://github.com/kylejcaron/coeftable/pull/11)
47
+ * Publish releases to PyPI automatically by [@kylejcaron](https://github.com/kylejcaron) in [#12](https://github.com/kylejcaron/coeftable/pull/12)
48
+
49
+ ## New Contributors
50
+ * [@github-actions](https://github.com/github-actions)[bot] made their first contribution in [#11](https://github.com/kylejcaron/coeftable/pull/11)
51
+
52
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.2.0...v0.2.1
53
+
54
+ [Changes][v0.2.1]
55
+
56
+
57
+ <a id="v0.2.0"></a>
58
+ # [v0.2.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.2.0) - 2026-08-04
59
+
60
+ <!-- Release notes generated using configuration in .github/release.yml at v0.2.0 -->
61
+
62
+ ## What's Changed
63
+ ### Maintenance
64
+ * ci: add workflow_dispatch release workflow with semver bump by [@kylejcaron](https://github.com/kylejcaron) in [#7](https://github.com/kylejcaron/coeftable/pull/7)
65
+ * Sparkline column: inline line plots with uncertainty by [@kylejcaron](https://github.com/kylejcaron) in [#10](https://github.com/kylejcaron/coeftable/pull/10)
66
+
67
+
68
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.1.0...v0.2.0
69
+
70
+ [Changes][v0.2.0]
71
+
72
+
73
+ <a id="v0.1.0"></a>
74
+ # [v0.1.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.1.0) - 2026-07-28
75
+
76
+ <!-- Release notes generated using configuration in .github/release.yml at main -->
77
+
78
+ ## What's Changed
79
+ ### Maintenance
80
+ * chore: install pre-commit hooks during setup by [@kylejcaron](https://github.com/kylejcaron) in [#1](https://github.com/kylejcaron/coeftable/pull/1)
81
+ * ci: drop redundant lint and typecheck jobs by [@kylejcaron](https://github.com/kylejcaron) in [#2](https://github.com/kylejcaron/coeftable/pull/2)
82
+ * Theming by [@kylejcaron](https://github.com/kylejcaron) in [#3](https://github.com/kylejcaron/coeftable/pull/3)
83
+ * docs patch: restore Parameters section header on Forest docstring by [@kylejcaron](https://github.com/kylejcaron) in [#4](https://github.com/kylejcaron/coeftable/pull/4)
84
+ * chore: complete package metadata for PyPI publishing by [@kylejcaron](https://github.com/kylejcaron) in [#5](https://github.com/kylejcaron/coeftable/pull/5)
85
+ * docs: add hero image, fix stale theming section in README by [@kylejcaron](https://github.com/kylejcaron) in [#6](https://github.com/kylejcaron/coeftable/pull/6)
86
+
87
+ ## New Contributors
88
+ * [@kylejcaron](https://github.com/kylejcaron) made their first contribution in [#1](https://github.com/kylejcaron/coeftable/pull/1)
89
+
90
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/commits/v0.1.0
91
+
92
+ [Changes][v0.1.0]
93
+
94
+
95
+ [v0.3.1]: https://github.com/kylejcaron/coeftable/compare/v0.3.0...v0.3.1
96
+ [v0.3.0]: https://github.com/kylejcaron/coeftable/compare/v0.2.1...v0.3.0
97
+ [v0.2.1]: https://github.com/kylejcaron/coeftable/compare/v0.2.0...v0.2.1
98
+ [v0.2.0]: https://github.com/kylejcaron/coeftable/compare/v0.1.0...v0.2.0
99
+ [v0.1.0]: https://github.com/kylejcaron/coeftable/tree/v0.1.0
100
+
101
+ <!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.1 -->
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coeftable
3
- Version: 0.2.1
3
+ Version: 0.3.1
4
4
  Summary: Publication-quality summary tables for estimates with uncertainty.
5
5
  Project-URL: Homepage, https://github.com/kylejcaron/coeftable
6
6
  Project-URL: Repository, https://github.com/kylejcaron/coeftable
@@ -87,10 +87,11 @@ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
87
87
  ```
88
88
 
89
89
  A `CoefTable` renders itself in marimo, Jupyter, and any other `_repr_html_`-aware viewer —
90
- leave it as the last expression in a cell, no extra call needed. Outside a notebook, use
91
- `.gt()` to reach the underlying [great_tables](https://posit-dev.github.io/great-tables/)
92
- object: `table.gt().as_raw_html()` for an HTML string, `table.gt().save("t.png")` for an
93
- image, `table.gt().tab_options(...)` to keep styling with great_tables' own API.
90
+ leave it as the last expression in a cell, no extra call needed. Outside a notebook,
91
+ `table.as_raw_html()` is the HTML string entry point. `.gt()` remains the escape hatch to the
92
+ underlying [great_tables](https://posit-dev.github.io/great-tables/) object itself:
93
+ `table.gt().save("t.png")` for an image, `table.gt().tab_options(...)` to keep styling with
94
+ great_tables' own API.
94
95
 
95
96
  ## Data shape
96
97
 
@@ -103,7 +104,15 @@ a `parameter` column.
103
104
  **Dimensions:**
104
105
  - `rows` — the label for each row in the table (e.g. a metric name).
105
106
  - `nest` — an optional secondary label stacked below each row.
106
- - `groups` — an optional column whose values produce section headers.
107
+ - `groups` — an optional column whose values produce section headers. A row
108
+ label may appear under more than one group, in which case it renders once per
109
+ section — so the same set of metrics can be reported per region without
110
+ inventing a `nest` column to tell them apart. Pass
111
+ `collapsible_groups=True` to make those sections collapsible in the rendered
112
+ HTML — a pure CSS toggle (relies on `:has()`, Baseline since late 2023; on an
113
+ older browser without it the toggle no-ops and sections stay expanded), no
114
+ JavaScript, sections start expanded. Applies to `as_raw_html()`/`_repr_html_`;
115
+ `.gt()` is unaffected.
107
116
  - `split_columns` — an optional column whose values produce repeated column
108
117
  groups side by side, useful for comparing methods.
109
118
 
@@ -172,8 +181,35 @@ methods = pl.DataFrame(
172
181
  (
173
182
  ct.CoefTable(
174
183
  methods, rows="metric", split_columns="method", estimate="est", ci=("lb", "ub")
184
+ ).header("Cohort Revenue by Method")
185
+ )
186
+ ```
187
+
188
+ ## Repeating metrics across sections
189
+
190
+ A row label may appear under more than one `groups` value. The same metrics can
191
+ therefore be reported per region, each section repeating the full set:
192
+
193
+ ```python
194
+ import polars as pl
195
+ import coeftable as ct
196
+
197
+ regions = pl.DataFrame(
198
+ {
199
+ "metric": ["Revenue", "Signups", "Revenue", "Signups"],
200
+ "region": ["US", "US", "EU", "EU"],
201
+ "est": [1.2, 0.4, 2.1, 0.9],
202
+ "lb": [0.8, 0.1, 1.5, 0.5],
203
+ "ub": [1.6, 0.7, 2.7, 1.3],
204
+ }
205
+ )
206
+
207
+ (
208
+ ct.CoefTable(
209
+ regions, rows="metric", groups="region", collapsible_groups=True
175
210
  )
176
- .header("Cohort Revenue by Method")
211
+ .estimate("Effect", "est", ci=("lb", "ub"))
212
+ .forest("Effect Plot", of="Effect", ref=0.0, symmetric=True)
177
213
  )
178
214
  ```
179
215
 
@@ -184,11 +220,11 @@ Four built-in themes are available from `coeftable.theme`:
184
220
  ```python
185
221
  from coeftable.theme import BLUE, COLORBLIND, DEFAULT, MONO, TEXTUAL
186
222
 
187
- DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
188
- TEXTUAL # Minimal, publication-style: muted colours, light chrome
189
- BLUE # The original blue-grey palette
190
- COLORBLIND # Colourblind-safe palette
191
- MONO # Grayscale for mono journals
223
+ DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
224
+ TEXTUAL # Minimal, publication-style: muted colours, light chrome
225
+ BLUE # The original blue-grey palette
226
+ COLORBLIND # Colourblind-safe palette
227
+ MONO # Grayscale for mono journals
192
228
  ```
193
229
 
194
230
  Apply one with `.with_theme(...)`:
@@ -209,9 +245,8 @@ Use `with_direction` to mark rows where lower values are favourable (reusing
209
245
  the `df` frame from [Quick start](#quick-start)):
210
246
 
211
247
  ```python
212
- table = (
213
- ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
214
- .with_direction({"Latency": "lower_is_better"})
248
+ table = ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub")).with_direction(
249
+ {"Latency": "lower_is_better"}
215
250
  )
216
251
  ```
217
252
 
@@ -232,8 +267,13 @@ companion-frame choice used elsewhere in coeftable:
232
267
  arrives in: a SQL export, a dbt model, an experimentation platform's daily
233
268
  metrics table. Pass `data=` a separate frame with one row per point, and
234
269
  `value` / `ci` / `x` name *scalar* columns on it. coeftable groups the
235
- companion frame by the table's `rows` (+ `nest`, + `split_columns`) keys
236
- and collapses each group into a series internally:
270
+ companion frame by the table's `rows` (+ `nest`, + `groups`, +
271
+ `split_columns`) keys and collapses each group into a series internally.
272
+ `groups` participates only when `data` actually carries that column, so a
273
+ companion frame keyed on the row alone stays valid. The one case that needs
274
+ it is a row label appearing under more than one group: without the group
275
+ column in `data` those rows are indistinguishable, so coeftable reports it
276
+ rather than serving every section the same merged series.
237
277
 
238
278
  ```python
239
279
  import datetime as dt
@@ -373,3 +413,29 @@ trend = pl.DataFrame(
373
413
  .sparkline("Override", value="lift", ref=1.0, ylim=(0.9, 1.1))
374
414
  )
375
415
  ```
416
+
417
+ **No reference, or a hidden one.** `ref` also drives the dashed line and
418
+ colour resolution; two ways to opt out, for data with no meaningful
419
+ zero (revenue, durations, absolute counts):
420
+
421
+ ```python
422
+ absolute = pl.DataFrame(
423
+ {
424
+ "metric": ["Revenue", "Latency"],
425
+ "value": [[282.3, 300.1, 320.0], [900.0, 910.0, 920.0]],
426
+ }
427
+ )
428
+
429
+ (
430
+ ct.CoefTable(absolute, rows="metric")
431
+ # ref=None: no reference at all. No dashed line, no forced domain
432
+ # inclusion of 0, and every cell colours neutral -- "favorable" has
433
+ # no meaning without something to compare against.
434
+ .sparkline("No reference", value="value", ref=None)
435
+ # ref=0.0, show_ref=False: the reference is real and still drives
436
+ # colour (favorable/unfavorable against 0), it just isn't drawn or
437
+ # forced into the domain. Opt in deliberately: a mark can then claim
438
+ # "above the reference" while the reference sits off-canvas.
439
+ .sparkline("Hidden reference", value="value", ref=0.0, show_ref=False)
440
+ )
441
+ ```
@@ -33,10 +33,11 @@ ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
33
33
  ```
34
34
 
35
35
  A `CoefTable` renders itself in marimo, Jupyter, and any other `_repr_html_`-aware viewer —
36
- leave it as the last expression in a cell, no extra call needed. Outside a notebook, use
37
- `.gt()` to reach the underlying [great_tables](https://posit-dev.github.io/great-tables/)
38
- object: `table.gt().as_raw_html()` for an HTML string, `table.gt().save("t.png")` for an
39
- image, `table.gt().tab_options(...)` to keep styling with great_tables' own API.
36
+ leave it as the last expression in a cell, no extra call needed. Outside a notebook,
37
+ `table.as_raw_html()` is the HTML string entry point. `.gt()` remains the escape hatch to the
38
+ underlying [great_tables](https://posit-dev.github.io/great-tables/) object itself:
39
+ `table.gt().save("t.png")` for an image, `table.gt().tab_options(...)` to keep styling with
40
+ great_tables' own API.
40
41
 
41
42
  ## Data shape
42
43
 
@@ -49,7 +50,15 @@ a `parameter` column.
49
50
  **Dimensions:**
50
51
  - `rows` — the label for each row in the table (e.g. a metric name).
51
52
  - `nest` — an optional secondary label stacked below each row.
52
- - `groups` — an optional column whose values produce section headers.
53
+ - `groups` — an optional column whose values produce section headers. A row
54
+ label may appear under more than one group, in which case it renders once per
55
+ section — so the same set of metrics can be reported per region without
56
+ inventing a `nest` column to tell them apart. Pass
57
+ `collapsible_groups=True` to make those sections collapsible in the rendered
58
+ HTML — a pure CSS toggle (relies on `:has()`, Baseline since late 2023; on an
59
+ older browser without it the toggle no-ops and sections stay expanded), no
60
+ JavaScript, sections start expanded. Applies to `as_raw_html()`/`_repr_html_`;
61
+ `.gt()` is unaffected.
53
62
  - `split_columns` — an optional column whose values produce repeated column
54
63
  groups side by side, useful for comparing methods.
55
64
 
@@ -118,8 +127,35 @@ methods = pl.DataFrame(
118
127
  (
119
128
  ct.CoefTable(
120
129
  methods, rows="metric", split_columns="method", estimate="est", ci=("lb", "ub")
130
+ ).header("Cohort Revenue by Method")
131
+ )
132
+ ```
133
+
134
+ ## Repeating metrics across sections
135
+
136
+ A row label may appear under more than one `groups` value. The same metrics can
137
+ therefore be reported per region, each section repeating the full set:
138
+
139
+ ```python
140
+ import polars as pl
141
+ import coeftable as ct
142
+
143
+ regions = pl.DataFrame(
144
+ {
145
+ "metric": ["Revenue", "Signups", "Revenue", "Signups"],
146
+ "region": ["US", "US", "EU", "EU"],
147
+ "est": [1.2, 0.4, 2.1, 0.9],
148
+ "lb": [0.8, 0.1, 1.5, 0.5],
149
+ "ub": [1.6, 0.7, 2.7, 1.3],
150
+ }
151
+ )
152
+
153
+ (
154
+ ct.CoefTable(
155
+ regions, rows="metric", groups="region", collapsible_groups=True
121
156
  )
122
- .header("Cohort Revenue by Method")
157
+ .estimate("Effect", "est", ci=("lb", "ub"))
158
+ .forest("Effect Plot", of="Effect", ref=0.0, symmetric=True)
123
159
  )
124
160
  ```
125
161
 
@@ -130,11 +166,11 @@ Four built-in themes are available from `coeftable.theme`:
130
166
  ```python
131
167
  from coeftable.theme import BLUE, COLORBLIND, DEFAULT, MONO, TEXTUAL
132
168
 
133
- DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
134
- TEXTUAL # Minimal, publication-style: muted colours, light chrome
135
- BLUE # The original blue-grey palette
136
- COLORBLIND # Colourblind-safe palette
137
- MONO # Grayscale for mono journals
169
+ DEFAULT # Alias for TEXTUAL -- what CoefTable uses if you don't set a theme
170
+ TEXTUAL # Minimal, publication-style: muted colours, light chrome
171
+ BLUE # The original blue-grey palette
172
+ COLORBLIND # Colourblind-safe palette
173
+ MONO # Grayscale for mono journals
138
174
  ```
139
175
 
140
176
  Apply one with `.with_theme(...)`:
@@ -155,9 +191,8 @@ Use `with_direction` to mark rows where lower values are favourable (reusing
155
191
  the `df` frame from [Quick start](#quick-start)):
156
192
 
157
193
  ```python
158
- table = (
159
- ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub"))
160
- .with_direction({"Latency": "lower_is_better"})
194
+ table = ct.CoefTable(df, rows="metric", estimate="est", ci=("lb", "ub")).with_direction(
195
+ {"Latency": "lower_is_better"}
161
196
  )
162
197
  ```
163
198
 
@@ -178,8 +213,13 @@ companion-frame choice used elsewhere in coeftable:
178
213
  arrives in: a SQL export, a dbt model, an experimentation platform's daily
179
214
  metrics table. Pass `data=` a separate frame with one row per point, and
180
215
  `value` / `ci` / `x` name *scalar* columns on it. coeftable groups the
181
- companion frame by the table's `rows` (+ `nest`, + `split_columns`) keys
182
- and collapses each group into a series internally:
216
+ companion frame by the table's `rows` (+ `nest`, + `groups`, +
217
+ `split_columns`) keys and collapses each group into a series internally.
218
+ `groups` participates only when `data` actually carries that column, so a
219
+ companion frame keyed on the row alone stays valid. The one case that needs
220
+ it is a row label appearing under more than one group: without the group
221
+ column in `data` those rows are indistinguishable, so coeftable reports it
222
+ rather than serving every section the same merged series.
183
223
 
184
224
  ```python
185
225
  import datetime as dt
@@ -319,3 +359,29 @@ trend = pl.DataFrame(
319
359
  .sparkline("Override", value="lift", ref=1.0, ylim=(0.9, 1.1))
320
360
  )
321
361
  ```
362
+
363
+ **No reference, or a hidden one.** `ref` also drives the dashed line and
364
+ colour resolution; two ways to opt out, for data with no meaningful
365
+ zero (revenue, durations, absolute counts):
366
+
367
+ ```python
368
+ absolute = pl.DataFrame(
369
+ {
370
+ "metric": ["Revenue", "Latency"],
371
+ "value": [[282.3, 300.1, 320.0], [900.0, 910.0, 920.0]],
372
+ }
373
+ )
374
+
375
+ (
376
+ ct.CoefTable(absolute, rows="metric")
377
+ # ref=None: no reference at all. No dashed line, no forced domain
378
+ # inclusion of 0, and every cell colours neutral -- "favorable" has
379
+ # no meaning without something to compare against.
380
+ .sparkline("No reference", value="value", ref=None)
381
+ # ref=0.0, show_ref=False: the reference is real and still drives
382
+ # colour (favorable/unfavorable against 0), it just isn't drawn or
383
+ # forced into the domain. Opt in deliberately: a mark can then claim
384
+ # "above the reference" while the reference sits off-canvas.
385
+ .sparkline("Hidden reference", value="value", ref=0.0, show_ref=False)
386
+ )
387
+ ```
@@ -44,8 +44,14 @@ Homepage = "https://github.com/kylejcaron/coeftable"
44
44
  Repository = "https://github.com/kylejcaron/coeftable"
45
45
  Issues = "https://github.com/kylejcaron/coeftable/issues"
46
46
 
47
+ # PyPI rejects core metadata 2.5, which hatchling 1.30 defaults to.
48
+ # Drop once https://github.com/pypi/warehouse/issues/19083 ships.
47
49
  [tool.hatch.build.targets.wheel]
48
50
  packages = ["src/coeftable"]
51
+ core-metadata-version = "2.4"
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ core-metadata-version = "2.4"
49
55
 
50
56
  [tool.hatch.version]
51
57
  source = "vcs"
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.1'
22
- __version_tuple__ = version_tuple = (0, 2, 1)
21
+ __version__ = version = '0.3.1'
22
+ __version_tuple__ = version_tuple = (0, 3, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None