coeftable 0.5.0__tar.gz → 0.7.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.
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/workflows/ci.yml +0 -1
- {coeftable-0.5.0 → coeftable-0.7.0}/.gitignore +1 -0
- coeftable-0.7.0/AGENTS.md +8 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/CHANGELOG.md +41 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/PKG-INFO +291 -1
- {coeftable-0.5.0 → coeftable-0.7.0}/README.md +290 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/pyproject.toml +14 -2
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/_version.py +2 -2
- coeftable-0.7.0/src/coeftable/cards/__init__.py +69 -0
- coeftable-0.7.0/src/coeftable/cards/adornments.py +272 -0
- coeftable-0.7.0/src/coeftable/cards/card.py +127 -0
- coeftable-0.7.0/src/coeftable/cards/chrome.py +85 -0
- coeftable-0.7.0/src/coeftable/cards/fragments.py +248 -0
- coeftable-0.7.0/src/coeftable/cards/measure.py +378 -0
- coeftable-0.7.0/src/coeftable/cards/panel.py +404 -0
- coeftable-0.7.0/src/coeftable/cards/regions.py +600 -0
- coeftable-0.7.0/src/coeftable/cards/template.py +110 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/frame.py +1 -1
- coeftable-0.7.0/src/coeftable/graph/__init__.py +34 -0
- coeftable-0.7.0/src/coeftable/graph/breakout.py +539 -0
- coeftable-0.7.0/src/coeftable/graph/driver_tree.py +1161 -0
- coeftable-0.7.0/src/coeftable/graph/honesty.py +353 -0
- coeftable-0.7.0/src/coeftable/graph/metric_tree.py +250 -0
- coeftable-0.7.0/src/coeftable/graph/model.py +969 -0
- coeftable-0.7.0/src/coeftable/graph/render.py +144 -0
- coeftable-0.7.0/src/coeftable/graph/report.py +192 -0
- coeftable-0.7.0/src/coeftable/graph/state.py +215 -0
- coeftable-0.7.0/src/coeftable/graph/timeline.py +384 -0
- coeftable-0.7.0/src/coeftable/graph/topology.py +183 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/grid.py +1 -1
- coeftable-0.7.0/src/coeftable/plots.py +36 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/svg.py +37 -26
- coeftable-0.7.0/tests/test_breakout.py +1511 -0
- coeftable-0.7.0/tests/test_card_entry.py +279 -0
- coeftable-0.7.0/tests/test_cards.py +1415 -0
- coeftable-0.7.0/tests/test_driver_tree.py +1606 -0
- coeftable-0.7.0/tests/test_graph.py +2907 -0
- coeftable-0.7.0/tests/test_graph_report.py +90 -0
- coeftable-0.7.0/tests/test_honesty.py +451 -0
- coeftable-0.7.0/tests/test_panel.py +688 -0
- coeftable-0.7.0/tests/test_plots.py +148 -0
- coeftable-0.7.0/tests/test_regions.py +871 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_svg.py +158 -0
- coeftable-0.7.0/tests/test_timeline.py +468 -0
- coeftable-0.5.0/AGENTS.md +0 -7
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/release.yml +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/workflows/post-release.yml +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/workflows/publish.yml +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/.github/workflows/release.yml +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/.pre-commit-config.yaml +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/LICENSE +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/Makefile +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/docs/images/example.png +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/docs/images/forest-robust-autoscale.png +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/docs/images/trend-example.png +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/noxfile.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/__init__.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/_axis.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/annotations.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/collapsible.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/errors.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/format.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/render.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/series.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/spec.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/src/coeftable/theme.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_annotations.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_collapsible.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_format.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_frame.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_package.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_public_api.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_render.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_series.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_sparkline.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_spec.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/tests/test_theme.py +0 -0
- {coeftable-0.5.0 → coeftable-0.7.0}/uv.lock +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Agent guidelines
|
|
2
|
+
|
|
3
|
+
## Changelog
|
|
4
|
+
|
|
5
|
+
Never edit `CHANGELOG.md`. It is generated by release automation:
|
|
6
|
+
`.github/workflows/post-release.yml` runs the `changelog-from-release` action
|
|
7
|
+
against it, from release notes built out of merged PR titles and categorized by
|
|
8
|
+
`.github/release.yml`. Describe changes in the PR description instead.
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
<a id="v0.6.0"></a>
|
|
2
|
+
# [v0.6.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.6.0) - 2026-08-27
|
|
3
|
+
|
|
4
|
+
<!-- Release notes generated using configuration in .github/release.yml at v0.6.0 -->
|
|
5
|
+
|
|
6
|
+
## What's Changed
|
|
7
|
+
### New Features
|
|
8
|
+
* Add coeftable.plots public facade for SVG plot primitives by [@kylejcaron](https://github.com/kylejcaron) in [#35](https://github.com/kylejcaron/coeftable/pull/35)
|
|
9
|
+
* Add typed card adornment vocabulary and HTML rendering by [@kylejcaron](https://github.com/kylejcaron) in [#36](https://github.com/kylejcaron/coeftable/pull/36)
|
|
10
|
+
* Add exact card measurement engine (chrome geometry) by [@kylejcaron](https://github.com/kylejcaron) in [#37](https://github.com/kylejcaron/coeftable/pull/37)
|
|
11
|
+
* Add trend, interval, metric, diagnostics and event card regions by [@kylejcaron](https://github.com/kylejcaron) in [#38](https://github.com/kylejcaron/coeftable/pull/38)
|
|
12
|
+
* Add public Card and CardGrid entry points by [@kylejcaron](https://github.com/kylejcaron) in [#39](https://github.com/kylejcaron/coeftable/pull/39)
|
|
13
|
+
* Add the panel composition layer by [@kylejcaron](https://github.com/kylejcaron) in [#40](https://github.com/kylejcaron/coeftable/pull/40)
|
|
14
|
+
* Add the metric tree / driver-tree graph builder by [@kylejcaron](https://github.com/kylejcaron) in [#41](https://github.com/kylejcaron/coeftable/pull/41)
|
|
15
|
+
### Maintenance
|
|
16
|
+
* Update changelog for v0.5.0 by [@github-actions](https://github.com/github-actions)[bot] in [#34](https://github.com/kylejcaron/coeftable/pull/34)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.5.0...v0.6.0
|
|
20
|
+
|
|
21
|
+
[Changes][v0.6.0]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
<a id="v0.5.0"></a>
|
|
25
|
+
# [v0.5.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.5.0) - 2026-08-19
|
|
26
|
+
|
|
27
|
+
<!-- Release notes generated using configuration in .github/release.yml at v0.5.0 -->
|
|
28
|
+
|
|
29
|
+
## What's Changed
|
|
30
|
+
### Maintenance
|
|
31
|
+
* Update changelog for v0.4.0 by [@github-actions](https://github.com/github-actions)[bot] in [#32](https://github.com/kylejcaron/coeftable/pull/32)
|
|
32
|
+
* feat: forest plot outlier handling. robust autoscale + margin-fade clipping by [@kylejcaron](https://github.com/kylejcaron) in [#33](https://github.com/kylejcaron/coeftable/pull/33)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.4.0...v0.5.0
|
|
36
|
+
|
|
37
|
+
[Changes][v0.5.0]
|
|
38
|
+
|
|
39
|
+
|
|
1
40
|
<a id="v0.4.0"></a>
|
|
2
41
|
# [v0.4.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.4.0) - 2026-08-14
|
|
3
42
|
|
|
@@ -108,6 +147,8 @@
|
|
|
108
147
|
[Changes][v0.1.0]
|
|
109
148
|
|
|
110
149
|
|
|
150
|
+
[v0.6.0]: https://github.com/kylejcaron/coeftable/compare/v0.5.0...v0.6.0
|
|
151
|
+
[v0.5.0]: https://github.com/kylejcaron/coeftable/compare/v0.4.0...v0.5.0
|
|
111
152
|
[v0.4.0]: https://github.com/kylejcaron/coeftable/compare/v0.3.1...v0.4.0
|
|
112
153
|
[v0.3.1]: https://github.com/kylejcaron/coeftable/compare/v0.3.0...v0.3.1
|
|
113
154
|
[v0.3.0]: https://github.com/kylejcaron/coeftable/compare/v0.2.1...v0.3.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coeftable
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.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
|
|
@@ -470,6 +470,296 @@ absolute = pl.DataFrame(
|
|
|
470
470
|
)
|
|
471
471
|
```
|
|
472
472
|
|
|
473
|
+
## Standalone plots
|
|
474
|
+
|
|
475
|
+
The inline SVG plots that power `.forest(...)` and `.sparkline(...)` columns
|
|
476
|
+
are available directly from `coeftable.plots` — the same emitters, the same
|
|
477
|
+
themes, no table required. Each function returns a complete `<svg>` string
|
|
478
|
+
for embedding anywhere HTML goes:
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
from coeftable.plots import forest_bar, sparkline_bar
|
|
482
|
+
from coeftable.theme import DEFAULT
|
|
483
|
+
import coeftable as ct
|
|
484
|
+
|
|
485
|
+
bar = forest_bar(
|
|
486
|
+
1.2, 0.4, 2.0, # estimate, lower, upper
|
|
487
|
+
domain=(-1.0, 3.0),
|
|
488
|
+
ref=0.0,
|
|
489
|
+
color=DEFAULT.favorable,
|
|
490
|
+
theme=DEFAULT,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
line = sparkline_bar(
|
|
494
|
+
[0.0, 1.0, 2.0], # x
|
|
495
|
+
[1.0, 1.5, 2.0], # y
|
|
496
|
+
[0.5, 1.0, 1.6], # lower
|
|
497
|
+
[1.5, 2.0, 2.4], # upper
|
|
498
|
+
x_domain=(0.0, 2.0),
|
|
499
|
+
domain=(0.0, 3.0),
|
|
500
|
+
ref=0.0,
|
|
501
|
+
color=DEFAULT.favorable,
|
|
502
|
+
fmt=ct.Number(),
|
|
503
|
+
theme=DEFAULT,
|
|
504
|
+
)
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Also exported: `sparkline_multi` + `Trace` for overlaid series,
|
|
508
|
+
`forest_axis` / `sparkline_axis` for the shared axis footer under a column
|
|
509
|
+
of plots, and `ResolvedRule` / `ResolvedBand` for reference lines and
|
|
510
|
+
shaded intervals via each function's `annotations=` parameter. Because a
|
|
511
|
+
standalone plot and a table column share one `Theme`, a report that mixes
|
|
512
|
+
both stays visually consistent.
|
|
513
|
+
|
|
514
|
+
## Metric cards (experimental)
|
|
515
|
+
|
|
516
|
+
`coeftable.cards` composes the standalone plots into measured, foldable
|
|
517
|
+
metric cards — the same formatters, themes, and SVG primitives as the
|
|
518
|
+
tables. The API is experimental and may change; it is deliberately not
|
|
519
|
+
exported from the top-level `coeftable` namespace yet.
|
|
520
|
+
|
|
521
|
+
```python
|
|
522
|
+
import coeftable as ct
|
|
523
|
+
from coeftable.cards import Card, CardGrid
|
|
524
|
+
from coeftable.cards.regions import Diagnostics, Event, Events, Metric, Trend
|
|
525
|
+
|
|
526
|
+
events = Events([Event("launch", "#4C72B0", at=2.0)])
|
|
527
|
+
revenue = Card(
|
|
528
|
+
"Revenue",
|
|
529
|
+
subtitle="weekly lift vs control",
|
|
530
|
+
content=[
|
|
531
|
+
Metric(3.4, ct.Percent(signed=True), ci=(1.2, 5.7), ref=0.0),
|
|
532
|
+
Trend(
|
|
533
|
+
x=(0, 1, 2, 3), y=(0.3, 0.8, 1.1, 1.5),
|
|
534
|
+
lower=(-0.1, 0.3, 0.6, 0.9), upper=(0.7, 1.3, 1.6, 2.1),
|
|
535
|
+
x_domain=(0, 3), domain=(-0.5, 2.5), ref=0.0,
|
|
536
|
+
annotations=events.rules(),
|
|
537
|
+
),
|
|
538
|
+
Diagnostics("diagnostics", [("n", 412), ("sigma", 0.8)]),
|
|
539
|
+
events,
|
|
540
|
+
],
|
|
541
|
+
)
|
|
542
|
+
grid = CardGrid([revenue, revenue.with_theme(ct.theme.BLUE)])
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
A `Card` (or `CardGrid`) left as the last notebook expression renders
|
|
546
|
+
itself; `as_raw_html()` is the string entry point. Cards are fixed-width
|
|
547
|
+
with exact measured heights and fold via a native `<details>` header (the
|
|
548
|
+
headline value stays visible as a chip when it fits beside the header,
|
|
549
|
+
and is omitted otherwise). A standalone card collapses to
|
|
550
|
+
its header; inside a `CardGrid` each card's expanded footprint stays
|
|
551
|
+
reserved, so folding never reflows neighbors.
|
|
552
|
+
|
|
553
|
+
## Metric panels (experimental)
|
|
554
|
+
|
|
555
|
+
`coeftable.cards` panels put named side-by-side panes in one bordered shell
|
|
556
|
+
with a shared header and footer: the compositional step above cards. The API
|
|
557
|
+
is experimental and may change; it is deliberately not exported from the
|
|
558
|
+
top-level `coeftable` namespace yet.
|
|
559
|
+
|
|
560
|
+
```python
|
|
561
|
+
import coeftable as ct
|
|
562
|
+
from coeftable.cards import Metric, Pane, Panel, Row, TextBlock
|
|
563
|
+
|
|
564
|
+
summary = Pane(
|
|
565
|
+
"Summary",
|
|
566
|
+
content=(
|
|
567
|
+
TextBlock("Weekly revenue", variant="subtitle"),
|
|
568
|
+
Metric(3.4, ct.Percent(signed=True), ci=(1.2, 5.7), ref=0.0),
|
|
569
|
+
),
|
|
570
|
+
width=220,
|
|
571
|
+
)
|
|
572
|
+
segments = Pane(
|
|
573
|
+
"Segments",
|
|
574
|
+
content=(
|
|
575
|
+
Row(
|
|
576
|
+
(
|
|
577
|
+
(TextBlock("Enterprise"), 85),
|
|
578
|
+
(Metric(8.1, ct.Percent(signed=True)), 105),
|
|
579
|
+
)
|
|
580
|
+
),
|
|
581
|
+
),
|
|
582
|
+
width=200,
|
|
583
|
+
)
|
|
584
|
+
header = Row(
|
|
585
|
+
(
|
|
586
|
+
(TextBlock("Revenue", variant="title"), 320),
|
|
587
|
+
(Metric(3.4, ct.Percent(signed=True)), 120),
|
|
588
|
+
),
|
|
589
|
+
gap=16,
|
|
590
|
+
)
|
|
591
|
+
panel = Panel((summary, segments), header=(header,))
|
|
592
|
+
html = panel.as_raw_html()
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
Panes declare usable content widths; cell widths are author inputs; the panel
|
|
596
|
+
derives its own footprint with exact measurement, not responsive guessing.
|
|
597
|
+
|
|
598
|
+
A `Panel` left as the last notebook expression renders itself via
|
|
599
|
+
`_repr_html_`; `with_theme()` returns the same composition under another
|
|
600
|
+
theme.
|
|
601
|
+
|
|
602
|
+
## Metric trees (experimental)
|
|
603
|
+
|
|
604
|
+
`coeftable.graph` turns metric cards wired into a driver DAG into a metric
|
|
605
|
+
tree: the graph layer adds slotted layout, vertical wires, and zero-JS
|
|
606
|
+
fold/downstream-hide behavior. The API is experimental and may change; it is
|
|
607
|
+
deliberately not exported from the top-level `coeftable` namespace yet.
|
|
608
|
+
|
|
609
|
+
```python
|
|
610
|
+
import coeftable as ct
|
|
611
|
+
from coeftable.cards import Card
|
|
612
|
+
from coeftable.graph import MetricTree
|
|
613
|
+
|
|
614
|
+
tree = MetricTree(
|
|
615
|
+
nodes=(
|
|
616
|
+
("revenue", Card("Revenue")),
|
|
617
|
+
("price", Card("Price")),
|
|
618
|
+
("volume", Card("Volume")),
|
|
619
|
+
("retention", Card("Retention")),
|
|
620
|
+
),
|
|
621
|
+
edges=(
|
|
622
|
+
("revenue", "price", 1.2),
|
|
623
|
+
("revenue", "volume", 2.1),
|
|
624
|
+
("volume", "retention", -0.6),
|
|
625
|
+
),
|
|
626
|
+
fmt=ct.Number(suffix=' pp', decimals=1, signed=True, thousands=False),
|
|
627
|
+
dom_prefix='drivers',
|
|
628
|
+
)
|
|
629
|
+
html = tree.as_raw_html()
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Use a distinct `dom_prefix` for each tree when rendering multiple graphs in one
|
|
633
|
+
document, so their generated control, card, and wire ids remain disjoint.
|
|
634
|
+
|
|
635
|
+
Each card retains its native `<details>` fold control. The nub under a parent
|
|
636
|
+
hides a node only when every root path to it is blocked: collapsing one parent
|
|
637
|
+
of a shared child leaves that child visible via the other path, all with pure
|
|
638
|
+
CSS and no JavaScript.
|
|
639
|
+
|
|
640
|
+
A `MetricTree` left as the last notebook expression renders itself via
|
|
641
|
+
`_repr_html_`; `with_theme()` follows the same conventions as cards.
|
|
642
|
+
|
|
643
|
+
## Driver-tree reports (experimental)
|
|
644
|
+
|
|
645
|
+
`coeftable.graph.DriverTree` is the composition root over the metric-tree
|
|
646
|
+
layer: give it level series and a decomposition per parent, and it builds the
|
|
647
|
+
cards, wires, and layout itself, plus three opt-in honesty checks a hand-built
|
|
648
|
+
`MetricTree` graph would otherwise have to redo per node. The API is
|
|
649
|
+
experimental and may change; it is deliberately not exported from the
|
|
650
|
+
top-level `coeftable` namespace yet.
|
|
651
|
+
|
|
652
|
+
```python
|
|
653
|
+
import coeftable as ct
|
|
654
|
+
from coeftable.graph import DriverTree
|
|
655
|
+
from coeftable.graph.breakout import Breakout
|
|
656
|
+
from coeftable.graph.timeline import TimelineEvent
|
|
657
|
+
|
|
658
|
+
x = (0.0, 1.0, 2.0, 3.0)
|
|
659
|
+
series = {
|
|
660
|
+
"revenue": (1000.0, 1071.0, 1144.0, 1219.0),
|
|
661
|
+
"users": (100.0, 105.0, 110.0, 115.0),
|
|
662
|
+
"aov": (10.0, 10.2, 10.4, 10.6),
|
|
663
|
+
"us": (600.0, 640.0, 680.0, 720.0),
|
|
664
|
+
"eu": (400.0, 431.0, 464.0, 499.0),
|
|
665
|
+
}
|
|
666
|
+
titles = {"revenue": "Revenue", "users": "Users", "aov": "AOV", "us": "US", "eu": "EU"}
|
|
667
|
+
breakouts = {
|
|
668
|
+
"revenue": (
|
|
669
|
+
Breakout(key="drivers", label="by drivers", op="x", children=("users", "aov")),
|
|
670
|
+
Breakout(key="region", label="by region", op="+", children=("us", "eu")),
|
|
671
|
+
)
|
|
672
|
+
}
|
|
673
|
+
events = (TimelineEvent(at=1.0, label="Launch", color="#4C72B0", affects=("revenue", "users")),)
|
|
674
|
+
|
|
675
|
+
report = DriverTree(
|
|
676
|
+
series,
|
|
677
|
+
titles,
|
|
678
|
+
breakouts,
|
|
679
|
+
ct.Percent(decimals=1),
|
|
680
|
+
x,
|
|
681
|
+
events=events,
|
|
682
|
+
level_fmt=ct.Number(decimals=1),
|
|
683
|
+
)
|
|
684
|
+
html = report.as_raw_html()
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Every node id in `series` and `titles` is derived from `breakouts`: each key
|
|
688
|
+
is a parent, and each `Breakout` names one decomposition of it (an `op="x"`
|
|
689
|
+
product or an `op="+"` sum) plus the child node ids it contributes. A parent
|
|
690
|
+
with two or more breakouts (like `revenue` above) renders a native
|
|
691
|
+
`<select>` that swaps its children's whole subtree in place — no JavaScript,
|
|
692
|
+
because the alternatives share one slotted position and pure CSS shows
|
|
693
|
+
exactly one of them at a time.
|
|
694
|
+
|
|
695
|
+
Each decomposition is checked against its own arithmetic: a parent that
|
|
696
|
+
should equal the sum or product of its children but falls short gets an
|
|
697
|
+
injected `"Unattributed"` residual card (additive shortfalls) or a reported
|
|
698
|
+
gap badge (multiplicative shortfalls, which have no subtraction fix), and a
|
|
699
|
+
decomposition explaining under 80% of its parent refuses to build rather than
|
|
700
|
+
render a misleading tree. Every wire's label role comes from its own child's
|
|
701
|
+
noise-aware confidence interval, so a wobbly delta renders muted with a
|
|
702
|
+
`· ns` marker instead of a confident color. Anti-correlated siblings (movers
|
|
703
|
+
that trade off against each other week to week) surface a callout hosted on
|
|
704
|
+
one of the participating children, so it stays visible only alongside that
|
|
705
|
+
alternative rather than the parent, which stays visible under every switch.
|
|
706
|
+
The root card renders no text of its own; pass `caption=` to add one. It
|
|
707
|
+
wraps to fit the card using an estimated character width, growing across
|
|
708
|
+
as many lines as that estimate needs. Runs of whitespace collapse to a
|
|
709
|
+
single space and line breaks are not preserved, and unusually wide text
|
|
710
|
+
can still be clipped on a line, since the wrap is an estimate rather than
|
|
711
|
+
a font measurement. A literal `{weeks}` placeholder is substituted for the
|
|
712
|
+
observed period count, and `caption` defaults to `None`, so nothing
|
|
713
|
+
renders unless you ask for it.
|
|
714
|
+
If you want a note that edge labels are accounting, not causal
|
|
715
|
+
claims, that wording is yours to choose — for example
|
|
716
|
+
`caption="Edge labels are an accounting of the realized {weeks}-week
|
|
717
|
+
change, not causal impact."` `events` fan out to every card named in their
|
|
718
|
+
`affects` tuple, both as sparkline markers and as captions, and the
|
|
719
|
+
report's header is a timeline strip indexing them across the whole canvas.
|
|
720
|
+
That strip carries no heading of its own — pass `strip_title=` if you want
|
|
721
|
+
one, since only you know whether your events are deploys, holidays, or
|
|
722
|
+
experiments. With neither events nor a `strip_title` there is nothing to
|
|
723
|
+
index and no heading to show, so the strip is omitted rather than reserving
|
|
724
|
+
space for a bare axis; supplying either one brings it back.
|
|
725
|
+
|
|
726
|
+
**Switchers can nest.** Every level of the tree may carry its own switcher at
|
|
727
|
+
the same time — revenue by drivers or region, and within drivers, active users
|
|
728
|
+
by funnel or country, and within that, sessions by platform or channel.
|
|
729
|
+
Switching an outer choice takes away the whole branch beneath it, including any
|
|
730
|
+
nested switcher and its alternatives; switching back restores it with the inner
|
|
731
|
+
selection intact, because a native `<select>` keeps its value while hidden.
|
|
732
|
+
|
|
733
|
+
**Limitations.** All alternatives of one switcher must have the same number of
|
|
734
|
+
children, since they share the same positions.
|
|
735
|
+
|
|
736
|
+
And every card must keep a visible parent under every combination of choices. A
|
|
737
|
+
card that disappears together with its own alternative is fine — that is the
|
|
738
|
+
point of a switcher. What is refused is a card whose visibility depends on
|
|
739
|
+
several switchers that do not subsume one another, so that some combination of
|
|
740
|
+
selections leaves it on the page with every route to it closed.
|
|
741
|
+
|
|
742
|
+
Nesting satisfies this on its own: an outer choice that removes a branch is
|
|
743
|
+
decided by the outer rule alone, and everything below goes with it. Being
|
|
744
|
+
reachable through more than one switcher is not by itself a problem either — if
|
|
745
|
+
every option of each switcher reaches the card, or if any route to it is
|
|
746
|
+
unconditional, it always keeps a parent and the tree is accepted.
|
|
747
|
+
|
|
748
|
+
One shape is refused: a card where *every* route can be independently closed and
|
|
749
|
+
none survives unconditionally. The clearest example is a card reachable through a
|
|
750
|
+
nested branch and also through a different alternative of the switcher above it.
|
|
751
|
+
Keep the outer branch that preserves the nested switcher, then pick an inner
|
|
752
|
+
option that does not reach the card, and both of its routes close at once. Since
|
|
753
|
+
each switcher's rules are emitted independently, no single one can see that. Such
|
|
754
|
+
a tree is rejected at construction with a `SpecError` naming the card and every
|
|
755
|
+
switcher implicated in it.
|
|
756
|
+
|
|
757
|
+
`DriverTree` returns a `GraphReport`: the underlying `Graph`, and above it a
|
|
758
|
+
measured, exact-width timeline strip whenever events or a `strip_title` call
|
|
759
|
+
for one. `report.measure()`,
|
|
760
|
+
`report.as_raw_html()`, and its `_repr_html_` notebook display all work the
|
|
761
|
+
same way they do for a plain `Graph`.
|
|
762
|
+
|
|
473
763
|
## Plot annotations
|
|
474
764
|
|
|
475
765
|
`ct.Rule` draws a line and `ct.Band` shades an interval in a forest plot or
|
|
@@ -416,6 +416,296 @@ absolute = pl.DataFrame(
|
|
|
416
416
|
)
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
+
## Standalone plots
|
|
420
|
+
|
|
421
|
+
The inline SVG plots that power `.forest(...)` and `.sparkline(...)` columns
|
|
422
|
+
are available directly from `coeftable.plots` — the same emitters, the same
|
|
423
|
+
themes, no table required. Each function returns a complete `<svg>` string
|
|
424
|
+
for embedding anywhere HTML goes:
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from coeftable.plots import forest_bar, sparkline_bar
|
|
428
|
+
from coeftable.theme import DEFAULT
|
|
429
|
+
import coeftable as ct
|
|
430
|
+
|
|
431
|
+
bar = forest_bar(
|
|
432
|
+
1.2, 0.4, 2.0, # estimate, lower, upper
|
|
433
|
+
domain=(-1.0, 3.0),
|
|
434
|
+
ref=0.0,
|
|
435
|
+
color=DEFAULT.favorable,
|
|
436
|
+
theme=DEFAULT,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
line = sparkline_bar(
|
|
440
|
+
[0.0, 1.0, 2.0], # x
|
|
441
|
+
[1.0, 1.5, 2.0], # y
|
|
442
|
+
[0.5, 1.0, 1.6], # lower
|
|
443
|
+
[1.5, 2.0, 2.4], # upper
|
|
444
|
+
x_domain=(0.0, 2.0),
|
|
445
|
+
domain=(0.0, 3.0),
|
|
446
|
+
ref=0.0,
|
|
447
|
+
color=DEFAULT.favorable,
|
|
448
|
+
fmt=ct.Number(),
|
|
449
|
+
theme=DEFAULT,
|
|
450
|
+
)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Also exported: `sparkline_multi` + `Trace` for overlaid series,
|
|
454
|
+
`forest_axis` / `sparkline_axis` for the shared axis footer under a column
|
|
455
|
+
of plots, and `ResolvedRule` / `ResolvedBand` for reference lines and
|
|
456
|
+
shaded intervals via each function's `annotations=` parameter. Because a
|
|
457
|
+
standalone plot and a table column share one `Theme`, a report that mixes
|
|
458
|
+
both stays visually consistent.
|
|
459
|
+
|
|
460
|
+
## Metric cards (experimental)
|
|
461
|
+
|
|
462
|
+
`coeftable.cards` composes the standalone plots into measured, foldable
|
|
463
|
+
metric cards — the same formatters, themes, and SVG primitives as the
|
|
464
|
+
tables. The API is experimental and may change; it is deliberately not
|
|
465
|
+
exported from the top-level `coeftable` namespace yet.
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
import coeftable as ct
|
|
469
|
+
from coeftable.cards import Card, CardGrid
|
|
470
|
+
from coeftable.cards.regions import Diagnostics, Event, Events, Metric, Trend
|
|
471
|
+
|
|
472
|
+
events = Events([Event("launch", "#4C72B0", at=2.0)])
|
|
473
|
+
revenue = Card(
|
|
474
|
+
"Revenue",
|
|
475
|
+
subtitle="weekly lift vs control",
|
|
476
|
+
content=[
|
|
477
|
+
Metric(3.4, ct.Percent(signed=True), ci=(1.2, 5.7), ref=0.0),
|
|
478
|
+
Trend(
|
|
479
|
+
x=(0, 1, 2, 3), y=(0.3, 0.8, 1.1, 1.5),
|
|
480
|
+
lower=(-0.1, 0.3, 0.6, 0.9), upper=(0.7, 1.3, 1.6, 2.1),
|
|
481
|
+
x_domain=(0, 3), domain=(-0.5, 2.5), ref=0.0,
|
|
482
|
+
annotations=events.rules(),
|
|
483
|
+
),
|
|
484
|
+
Diagnostics("diagnostics", [("n", 412), ("sigma", 0.8)]),
|
|
485
|
+
events,
|
|
486
|
+
],
|
|
487
|
+
)
|
|
488
|
+
grid = CardGrid([revenue, revenue.with_theme(ct.theme.BLUE)])
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
A `Card` (or `CardGrid`) left as the last notebook expression renders
|
|
492
|
+
itself; `as_raw_html()` is the string entry point. Cards are fixed-width
|
|
493
|
+
with exact measured heights and fold via a native `<details>` header (the
|
|
494
|
+
headline value stays visible as a chip when it fits beside the header,
|
|
495
|
+
and is omitted otherwise). A standalone card collapses to
|
|
496
|
+
its header; inside a `CardGrid` each card's expanded footprint stays
|
|
497
|
+
reserved, so folding never reflows neighbors.
|
|
498
|
+
|
|
499
|
+
## Metric panels (experimental)
|
|
500
|
+
|
|
501
|
+
`coeftable.cards` panels put named side-by-side panes in one bordered shell
|
|
502
|
+
with a shared header and footer: the compositional step above cards. The API
|
|
503
|
+
is experimental and may change; it is deliberately not exported from the
|
|
504
|
+
top-level `coeftable` namespace yet.
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
import coeftable as ct
|
|
508
|
+
from coeftable.cards import Metric, Pane, Panel, Row, TextBlock
|
|
509
|
+
|
|
510
|
+
summary = Pane(
|
|
511
|
+
"Summary",
|
|
512
|
+
content=(
|
|
513
|
+
TextBlock("Weekly revenue", variant="subtitle"),
|
|
514
|
+
Metric(3.4, ct.Percent(signed=True), ci=(1.2, 5.7), ref=0.0),
|
|
515
|
+
),
|
|
516
|
+
width=220,
|
|
517
|
+
)
|
|
518
|
+
segments = Pane(
|
|
519
|
+
"Segments",
|
|
520
|
+
content=(
|
|
521
|
+
Row(
|
|
522
|
+
(
|
|
523
|
+
(TextBlock("Enterprise"), 85),
|
|
524
|
+
(Metric(8.1, ct.Percent(signed=True)), 105),
|
|
525
|
+
)
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
width=200,
|
|
529
|
+
)
|
|
530
|
+
header = Row(
|
|
531
|
+
(
|
|
532
|
+
(TextBlock("Revenue", variant="title"), 320),
|
|
533
|
+
(Metric(3.4, ct.Percent(signed=True)), 120),
|
|
534
|
+
),
|
|
535
|
+
gap=16,
|
|
536
|
+
)
|
|
537
|
+
panel = Panel((summary, segments), header=(header,))
|
|
538
|
+
html = panel.as_raw_html()
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Panes declare usable content widths; cell widths are author inputs; the panel
|
|
542
|
+
derives its own footprint with exact measurement, not responsive guessing.
|
|
543
|
+
|
|
544
|
+
A `Panel` left as the last notebook expression renders itself via
|
|
545
|
+
`_repr_html_`; `with_theme()` returns the same composition under another
|
|
546
|
+
theme.
|
|
547
|
+
|
|
548
|
+
## Metric trees (experimental)
|
|
549
|
+
|
|
550
|
+
`coeftable.graph` turns metric cards wired into a driver DAG into a metric
|
|
551
|
+
tree: the graph layer adds slotted layout, vertical wires, and zero-JS
|
|
552
|
+
fold/downstream-hide behavior. The API is experimental and may change; it is
|
|
553
|
+
deliberately not exported from the top-level `coeftable` namespace yet.
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
import coeftable as ct
|
|
557
|
+
from coeftable.cards import Card
|
|
558
|
+
from coeftable.graph import MetricTree
|
|
559
|
+
|
|
560
|
+
tree = MetricTree(
|
|
561
|
+
nodes=(
|
|
562
|
+
("revenue", Card("Revenue")),
|
|
563
|
+
("price", Card("Price")),
|
|
564
|
+
("volume", Card("Volume")),
|
|
565
|
+
("retention", Card("Retention")),
|
|
566
|
+
),
|
|
567
|
+
edges=(
|
|
568
|
+
("revenue", "price", 1.2),
|
|
569
|
+
("revenue", "volume", 2.1),
|
|
570
|
+
("volume", "retention", -0.6),
|
|
571
|
+
),
|
|
572
|
+
fmt=ct.Number(suffix=' pp', decimals=1, signed=True, thousands=False),
|
|
573
|
+
dom_prefix='drivers',
|
|
574
|
+
)
|
|
575
|
+
html = tree.as_raw_html()
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Use a distinct `dom_prefix` for each tree when rendering multiple graphs in one
|
|
579
|
+
document, so their generated control, card, and wire ids remain disjoint.
|
|
580
|
+
|
|
581
|
+
Each card retains its native `<details>` fold control. The nub under a parent
|
|
582
|
+
hides a node only when every root path to it is blocked: collapsing one parent
|
|
583
|
+
of a shared child leaves that child visible via the other path, all with pure
|
|
584
|
+
CSS and no JavaScript.
|
|
585
|
+
|
|
586
|
+
A `MetricTree` left as the last notebook expression renders itself via
|
|
587
|
+
`_repr_html_`; `with_theme()` follows the same conventions as cards.
|
|
588
|
+
|
|
589
|
+
## Driver-tree reports (experimental)
|
|
590
|
+
|
|
591
|
+
`coeftable.graph.DriverTree` is the composition root over the metric-tree
|
|
592
|
+
layer: give it level series and a decomposition per parent, and it builds the
|
|
593
|
+
cards, wires, and layout itself, plus three opt-in honesty checks a hand-built
|
|
594
|
+
`MetricTree` graph would otherwise have to redo per node. The API is
|
|
595
|
+
experimental and may change; it is deliberately not exported from the
|
|
596
|
+
top-level `coeftable` namespace yet.
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
import coeftable as ct
|
|
600
|
+
from coeftable.graph import DriverTree
|
|
601
|
+
from coeftable.graph.breakout import Breakout
|
|
602
|
+
from coeftable.graph.timeline import TimelineEvent
|
|
603
|
+
|
|
604
|
+
x = (0.0, 1.0, 2.0, 3.0)
|
|
605
|
+
series = {
|
|
606
|
+
"revenue": (1000.0, 1071.0, 1144.0, 1219.0),
|
|
607
|
+
"users": (100.0, 105.0, 110.0, 115.0),
|
|
608
|
+
"aov": (10.0, 10.2, 10.4, 10.6),
|
|
609
|
+
"us": (600.0, 640.0, 680.0, 720.0),
|
|
610
|
+
"eu": (400.0, 431.0, 464.0, 499.0),
|
|
611
|
+
}
|
|
612
|
+
titles = {"revenue": "Revenue", "users": "Users", "aov": "AOV", "us": "US", "eu": "EU"}
|
|
613
|
+
breakouts = {
|
|
614
|
+
"revenue": (
|
|
615
|
+
Breakout(key="drivers", label="by drivers", op="x", children=("users", "aov")),
|
|
616
|
+
Breakout(key="region", label="by region", op="+", children=("us", "eu")),
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
events = (TimelineEvent(at=1.0, label="Launch", color="#4C72B0", affects=("revenue", "users")),)
|
|
620
|
+
|
|
621
|
+
report = DriverTree(
|
|
622
|
+
series,
|
|
623
|
+
titles,
|
|
624
|
+
breakouts,
|
|
625
|
+
ct.Percent(decimals=1),
|
|
626
|
+
x,
|
|
627
|
+
events=events,
|
|
628
|
+
level_fmt=ct.Number(decimals=1),
|
|
629
|
+
)
|
|
630
|
+
html = report.as_raw_html()
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
Every node id in `series` and `titles` is derived from `breakouts`: each key
|
|
634
|
+
is a parent, and each `Breakout` names one decomposition of it (an `op="x"`
|
|
635
|
+
product or an `op="+"` sum) plus the child node ids it contributes. A parent
|
|
636
|
+
with two or more breakouts (like `revenue` above) renders a native
|
|
637
|
+
`<select>` that swaps its children's whole subtree in place — no JavaScript,
|
|
638
|
+
because the alternatives share one slotted position and pure CSS shows
|
|
639
|
+
exactly one of them at a time.
|
|
640
|
+
|
|
641
|
+
Each decomposition is checked against its own arithmetic: a parent that
|
|
642
|
+
should equal the sum or product of its children but falls short gets an
|
|
643
|
+
injected `"Unattributed"` residual card (additive shortfalls) or a reported
|
|
644
|
+
gap badge (multiplicative shortfalls, which have no subtraction fix), and a
|
|
645
|
+
decomposition explaining under 80% of its parent refuses to build rather than
|
|
646
|
+
render a misleading tree. Every wire's label role comes from its own child's
|
|
647
|
+
noise-aware confidence interval, so a wobbly delta renders muted with a
|
|
648
|
+
`· ns` marker instead of a confident color. Anti-correlated siblings (movers
|
|
649
|
+
that trade off against each other week to week) surface a callout hosted on
|
|
650
|
+
one of the participating children, so it stays visible only alongside that
|
|
651
|
+
alternative rather than the parent, which stays visible under every switch.
|
|
652
|
+
The root card renders no text of its own; pass `caption=` to add one. It
|
|
653
|
+
wraps to fit the card using an estimated character width, growing across
|
|
654
|
+
as many lines as that estimate needs. Runs of whitespace collapse to a
|
|
655
|
+
single space and line breaks are not preserved, and unusually wide text
|
|
656
|
+
can still be clipped on a line, since the wrap is an estimate rather than
|
|
657
|
+
a font measurement. A literal `{weeks}` placeholder is substituted for the
|
|
658
|
+
observed period count, and `caption` defaults to `None`, so nothing
|
|
659
|
+
renders unless you ask for it.
|
|
660
|
+
If you want a note that edge labels are accounting, not causal
|
|
661
|
+
claims, that wording is yours to choose — for example
|
|
662
|
+
`caption="Edge labels are an accounting of the realized {weeks}-week
|
|
663
|
+
change, not causal impact."` `events` fan out to every card named in their
|
|
664
|
+
`affects` tuple, both as sparkline markers and as captions, and the
|
|
665
|
+
report's header is a timeline strip indexing them across the whole canvas.
|
|
666
|
+
That strip carries no heading of its own — pass `strip_title=` if you want
|
|
667
|
+
one, since only you know whether your events are deploys, holidays, or
|
|
668
|
+
experiments. With neither events nor a `strip_title` there is nothing to
|
|
669
|
+
index and no heading to show, so the strip is omitted rather than reserving
|
|
670
|
+
space for a bare axis; supplying either one brings it back.
|
|
671
|
+
|
|
672
|
+
**Switchers can nest.** Every level of the tree may carry its own switcher at
|
|
673
|
+
the same time — revenue by drivers or region, and within drivers, active users
|
|
674
|
+
by funnel or country, and within that, sessions by platform or channel.
|
|
675
|
+
Switching an outer choice takes away the whole branch beneath it, including any
|
|
676
|
+
nested switcher and its alternatives; switching back restores it with the inner
|
|
677
|
+
selection intact, because a native `<select>` keeps its value while hidden.
|
|
678
|
+
|
|
679
|
+
**Limitations.** All alternatives of one switcher must have the same number of
|
|
680
|
+
children, since they share the same positions.
|
|
681
|
+
|
|
682
|
+
And every card must keep a visible parent under every combination of choices. A
|
|
683
|
+
card that disappears together with its own alternative is fine — that is the
|
|
684
|
+
point of a switcher. What is refused is a card whose visibility depends on
|
|
685
|
+
several switchers that do not subsume one another, so that some combination of
|
|
686
|
+
selections leaves it on the page with every route to it closed.
|
|
687
|
+
|
|
688
|
+
Nesting satisfies this on its own: an outer choice that removes a branch is
|
|
689
|
+
decided by the outer rule alone, and everything below goes with it. Being
|
|
690
|
+
reachable through more than one switcher is not by itself a problem either — if
|
|
691
|
+
every option of each switcher reaches the card, or if any route to it is
|
|
692
|
+
unconditional, it always keeps a parent and the tree is accepted.
|
|
693
|
+
|
|
694
|
+
One shape is refused: a card where *every* route can be independently closed and
|
|
695
|
+
none survives unconditionally. The clearest example is a card reachable through a
|
|
696
|
+
nested branch and also through a different alternative of the switcher above it.
|
|
697
|
+
Keep the outer branch that preserves the nested switcher, then pick an inner
|
|
698
|
+
option that does not reach the card, and both of its routes close at once. Since
|
|
699
|
+
each switcher's rules are emitted independently, no single one can see that. Such
|
|
700
|
+
a tree is rejected at construction with a `SpecError` naming the card and every
|
|
701
|
+
switcher implicated in it.
|
|
702
|
+
|
|
703
|
+
`DriverTree` returns a `GraphReport`: the underlying `Graph`, and above it a
|
|
704
|
+
measured, exact-width timeline strip whenever events or a `strip_title` call
|
|
705
|
+
for one. `report.measure()`,
|
|
706
|
+
`report.as_raw_html()`, and its `_repr_html_` notebook display all work the
|
|
707
|
+
same way they do for a plain `Graph`.
|
|
708
|
+
|
|
419
709
|
## Plot annotations
|
|
420
710
|
|
|
421
711
|
`ct.Rule` draws a line and `ct.Band` shades an interval in a forest plot or
|