coeftable 0.4.0__tar.gz → 0.6.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.4.0 → coeftable-0.6.0}/.gitignore +1 -0
- coeftable-0.6.0/AGENTS.md +7 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/CHANGELOG.md +32 -3
- {coeftable-0.4.0 → coeftable-0.6.0}/PKG-INFO +201 -1
- {coeftable-0.4.0 → coeftable-0.6.0}/README.md +200 -0
- coeftable-0.6.0/docs/images/forest-robust-autoscale.png +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/pyproject.toml +14 -2
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/_version.py +2 -2
- coeftable-0.6.0/src/coeftable/cards/__init__.py +67 -0
- coeftable-0.6.0/src/coeftable/cards/adornments.py +254 -0
- coeftable-0.6.0/src/coeftable/cards/card.py +127 -0
- coeftable-0.6.0/src/coeftable/cards/chrome.py +83 -0
- coeftable-0.6.0/src/coeftable/cards/fragments.py +240 -0
- coeftable-0.6.0/src/coeftable/cards/measure.py +361 -0
- coeftable-0.6.0/src/coeftable/cards/panel.py +402 -0
- coeftable-0.6.0/src/coeftable/cards/regions.py +598 -0
- coeftable-0.6.0/src/coeftable/cards/template.py +110 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/frame.py +1 -1
- coeftable-0.6.0/src/coeftable/graph/__init__.py +25 -0
- coeftable-0.6.0/src/coeftable/graph/metric_tree.py +250 -0
- coeftable-0.6.0/src/coeftable/graph/model.py +876 -0
- coeftable-0.6.0/src/coeftable/graph/render.py +144 -0
- coeftable-0.6.0/src/coeftable/graph/state.py +215 -0
- coeftable-0.6.0/src/coeftable/graph/topology.py +183 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/grid.py +1 -1
- coeftable-0.6.0/src/coeftable/plots.py +36 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/spec.py +48 -5
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/svg.py +125 -42
- coeftable-0.6.0/tests/test_card_entry.py +279 -0
- coeftable-0.6.0/tests/test_cards.py +1342 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_frame.py +88 -0
- coeftable-0.6.0/tests/test_graph.py +2652 -0
- coeftable-0.6.0/tests/test_panel.py +688 -0
- coeftable-0.6.0/tests/test_plots.py +148 -0
- coeftable-0.6.0/tests/test_regions.py +871 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_svg.py +194 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/release.yml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/workflows/ci.yml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/workflows/post-release.yml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/workflows/publish.yml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.github/workflows/release.yml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/.pre-commit-config.yaml +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/LICENSE +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/Makefile +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/docs/images/example.png +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/docs/images/trend-example.png +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/noxfile.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/__init__.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/_axis.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/annotations.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/collapsible.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/errors.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/format.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/render.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/series.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/src/coeftable/theme.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_annotations.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_collapsible.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_format.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_package.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_public_api.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_render.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_series.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_sparkline.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_spec.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/tests/test_theme.py +0 -0
- {coeftable-0.4.0 → coeftable-0.6.0}/uv.lock +0 -0
|
@@ -1,7 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
<a id="v0.5.0"></a>
|
|
2
|
+
# [v0.5.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.5.0) - 2026-08-19
|
|
3
|
+
|
|
4
|
+
<!-- Release notes generated using configuration in .github/release.yml at v0.5.0 -->
|
|
5
|
+
|
|
6
|
+
## What's Changed
|
|
7
|
+
### Maintenance
|
|
8
|
+
* Update changelog for v0.4.0 by [@github-actions](https://github.com/github-actions)[bot] in [#32](https://github.com/kylejcaron/coeftable/pull/32)
|
|
9
|
+
* 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)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.4.0...v0.5.0
|
|
13
|
+
|
|
14
|
+
[Changes][v0.5.0]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
<a id="v0.4.0"></a>
|
|
18
|
+
# [v0.4.0](https://github.com/kylejcaron/coeftable/releases/tag/v0.4.0) - 2026-08-14
|
|
19
|
+
|
|
20
|
+
<!-- Release notes generated using configuration in .github/release.yml at v0.4.0 -->
|
|
21
|
+
|
|
22
|
+
## What's Changed
|
|
23
|
+
### Maintenance
|
|
24
|
+
* build: align hatchling metadata floor by [@kylejcaron](https://github.com/kylejcaron) in [#30](https://github.com/kylejcaron/coeftable/pull/30)
|
|
25
|
+
* feature: support plot annotations by [@kylejcaron](https://github.com/kylejcaron) in [#31](https://github.com/kylejcaron/coeftable/pull/31)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
**Full Changelog**: https://github.com/kylejcaron/coeftable/compare/v0.3.1...v0.4.0
|
|
29
|
+
|
|
30
|
+
[Changes][v0.4.0]
|
|
2
31
|
|
|
3
|
-
### Features
|
|
4
|
-
- Add typed rule and band annotations to forest plots and sparklines, including row-specific field binding and domain-aware layering.
|
|
5
32
|
|
|
6
33
|
<a id="v0.3.1"></a>
|
|
7
34
|
# [v0.3.1](https://github.com/kylejcaron/coeftable/releases/tag/v0.3.1) - 2026-08-14
|
|
@@ -97,6 +124,8 @@
|
|
|
97
124
|
[Changes][v0.1.0]
|
|
98
125
|
|
|
99
126
|
|
|
127
|
+
[v0.5.0]: https://github.com/kylejcaron/coeftable/compare/v0.4.0...v0.5.0
|
|
128
|
+
[v0.4.0]: https://github.com/kylejcaron/coeftable/compare/v0.3.1...v0.4.0
|
|
100
129
|
[v0.3.1]: https://github.com/kylejcaron/coeftable/compare/v0.3.0...v0.3.1
|
|
101
130
|
[v0.3.0]: https://github.com/kylejcaron/coeftable/compare/v0.2.1...v0.3.0
|
|
102
131
|
[v0.2.1]: https://github.com/kylejcaron/coeftable/compare/v0.2.0...v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coeftable
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -414,6 +414,36 @@ trend = pl.DataFrame(
|
|
|
414
414
|
)
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
+
**Forest plots take `autoscale` too.** A single extreme interval (say a
|
|
418
|
+
+3000% lift among sub-1% metrics) otherwise stretches the shared domain
|
|
419
|
+
and flattens every other bar:
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
lifts = pl.DataFrame(
|
|
423
|
+
{
|
|
424
|
+
"metric": ["Checkout CVR", "Signup Rate", "Retention D7", "AOV", "Rare Event"],
|
|
425
|
+
"rel": [0.03, -0.05, 0.08, 0.02, 30.0],
|
|
426
|
+
"rel_lb": [0.01, -0.09, 0.04, -0.01, 22.0],
|
|
427
|
+
"rel_ub": [0.05, -0.01, 0.12, 0.05, 41.0],
|
|
428
|
+
}
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
(
|
|
432
|
+
ct.CoefTable(lifts, rows="metric")
|
|
433
|
+
.estimate("Lift %", "rel", ci=("rel_lb", "rel_ub"), fmt=ct.Percent(signed=True))
|
|
434
|
+
# "robust" fits an IQR/Tukey fence over the pooled values instead of
|
|
435
|
+
# the raw min/max, so the outlier stops dictating the axis. Composes
|
|
436
|
+
# with symmetric= (fence first, then mirror around ref); ylim= still
|
|
437
|
+
# overrides everything.
|
|
438
|
+
.forest("Lift Plot", of="Lift %", ref=0.0, autoscale="robust")
|
|
439
|
+
)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
A discounted interval is never hidden: bars in a clipping domain plot
|
|
443
|
+
against a slightly inset region and the clipped interval continues into
|
|
444
|
+
the reserved margin as a gradient fade, reading as "extends beyond the
|
|
445
|
+
axis". Buckets with nothing clipped render exactly as before.
|
|
446
|
+
|
|
417
447
|
**No reference, or a hidden one.** `ref` also drives the dashed line and
|
|
418
448
|
colour resolution; two ways to opt out, for data with no meaningful
|
|
419
449
|
zero (revenue, durations, absolute counts):
|
|
@@ -440,6 +470,176 @@ absolute = pl.DataFrame(
|
|
|
440
470
|
)
|
|
441
471
|
```
|
|
442
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
|
+
|
|
443
643
|
## Plot annotations
|
|
444
644
|
|
|
445
645
|
`ct.Rule` draws a line and `ct.Band` shades an interval in a forest plot or
|
|
@@ -360,6 +360,36 @@ trend = pl.DataFrame(
|
|
|
360
360
|
)
|
|
361
361
|
```
|
|
362
362
|
|
|
363
|
+
**Forest plots take `autoscale` too.** A single extreme interval (say a
|
|
364
|
+
+3000% lift among sub-1% metrics) otherwise stretches the shared domain
|
|
365
|
+
and flattens every other bar:
|
|
366
|
+
|
|
367
|
+
```python
|
|
368
|
+
lifts = pl.DataFrame(
|
|
369
|
+
{
|
|
370
|
+
"metric": ["Checkout CVR", "Signup Rate", "Retention D7", "AOV", "Rare Event"],
|
|
371
|
+
"rel": [0.03, -0.05, 0.08, 0.02, 30.0],
|
|
372
|
+
"rel_lb": [0.01, -0.09, 0.04, -0.01, 22.0],
|
|
373
|
+
"rel_ub": [0.05, -0.01, 0.12, 0.05, 41.0],
|
|
374
|
+
}
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
(
|
|
378
|
+
ct.CoefTable(lifts, rows="metric")
|
|
379
|
+
.estimate("Lift %", "rel", ci=("rel_lb", "rel_ub"), fmt=ct.Percent(signed=True))
|
|
380
|
+
# "robust" fits an IQR/Tukey fence over the pooled values instead of
|
|
381
|
+
# the raw min/max, so the outlier stops dictating the axis. Composes
|
|
382
|
+
# with symmetric= (fence first, then mirror around ref); ylim= still
|
|
383
|
+
# overrides everything.
|
|
384
|
+
.forest("Lift Plot", of="Lift %", ref=0.0, autoscale="robust")
|
|
385
|
+
)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
A discounted interval is never hidden: bars in a clipping domain plot
|
|
389
|
+
against a slightly inset region and the clipped interval continues into
|
|
390
|
+
the reserved margin as a gradient fade, reading as "extends beyond the
|
|
391
|
+
axis". Buckets with nothing clipped render exactly as before.
|
|
392
|
+
|
|
363
393
|
**No reference, or a hidden one.** `ref` also drives the dashed line and
|
|
364
394
|
colour resolution; two ways to opt out, for data with no meaningful
|
|
365
395
|
zero (revenue, durations, absolute counts):
|
|
@@ -386,6 +416,176 @@ absolute = pl.DataFrame(
|
|
|
386
416
|
)
|
|
387
417
|
```
|
|
388
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
|
+
|
|
389
589
|
## Plot annotations
|
|
390
590
|
|
|
391
591
|
`ct.Rule` draws a line and `ct.Band` shades an interval in a forest plot or
|
|
Binary file
|
|
@@ -72,14 +72,26 @@ src = ["src", "tests"]
|
|
|
72
72
|
docstring-code-format = true
|
|
73
73
|
|
|
74
74
|
[tool.ruff.lint]
|
|
75
|
-
select = ["B", "D", "E", "F", "I", "RUF", "S", "UP", "W"]
|
|
75
|
+
select = ["B", "C90", "D", "E", "F", "I", "PLR0911", "PLR0912", "PLR0913", "PLR0915", "RUF", "S", "UP", "W"]
|
|
76
76
|
ignore = ["RUF001", "RUF002", "RUF012"]
|
|
77
77
|
|
|
78
|
+
[tool.ruff.lint.mccabe]
|
|
79
|
+
# Anti-spaghetti gate: flag functions with cyclomatic complexity >= 25.
|
|
80
|
+
max-complexity = 24
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.pylint]
|
|
83
|
+
max-branches = 24
|
|
84
|
+
max-statements = 100
|
|
85
|
+
max-returns = 12
|
|
86
|
+
# Wide keyword-only APIs (plot primitives, table builders) are deliberate;
|
|
87
|
+
# only flag genuine monsters.
|
|
88
|
+
max-args = 24
|
|
89
|
+
|
|
78
90
|
[tool.ruff.lint.pydocstyle]
|
|
79
91
|
convention = "numpy"
|
|
80
92
|
|
|
81
93
|
[tool.ruff.lint.per-file-ignores]
|
|
82
|
-
"tests/*" = ["D", "S101"]
|
|
94
|
+
"tests/*" = ["D", "S101", "PLR0915"]
|
|
83
95
|
|
|
84
96
|
[tool.pytest.ini_options]
|
|
85
97
|
testpaths = ["tests"]
|
|
@@ -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.
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Experimental card layer. Not part of the public coeftable API yet.
|
|
2
|
+
|
|
3
|
+
The adornment vocabulary is the closed content contract card regions
|
|
4
|
+
resolve into; `render_adornment` is its only serializer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from coeftable.cards.adornments import (
|
|
8
|
+
Adornment,
|
|
9
|
+
Badge,
|
|
10
|
+
CaptionRow,
|
|
11
|
+
InlineSvg,
|
|
12
|
+
KeyValuePopover,
|
|
13
|
+
Legend,
|
|
14
|
+
MetricValue,
|
|
15
|
+
RuleStrip,
|
|
16
|
+
SelectControl,
|
|
17
|
+
TextBlock,
|
|
18
|
+
)
|
|
19
|
+
from coeftable.cards.card import Card, CardGrid
|
|
20
|
+
from coeftable.cards.chrome import DEFAULT_CHROME, CardChrome
|
|
21
|
+
from coeftable.cards.fragments import render_adornment
|
|
22
|
+
from coeftable.cards.measure import Anchor, MeasuredCard
|
|
23
|
+
from coeftable.cards.panel import MeasuredPanel, Pane, Panel, Row
|
|
24
|
+
from coeftable.cards.regions import (
|
|
25
|
+
Diagnostics,
|
|
26
|
+
Event,
|
|
27
|
+
Events,
|
|
28
|
+
Interval,
|
|
29
|
+
Metric,
|
|
30
|
+
Region,
|
|
31
|
+
Trend,
|
|
32
|
+
resolve_content,
|
|
33
|
+
)
|
|
34
|
+
from coeftable.cards.template import CardTemplate
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_CHROME",
|
|
38
|
+
"Adornment",
|
|
39
|
+
"Anchor",
|
|
40
|
+
"Badge",
|
|
41
|
+
"CaptionRow",
|
|
42
|
+
"Card",
|
|
43
|
+
"CardChrome",
|
|
44
|
+
"CardGrid",
|
|
45
|
+
"CardTemplate",
|
|
46
|
+
"Diagnostics",
|
|
47
|
+
"Event",
|
|
48
|
+
"Events",
|
|
49
|
+
"InlineSvg",
|
|
50
|
+
"Interval",
|
|
51
|
+
"KeyValuePopover",
|
|
52
|
+
"Legend",
|
|
53
|
+
"MeasuredCard",
|
|
54
|
+
"MeasuredPanel",
|
|
55
|
+
"Metric",
|
|
56
|
+
"MetricValue",
|
|
57
|
+
"Pane",
|
|
58
|
+
"Panel",
|
|
59
|
+
"Region",
|
|
60
|
+
"Row",
|
|
61
|
+
"RuleStrip",
|
|
62
|
+
"SelectControl",
|
|
63
|
+
"TextBlock",
|
|
64
|
+
"Trend",
|
|
65
|
+
"render_adornment",
|
|
66
|
+
"resolve_content",
|
|
67
|
+
]
|