coeftable 0.13.0__tar.gz → 0.14.0__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 (100) hide show
  1. {coeftable-0.13.0 → coeftable-0.14.0}/.github/workflows/ci.yml +12 -0
  2. {coeftable-0.13.0 → coeftable-0.14.0}/CHANGELOG.md +34 -0
  3. coeftable-0.14.0/Makefile +25 -0
  4. {coeftable-0.13.0 → coeftable-0.14.0}/PKG-INFO +135 -1
  5. {coeftable-0.13.0 → coeftable-0.14.0}/README.md +131 -0
  6. {coeftable-0.13.0 → coeftable-0.14.0}/pyproject.toml +3 -0
  7. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/__init__.py +6 -0
  8. coeftable-0.14.0/src/coeftable/_tooltip_runtime.js +419 -0
  9. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/_version.py +2 -2
  10. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/regions.py +39 -2
  11. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/frame.py +5 -9
  12. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/grid.py +14 -11
  13. coeftable-0.14.0/src/coeftable/interactive.py +58 -0
  14. coeftable-0.14.0/src/coeftable/labels.py +218 -0
  15. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/plots.py +4 -0
  16. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/render.py +48 -2
  17. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/series.py +47 -2
  18. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/spec.py +74 -4
  19. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/svg.py +222 -4
  20. coeftable-0.14.0/src/coeftable/tooltips.py +143 -0
  21. coeftable-0.14.0/tests/conftest.py +31 -0
  22. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_cards.py +0 -46
  23. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_frame.py +42 -33
  24. coeftable-0.14.0/tests/test_interactive.py +536 -0
  25. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_plots.py +0 -19
  26. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_regions.py +89 -0
  27. coeftable-0.14.0/tests/test_render.py +840 -0
  28. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_series.py +54 -0
  29. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_sparkline.py +66 -0
  30. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_svg.py +179 -0
  31. coeftable-0.14.0/tests/test_tooltips.py +49 -0
  32. {coeftable-0.13.0 → coeftable-0.14.0}/uv.lock +108 -15
  33. coeftable-0.13.0/Makefile +0 -20
  34. coeftable-0.13.0/tests/test_render.py +0 -356
  35. {coeftable-0.13.0 → coeftable-0.14.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {coeftable-0.13.0 → coeftable-0.14.0}/.github/release.yml +0 -0
  37. {coeftable-0.13.0 → coeftable-0.14.0}/.github/workflows/post-release.yml +0 -0
  38. {coeftable-0.13.0 → coeftable-0.14.0}/.github/workflows/publish.yml +0 -0
  39. {coeftable-0.13.0 → coeftable-0.14.0}/.github/workflows/release.yml +0 -0
  40. {coeftable-0.13.0 → coeftable-0.14.0}/.gitignore +0 -0
  41. {coeftable-0.13.0 → coeftable-0.14.0}/.pre-commit-config.yaml +0 -0
  42. {coeftable-0.13.0 → coeftable-0.14.0}/AGENTS.md +0 -0
  43. {coeftable-0.13.0 → coeftable-0.14.0}/LICENSE +0 -0
  44. {coeftable-0.13.0 → coeftable-0.14.0}/docs/images/example.png +0 -0
  45. {coeftable-0.13.0 → coeftable-0.14.0}/docs/images/forest-robust-autoscale.png +0 -0
  46. {coeftable-0.13.0 → coeftable-0.14.0}/docs/images/trend-example.png +0 -0
  47. {coeftable-0.13.0 → coeftable-0.14.0}/noxfile.py +0 -0
  48. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/_axis.py +0 -0
  49. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/annotations.py +0 -0
  50. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/__init__.py +0 -0
  51. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/adornments.py +0 -0
  52. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/appearance.py +0 -0
  53. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/card.py +0 -0
  54. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/chrome.py +0 -0
  55. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/fragments.py +0 -0
  56. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/measure.py +0 -0
  57. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/panel.py +0 -0
  58. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/cards/template.py +0 -0
  59. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/collapsible.py +0 -0
  60. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/errors.py +0 -0
  61. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/format.py +0 -0
  62. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/__init__.py +0 -0
  63. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/_layered.py +0 -0
  64. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/_routes.py +0 -0
  65. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/_staged.py +0 -0
  66. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/breakout.py +0 -0
  67. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/causal_graph.py +0 -0
  68. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/driver_tree.py +0 -0
  69. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/event_flow.py +0 -0
  70. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/honesty.py +0 -0
  71. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/metric_tree.py +0 -0
  72. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/model.py +0 -0
  73. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/product_flow.py +0 -0
  74. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/render.py +0 -0
  75. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/report.py +0 -0
  76. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/state.py +0 -0
  77. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/timeline.py +0 -0
  78. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/graph/topology.py +0 -0
  79. {coeftable-0.13.0 → coeftable-0.14.0}/src/coeftable/theme.py +0 -0
  80. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_annotations.py +0 -0
  81. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_breakout.py +0 -0
  82. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_card_column.py +0 -0
  83. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_card_entry.py +0 -0
  84. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_causal_graph.py +0 -0
  85. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_collapsible.py +0 -0
  86. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_driver_tree.py +0 -0
  87. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_event_flow.py +0 -0
  88. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_event_flow_routes.py +0 -0
  89. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_format.py +0 -0
  90. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_graph.py +0 -0
  91. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_graph_report.py +0 -0
  92. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_honesty.py +0 -0
  93. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_package.py +0 -0
  94. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_panel.py +0 -0
  95. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_product_flow.py +0 -0
  96. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_product_flow_reference.py +0 -0
  97. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_public_api.py +0 -0
  98. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_spec.py +0 -0
  99. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_theme.py +0 -0
  100. {coeftable-0.13.0 → coeftable-0.14.0}/tests/test_timeline.py +0 -0
@@ -30,3 +30,15 @@ jobs:
30
30
  with:
31
31
  python-version: ${{ matrix.python-version }}
32
32
  - run: uv run --with nox nox -s "tests-${{ matrix.python-version }}"
33
+
34
+ browser:
35
+ runs-on: ubuntu-latest
36
+ needs: prek
37
+ steps:
38
+ - uses: actions/checkout@v6
39
+ - uses: astral-sh/setup-uv@v6
40
+ with:
41
+ python-version: "3.13"
42
+ - run: uv sync --extra dev --extra browser
43
+ - run: uv run python -m playwright install --with-deps chromium
44
+ - run: uv run pytest --no-cov tests/test_interactive.py
@@ -1,3 +1,35 @@
1
+ <a id="v0.13.1"></a>
2
+ # [v0.13.1](https://github.com/kylejcaron/coeftable/releases/tag/v0.13.1) - 2026-09-30
3
+
4
+ <!-- Release notes generated using configuration in .github/release.yml at v0.13.1 -->
5
+
6
+ ## What's Changed
7
+ ### Maintenance
8
+ * Update changelog for v0.13.0 by [@github-actions](https://github.com/github-actions)[bot] in [#63](https://github.com/kylejcaron/coeftable/pull/63)
9
+ * Neutralize unsafe Markdown targets in table labels by [@kylejcaron](https://github.com/kylejcaron) in [#64](https://github.com/kylejcaron/coeftable/pull/64)
10
+
11
+
12
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.13.0...v0.13.1
13
+
14
+ [Changes][v0.13.1]
15
+
16
+
17
+ <a id="v0.13.0"></a>
18
+ # [v0.13.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.13.0) - 2026-09-28
19
+
20
+ <!-- Release notes generated using configuration in .github/release.yml at v0.13.0 -->
21
+
22
+ ## What's Changed
23
+ ### Maintenance
24
+ * Update changelog for v0.12.1 by [@github-actions](https://github.com/github-actions)[bot] in [#61](https://github.com/kylejcaron/coeftable/pull/61)
25
+ * Add visible y-axes to sparkline surfaces by [@kylejcaron](https://github.com/kylejcaron) in [#62](https://github.com/kylejcaron/coeftable/pull/62)
26
+
27
+
28
+ **Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.12.1...v0.13.0
29
+
30
+ [Changes][v0.13.0]
31
+
32
+
1
33
  <a id="v0.12.1"></a>
2
34
  # [v0.12.1](https://github.com/kylejcaron/coeftable/releases/tag/v0.12.1) - 2026-09-24
3
35
 
@@ -266,6 +298,8 @@
266
298
  [Changes][v0.1.0]
267
299
 
268
300
 
301
+ [v0.13.1]: https://github.com/kylejcaron/coeftable/compare/v0.13.0...v0.13.1
302
+ [v0.13.0]: https://github.com/kylejcaron/coeftable/compare/v0.12.1...v0.13.0
269
303
  [v0.12.1]: https://github.com/kylejcaron/coeftable/compare/v0.12.0...v0.12.1
270
304
  [v0.12.0]: https://github.com/kylejcaron/coeftable/compare/v0.11.0...v0.12.0
271
305
  [v0.11.0]: https://github.com/kylejcaron/coeftable/compare/v0.10.0...v0.11.0
@@ -0,0 +1,25 @@
1
+ .PHONY: setup tests tests-all tests-browser lint typecheck prek
2
+
3
+ setup:
4
+ uv sync --extra dev
5
+ uv run prek install
6
+
7
+ tests:
8
+ uv run pytest
9
+
10
+ tests-all:
11
+ uv run nox -s tests
12
+
13
+ tests-browser:
14
+ uv sync --extra dev --extra browser
15
+ uv run python -m playwright install chromium
16
+ uv run pytest --no-cov tests/test_interactive.py
17
+
18
+ lint:
19
+ uv run nox -s lint
20
+
21
+ typecheck:
22
+ uv run nox -s typecheck
23
+
24
+ prek:
25
+ SKIP=no-commit-to-branch uv run prek run --all-files
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coeftable
3
- Version: 0.13.0
3
+ Version: 0.14.0
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
@@ -39,7 +39,10 @@ Classifier: Programming Language :: Python :: 3.14
39
39
  Classifier: Topic :: Scientific/Engineering
40
40
  Requires-Python: >=3.12
41
41
  Requires-Dist: great-tables>=0.22
42
+ Requires-Dist: multimark>=0.1.3
42
43
  Requires-Dist: narwhals>=2.24
44
+ Provides-Extra: browser
45
+ Requires-Dist: playwright>=1.50; extra == 'browser'
43
46
  Provides-Extra: dev
44
47
  Requires-Dist: nox>=2025.5; extra == 'dev'
45
48
  Requires-Dist: pandas>=2.2; extra == 'dev'
@@ -116,6 +119,16 @@ a `parameter` column.
116
119
  - `split_columns` — an optional column whose values produce repeated column
117
120
  groups side by side, useful for comparing methods.
118
121
 
122
+ Row and nested labels support safe Markdown, including `{{m s^-1}}` units
123
+ notation. Raw HTML in labels is displayed as text; dangerous link targets are
124
+ neutralized. Group and split-column labels are literal text. These rules apply
125
+ to standalone `as_raw_html()` and the table's initial `.gt()` HTML rendering;
126
+ subsequent formatting added through `.gt()` follows Great Tables' own rules.
127
+ LaTeX export also escapes group and split-column labels, including numeric and
128
+ date-valued groups.
129
+ Native `.gt()` spanner styles and footnotes select split headings using their
130
+ original string IDs.
131
+
119
132
  **Series columns bend this rule.** A point estimate is one number (plus
120
133
  bounds), so a triple of scalar columns holds it. A `.sparkline(...)` series
121
134
  is N points, not one -- most naturally via the companion-frame door, a
@@ -541,6 +554,118 @@ shaded intervals via each function's `annotations=` parameter. Because a
541
554
  standalone plot and a table column share one `Theme`, a report that mixes
542
555
  both stays visually consistent.
543
556
 
557
+ ## Inspect plot values
558
+
559
+ Pass `tooltip=ct.Tooltip()` to opt a plot into value inspection, then activate
560
+ the composed HTML with `ct.interactive_html`. Existing renderers remain passive:
561
+ `as_raw_html()`, `_repr_html_()`, `.gt()`, and standalone SVG strings do not
562
+ execute scripts themselves.
563
+
564
+ ```python
565
+ from pathlib import Path
566
+
567
+ import coeftable as ct
568
+ import polars as pl
569
+ from coeftable.cards import Card, Trend
570
+ from coeftable.plots import sparkline_bar
571
+
572
+ history = pl.DataFrame({
573
+ "metric": ["Revenue", "Revenue", "Revenue"],
574
+ "day": [0, 2, 4],
575
+ "estimate": [1.0, None, 5.0],
576
+ "n": [10, 20, 30],
577
+ })
578
+ table = ct.CoefTable(pl.DataFrame({"metric": ["Revenue"]}), rows="metric").sparkline(
579
+ "Trend", value="estimate", x="day", data=history, ylim=(0, 3),
580
+ tooltip=ct.Tooltip(
581
+ fields=[ct.TooltipField("n", label="Sample size")],
582
+ value_label="Revenue", x_label="Day",
583
+ ),
584
+ )
585
+
586
+ # Direct plots and card regions receive aligned arrays, not dataframe fields.
587
+ extras = [ct.TooltipValues("Sample size", [10, 20, 30])]
588
+ line = sparkline_bar(
589
+ [0, 2, 4], [1, None, 5], [None] * 3, [None] * 3,
590
+ x_domain=(0, 4), domain=(0, 3), ref=None, color="#4C72B0", fmt=ct.Number(),
591
+ tooltip=ct.Tooltip(value_label="Revenue", x_label="Day"), tooltip_values=extras,
592
+ )
593
+ card = Card("Revenue", content=[
594
+ Trend(
595
+ x=[0, 2, 4], y=[1, None, 5], x_domain=(0, 4), domain=(0, 3),
596
+ tooltip=ct.Tooltip(value_label="Revenue", x_label="Day"), tooltip_values=extras,
597
+ ),
598
+ ])
599
+ fragment = table.as_raw_html() + line + card.as_raw_html()
600
+ document = ct.interactive_html(fragment, full_document=True)
601
+ Path("inspection.html").write_text(document, encoding="utf-8")
602
+ ```
603
+
604
+ Primary values are bold; the x coordinate is context, not the headline.
605
+ `Tooltip(value_label="Sales", x_label="Date")` names both values explicitly.
606
+ Labels default to `Value` and `x`; forest inspection uses only `value_label`.
607
+
608
+ `TooltipField` binds **main-frame scalar columns for forest plots**, main-frame
609
+ list columns for list-mode sparklines, and **companion-frame scalar columns**
610
+ when `data=` is supplied. Companion sorting applies the same permutation to
611
+ the extra measures. List-mode drawing retains its input order. Extras never
612
+ affect plot domains or make an otherwise blank table cell drawable.
613
+
614
+ `forest_bar` and card `Interval` accept `tooltip_values` of length one per
615
+ field; `sparkline_bar` and `Trend` require each field to match the point arrays.
616
+ For `sparkline_multi`, `tooltip_values` is an outer sequence aligned to the
617
+ input traces: include an empty entry for any trace without extras. An empty
618
+ outer sequence means no extras on any trace. `Trace` remains unchanged.
619
+ Direct plots/regions reject unresolved `Tooltip.fields`; extra values without
620
+ an enabled tooltip raise `SpecError`.
621
+
622
+ **Selection and formatting.** Move anywhere horizontally inside the plot's
623
+ data area, or drag, to select the nearest observed x. Midpoint ties choose
624
+ the smaller x. Missing y displays “Missing”; an arm without that exact x
625
+ displays “No observation”. Duplicate observations at one x remain separate.
626
+ No interpolation or substitution from another date. Off-scale values keep
627
+ their actual values and an above/below (forest: left/right) indication.
628
+ Forest plots expose their one interval anywhere in the data area.
629
+
630
+ `Tooltip(fmt=..., x_fmt=...)` overrides observation value/interval and x
631
+ formatters independently. Otherwise the primary plot/region formatter is used
632
+ for values (`Number()` for direct `forest_bar`). Numeric x defaults to
633
+ `Number()`. Temporal table coordinates and `Trend(temporal=True)` use full
634
+ `YYYY-MM-DD HH:MM:SS` coordinate dates, not contextual axis ticks such as “14”.
635
+ These dates follow the existing epoch-coordinate convention formatted in UTC;
636
+ this does not reinterpret the source timezone. Direct numeric emitters need
637
+ an explicit `x_fmt` for date coordinates. Each extra field has its own `fmt`;
638
+ null/NaN extras display “—” without invoking it. Formatters must return strings.
639
+
640
+ **Input.** Focus an enabled plot, then use Left/Right, Home/End, or Escape.
641
+ There is one tab stop per plot, not per point. Touch/pen release leaves the
642
+ inspector available to read until dismissal or another selection; cancellation
643
+ clears it and vertical page scrolling remains available. Long inspectors wrap
644
+ and scroll within the viewport: PageUp/PageDown work while the plot is focused;
645
+ Tab can enter the scroll region. Escape or an outside tap dismisses.
646
+
647
+ | Delivery path | Behavior |
648
+ |---|---|
649
+ | Saved full document | Interactive offline when scripts are permitted; no kernel/server/CDN needed. |
650
+ | Parser-inserted fragment | `interactive_html(fragment)` activates within its own wrapper. |
651
+ | Dynamic `innerHTML` insertion | Scripts are inert. Load the packaged `coeftable/_tooltip_runtime.js` yourself and call `window.coeftableTooltips.mount(root)`; the returned function disposes the mount. Repeated mounts of the same root are idempotent. |
652
+ | Live marimo | `mo.iframe(ct.interactive_html(fragment, full_document=True), height="500px")`; `mo.Html` is a passive path. |
653
+ | SVG as an image, PNG/PDF, scripts blocked/disabled | Existing static appearance, without inspection. |
654
+
655
+ Compose table, standalone and card HTML first, then activate once.
656
+ `interactive_html` accepts **trusted fragments only**, not complete
657
+ `html`/`head`/`body` documents, and is not an HTML sanitizer. If using
658
+ `table.gt().as_raw_html(...)`, request a fragment rather than a document.
659
+ Restrictive CSP policies may block the inline bootstrap/styles; asset/nonces
660
+ integration, automatic notebook activation, Python callbacks and untested
661
+ notebook/export hosts are not promised.
662
+
663
+ **Data disclosure:** all configured observation values and extra fields are
664
+ embedded in browser-readable output, including missing/off-scale observations.
665
+ Plain-text escaping prevents formatter output from becoming markup; it does
666
+ not make sensitive data private. Include only fields intended for recipients.
667
+
668
+
544
669
  ## Metric cards
545
670
 
546
671
  `coeftable.cards` composes the standalone plots into measured, foldable
@@ -1121,3 +1246,12 @@ the Sparkline y-domain and clip or omit distant marks. Annotations supplement
1121
1246
  rather than replace
1122
1247
  `ref`: `ref` remains the built-in semantic reference that controls colors and
1123
1248
  its optional dashed line.
1249
+
1250
+ ## Development checks
1251
+
1252
+ `make setup` installs development dependencies without enabling optional browser
1253
+ tests. `make tests` runs the installed test selection. For plot interaction
1254
+ checks, `make tests-browser` installs the optional Playwright package and
1255
+ Chromium, then runs `tests/test_interactive.py`. Without the browser extra,
1256
+ only browser-dependent cases skip; HTML assembly checks still run. An installed
1257
+ Playwright package without Chromium is an error, not a passing skip.
@@ -62,6 +62,16 @@ a `parameter` column.
62
62
  - `split_columns` — an optional column whose values produce repeated column
63
63
  groups side by side, useful for comparing methods.
64
64
 
65
+ Row and nested labels support safe Markdown, including `{{m s^-1}}` units
66
+ notation. Raw HTML in labels is displayed as text; dangerous link targets are
67
+ neutralized. Group and split-column labels are literal text. These rules apply
68
+ to standalone `as_raw_html()` and the table's initial `.gt()` HTML rendering;
69
+ subsequent formatting added through `.gt()` follows Great Tables' own rules.
70
+ LaTeX export also escapes group and split-column labels, including numeric and
71
+ date-valued groups.
72
+ Native `.gt()` spanner styles and footnotes select split headings using their
73
+ original string IDs.
74
+
65
75
  **Series columns bend this rule.** A point estimate is one number (plus
66
76
  bounds), so a triple of scalar columns holds it. A `.sparkline(...)` series
67
77
  is N points, not one -- most naturally via the companion-frame door, a
@@ -487,6 +497,118 @@ shaded intervals via each function's `annotations=` parameter. Because a
487
497
  standalone plot and a table column share one `Theme`, a report that mixes
488
498
  both stays visually consistent.
489
499
 
500
+ ## Inspect plot values
501
+
502
+ Pass `tooltip=ct.Tooltip()` to opt a plot into value inspection, then activate
503
+ the composed HTML with `ct.interactive_html`. Existing renderers remain passive:
504
+ `as_raw_html()`, `_repr_html_()`, `.gt()`, and standalone SVG strings do not
505
+ execute scripts themselves.
506
+
507
+ ```python
508
+ from pathlib import Path
509
+
510
+ import coeftable as ct
511
+ import polars as pl
512
+ from coeftable.cards import Card, Trend
513
+ from coeftable.plots import sparkline_bar
514
+
515
+ history = pl.DataFrame({
516
+ "metric": ["Revenue", "Revenue", "Revenue"],
517
+ "day": [0, 2, 4],
518
+ "estimate": [1.0, None, 5.0],
519
+ "n": [10, 20, 30],
520
+ })
521
+ table = ct.CoefTable(pl.DataFrame({"metric": ["Revenue"]}), rows="metric").sparkline(
522
+ "Trend", value="estimate", x="day", data=history, ylim=(0, 3),
523
+ tooltip=ct.Tooltip(
524
+ fields=[ct.TooltipField("n", label="Sample size")],
525
+ value_label="Revenue", x_label="Day",
526
+ ),
527
+ )
528
+
529
+ # Direct plots and card regions receive aligned arrays, not dataframe fields.
530
+ extras = [ct.TooltipValues("Sample size", [10, 20, 30])]
531
+ line = sparkline_bar(
532
+ [0, 2, 4], [1, None, 5], [None] * 3, [None] * 3,
533
+ x_domain=(0, 4), domain=(0, 3), ref=None, color="#4C72B0", fmt=ct.Number(),
534
+ tooltip=ct.Tooltip(value_label="Revenue", x_label="Day"), tooltip_values=extras,
535
+ )
536
+ card = Card("Revenue", content=[
537
+ Trend(
538
+ x=[0, 2, 4], y=[1, None, 5], x_domain=(0, 4), domain=(0, 3),
539
+ tooltip=ct.Tooltip(value_label="Revenue", x_label="Day"), tooltip_values=extras,
540
+ ),
541
+ ])
542
+ fragment = table.as_raw_html() + line + card.as_raw_html()
543
+ document = ct.interactive_html(fragment, full_document=True)
544
+ Path("inspection.html").write_text(document, encoding="utf-8")
545
+ ```
546
+
547
+ Primary values are bold; the x coordinate is context, not the headline.
548
+ `Tooltip(value_label="Sales", x_label="Date")` names both values explicitly.
549
+ Labels default to `Value` and `x`; forest inspection uses only `value_label`.
550
+
551
+ `TooltipField` binds **main-frame scalar columns for forest plots**, main-frame
552
+ list columns for list-mode sparklines, and **companion-frame scalar columns**
553
+ when `data=` is supplied. Companion sorting applies the same permutation to
554
+ the extra measures. List-mode drawing retains its input order. Extras never
555
+ affect plot domains or make an otherwise blank table cell drawable.
556
+
557
+ `forest_bar` and card `Interval` accept `tooltip_values` of length one per
558
+ field; `sparkline_bar` and `Trend` require each field to match the point arrays.
559
+ For `sparkline_multi`, `tooltip_values` is an outer sequence aligned to the
560
+ input traces: include an empty entry for any trace without extras. An empty
561
+ outer sequence means no extras on any trace. `Trace` remains unchanged.
562
+ Direct plots/regions reject unresolved `Tooltip.fields`; extra values without
563
+ an enabled tooltip raise `SpecError`.
564
+
565
+ **Selection and formatting.** Move anywhere horizontally inside the plot's
566
+ data area, or drag, to select the nearest observed x. Midpoint ties choose
567
+ the smaller x. Missing y displays “Missing”; an arm without that exact x
568
+ displays “No observation”. Duplicate observations at one x remain separate.
569
+ No interpolation or substitution from another date. Off-scale values keep
570
+ their actual values and an above/below (forest: left/right) indication.
571
+ Forest plots expose their one interval anywhere in the data area.
572
+
573
+ `Tooltip(fmt=..., x_fmt=...)` overrides observation value/interval and x
574
+ formatters independently. Otherwise the primary plot/region formatter is used
575
+ for values (`Number()` for direct `forest_bar`). Numeric x defaults to
576
+ `Number()`. Temporal table coordinates and `Trend(temporal=True)` use full
577
+ `YYYY-MM-DD HH:MM:SS` coordinate dates, not contextual axis ticks such as “14”.
578
+ These dates follow the existing epoch-coordinate convention formatted in UTC;
579
+ this does not reinterpret the source timezone. Direct numeric emitters need
580
+ an explicit `x_fmt` for date coordinates. Each extra field has its own `fmt`;
581
+ null/NaN extras display “—” without invoking it. Formatters must return strings.
582
+
583
+ **Input.** Focus an enabled plot, then use Left/Right, Home/End, or Escape.
584
+ There is one tab stop per plot, not per point. Touch/pen release leaves the
585
+ inspector available to read until dismissal or another selection; cancellation
586
+ clears it and vertical page scrolling remains available. Long inspectors wrap
587
+ and scroll within the viewport: PageUp/PageDown work while the plot is focused;
588
+ Tab can enter the scroll region. Escape or an outside tap dismisses.
589
+
590
+ | Delivery path | Behavior |
591
+ |---|---|
592
+ | Saved full document | Interactive offline when scripts are permitted; no kernel/server/CDN needed. |
593
+ | Parser-inserted fragment | `interactive_html(fragment)` activates within its own wrapper. |
594
+ | Dynamic `innerHTML` insertion | Scripts are inert. Load the packaged `coeftable/_tooltip_runtime.js` yourself and call `window.coeftableTooltips.mount(root)`; the returned function disposes the mount. Repeated mounts of the same root are idempotent. |
595
+ | Live marimo | `mo.iframe(ct.interactive_html(fragment, full_document=True), height="500px")`; `mo.Html` is a passive path. |
596
+ | SVG as an image, PNG/PDF, scripts blocked/disabled | Existing static appearance, without inspection. |
597
+
598
+ Compose table, standalone and card HTML first, then activate once.
599
+ `interactive_html` accepts **trusted fragments only**, not complete
600
+ `html`/`head`/`body` documents, and is not an HTML sanitizer. If using
601
+ `table.gt().as_raw_html(...)`, request a fragment rather than a document.
602
+ Restrictive CSP policies may block the inline bootstrap/styles; asset/nonces
603
+ integration, automatic notebook activation, Python callbacks and untested
604
+ notebook/export hosts are not promised.
605
+
606
+ **Data disclosure:** all configured observation values and extra fields are
607
+ embedded in browser-readable output, including missing/off-scale observations.
608
+ Plain-text escaping prevents formatter output from becoming markup; it does
609
+ not make sensitive data private. Include only fields intended for recipients.
610
+
611
+
490
612
  ## Metric cards
491
613
 
492
614
  `coeftable.cards` composes the standalone plots into measured, foldable
@@ -1067,3 +1189,12 @@ the Sparkline y-domain and clip or omit distant marks. Annotations supplement
1067
1189
  rather than replace
1068
1190
  `ref`: `ref` remains the built-in semantic reference that controls colors and
1069
1191
  its optional dashed line.
1192
+
1193
+ ## Development checks
1194
+
1195
+ `make setup` installs development dependencies without enabling optional browser
1196
+ tests. `make tests` runs the installed test selection. For plot interaction
1197
+ checks, `make tests-browser` installs the optional Playwright package and
1198
+ Chromium, then runs `tests/test_interactive.py`. Without the browser extra,
1199
+ only browser-dependent cases skip; HTML assembly checks still run. An installed
1200
+ Playwright package without Chromium is an error, not a passing skip.
@@ -23,6 +23,7 @@ classifiers = [
23
23
  ]
24
24
  dependencies = [
25
25
  "great-tables>=0.22",
26
+ "multimark>=0.1.3",
26
27
  "narwhals>=2.24",
27
28
  ]
28
29
 
@@ -38,6 +39,7 @@ dev = [
38
39
  "nox>=2025.5",
39
40
  "pyarrow>=25.0.0",
40
41
  ]
42
+ browser = ["playwright>=1.50"]
41
43
 
42
44
  [project.urls]
43
45
  Homepage = "https://github.com/kylejcaron/coeftable"
@@ -95,6 +97,7 @@ convention = "numpy"
95
97
 
96
98
  [tool.pytest.ini_options]
97
99
  testpaths = ["tests"]
100
+ markers = ["integration: exercises rendering, subprocess, filesystem, or browser boundaries"]
98
101
  addopts = [
99
102
  "-v",
100
103
  "--strict-markers",
@@ -13,8 +13,10 @@ from coeftable.format import (
13
13
  Percent,
14
14
  TimeFormat,
15
15
  )
16
+ from coeftable.interactive import interactive_html
16
17
  from coeftable.spec import CardColumn, CoefTable, Estimate, Forest, Passthrough, Sparkline
17
18
  from coeftable.theme import Theme, role_for
19
+ from coeftable.tooltips import Tooltip, TooltipField, TooltipValues
18
20
 
19
21
  try:
20
22
  __version__ = version("coeftable")
@@ -40,7 +42,11 @@ __all__ = [
40
42
  "SpecError",
41
43
  "Theme",
42
44
  "TimeFormat",
45
+ "Tooltip",
46
+ "TooltipField",
47
+ "TooltipValues",
43
48
  "__version__",
49
+ "interactive_html",
44
50
  "role_for",
45
51
  ]
46
52