tree-annotated-plot 0.2.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,9 +109,14 @@ class PlotConfig:
109
109
  "When off (default), tree tips not present in the chart's strain "
110
110
  "set are a fatal error. When on, those tips (and any internal "
111
111
  "nodes whose subtrees become empty) are dropped before drawing, "
112
- "with single-child internals collapsed into their kept child. "
113
- "Chart strains not present in the tree are *always* fatal "
114
- "regardless of this flag — pruning would silently lose plot data.",
112
+ "with single-child internals collapsed into their kept child.",
113
+ ] = False
114
+
115
+ prune_chart_to_tree: Annotated[
116
+ bool,
117
+ "When off (default), chart strains not present in the tree are a "
118
+ "fatal error. When on, chart rows whose `chart_strain_field` "
119
+ "value isn't a tree tip are filtered out before drawing.",
115
120
  ] = False
116
121
 
117
122
  strict_version: Annotated[
@@ -55,6 +55,7 @@ def plot(
55
55
  scale_bar: bool = False,
56
56
  branch_length_units: str | None = None,
57
57
  prune_tree_to_chart: bool = False,
58
+ prune_chart_to_tree: bool = False,
58
59
  strict_version: bool = True,
59
60
  connect_leader_to_label: bool = False,
60
61
  strain_label_font_size: float = 10.0,
@@ -82,6 +83,7 @@ def plot(
82
83
  scale_bar=scale_bar,
83
84
  branch_length_units=branch_length_units,
84
85
  prune_tree_to_chart=prune_tree_to_chart,
86
+ prune_chart_to_tree=prune_chart_to_tree,
85
87
  strict_version=strict_version,
86
88
  connect_leader_to_label=connect_leader_to_label,
87
89
  strain_label_font_size=strain_label_font_size,
@@ -168,12 +170,33 @@ def _build(
168
170
 
169
171
  chart_strains = _extract_chart_strains(spec, axis_hits, config.chart_strain_field)
170
172
 
173
+ if config.prune_chart_to_tree and (set(chart_strains) - set(tip_names)):
174
+ _prune_chart_spec_to_strains(
175
+ spec,
176
+ chart_strain_field=config.chart_strain_field,
177
+ keep_strains=set(tip_names),
178
+ )
179
+ chart = alt.Chart.from_dict(spec)
180
+ chart_strains = _extract_chart_strains(
181
+ spec, axis_hits, config.chart_strain_field
182
+ )
183
+ if not chart_strains:
184
+ raise ValueError(
185
+ "prune_chart_to_tree=True dropped every chart row: no "
186
+ "chart strain matched any tree tip under "
187
+ f"chart_strain_field={config.chart_strain_field!r} / "
188
+ f"tree_strain_field={config.tree_strain_field!r}. Pruning "
189
+ "is meant for charts that bundle a superset of strains; "
190
+ "an empty intersection suggests a wrong field choice."
191
+ )
192
+
171
193
  _reconcile_tips_and_strains(
172
194
  tree_strains=tip_names,
173
195
  chart_strains=chart_strains,
174
196
  chart_strain_field=config.chart_strain_field,
175
197
  tree_strain_field=config.tree_strain_field,
176
198
  prune_tree_to_chart=config.prune_tree_to_chart,
199
+ prune_chart_to_tree=config.prune_chart_to_tree,
177
200
  chart_spec=spec,
178
201
  tree_source=tree,
179
202
  )
@@ -641,6 +664,70 @@ def _extract_chart_strains(
641
664
  return _extract_field_values_from_spec_data(spec, chart_strain_field)
642
665
 
643
666
 
667
+ def _prune_chart_spec_to_strains(
668
+ spec: dict, *, chart_strain_field: str, keep_strains: set[str]
669
+ ) -> None:
670
+ """Filter a Vega-Lite spec in place to drop rows outside `keep_strains`.
671
+
672
+ Mutates three kinds of structure:
673
+ - top-level `datasets` entries (each a list of row-dicts).
674
+ - inline `data.values` lists anywhere in the spec tree.
675
+ - explicit `sort` lists on encoding channels bound to
676
+ `chart_strain_field` (any other `sort` is left alone).
677
+
678
+ Rows that don't carry `chart_strain_field` at all are preserved
679
+ (we have no signal to drop them). URL-backed data raises — we
680
+ can't fetch + filter at plot time, mirroring `_extract_chart_strains`.
681
+ """
682
+
683
+ def row_kept(row: Any) -> bool:
684
+ if not isinstance(row, dict):
685
+ return True
686
+ if chart_strain_field not in row:
687
+ return True
688
+ return row[chart_strain_field] in keep_strains
689
+
690
+ datasets = spec.get("datasets") if isinstance(spec, dict) else None
691
+ if isinstance(datasets, dict):
692
+ for name, rows in list(datasets.items()):
693
+ if isinstance(rows, list):
694
+ datasets[name] = [row for row in rows if row_kept(row)]
695
+
696
+ def walk(node: Any) -> None:
697
+ if isinstance(node, dict):
698
+ data = node.get("data")
699
+ if isinstance(data, dict):
700
+ if "url" in data:
701
+ raise ValueError(
702
+ f"chart references data via URL ({data['url']!r}); "
703
+ "URL data is not supported, so prune_chart_to_tree "
704
+ "cannot filter it. Materialize the data inline "
705
+ "(via alt.Chart(df) with a pandas DataFrame) before "
706
+ "saving the chart."
707
+ )
708
+ if "values" in data and isinstance(data["values"], list):
709
+ data["values"] = [row for row in data["values"] if row_kept(row)]
710
+ encoding = node.get("encoding")
711
+ if isinstance(encoding, dict):
712
+ for channel in encoding.values():
713
+ if not isinstance(channel, dict):
714
+ continue
715
+ if channel.get("field") != chart_strain_field:
716
+ continue
717
+ sort = channel.get("sort")
718
+ if isinstance(sort, list):
719
+ channel["sort"] = [s for s in sort if s in keep_strains]
720
+ for k, v in node.items():
721
+ if k in ("data", "datasets"):
722
+ continue
723
+ walk(v)
724
+ elif isinstance(node, list):
725
+ for item in node:
726
+ walk(item)
727
+
728
+ walk(spec)
729
+
730
+
644
731
  def _extract_field_values_from_spec_data(spec: dict, field: str) -> list[str]:
645
732
  """Walk spec for inline / named data and return distinct values of `field`.
646
733
 
@@ -708,13 +795,16 @@ def _reconcile_tips_and_strains(
708
795
  chart_strain_field: str,
709
796
  tree_strain_field: str,
710
797
  prune_tree_to_chart: bool,
798
+ prune_chart_to_tree: bool,
711
799
  chart_spec: dict,
712
800
  tree_source: Any,
713
801
  ) -> None:
714
802
  """Verify tree strains and chart strains are reconcilable.
715
803
 
716
804
  Three asymmetries:
717
- - chart strains not in tree → always fatal.
805
+ - chart strains not in tree → fatal unless `prune_chart_to_tree=True`
806
+ (in which case the chart spec has already been pre-filtered upstream
807
+ and this set is expected to be empty by the time we get here).
718
808
  - tree tips not in chart → fatal unless `prune_tree_to_chart=True`.
719
809
  - (duplicate tree_strain_field values across tips → handled by the
720
810
  separate `_check_no_duplicate_tip_strains`.)
@@ -728,7 +818,9 @@ def _reconcile_tips_and_strains(
728
818
  chart_minus_tree = chart_set - tree_set
729
819
  tree_minus_chart = tree_set - chart_set
730
820
 
731
- if not chart_minus_tree and (not tree_minus_chart or prune_tree_to_chart):
821
+ chart_ok = not chart_minus_tree
822
+ tree_ok = not tree_minus_chart or prune_tree_to_chart
823
+ if chart_ok and tree_ok:
732
824
  return
733
825
 
734
826
  hints = _candidate_field_hints(
@@ -748,6 +840,7 @@ def _reconcile_tips_and_strains(
748
840
  chart_minus_tree=chart_minus_tree,
749
841
  tree_minus_chart=tree_minus_chart,
750
842
  prune_tree_to_chart=prune_tree_to_chart,
843
+ prune_chart_to_tree=prune_chart_to_tree,
751
844
  hints=hints,
752
845
  )
753
846
  )
@@ -762,14 +855,16 @@ def _format_strain_mismatch(
762
855
  chart_minus_tree: set[str],
763
856
  tree_minus_chart: set[str],
764
857
  prune_tree_to_chart: bool,
858
+ prune_chart_to_tree: bool,
765
859
  hints: list[str],
766
860
  ) -> str:
767
861
  parts: list[str] = []
768
- if chart_minus_tree:
862
+ if chart_minus_tree and not prune_chart_to_tree:
769
863
  parts.append(
770
864
  f"{len(chart_minus_tree)} chart strain(s) are not present in the "
771
- "tree (these would be silently dropped if we pruned, so this is "
772
- "always fatal)."
865
+ "tree. Pass `prune_chart_to_tree=True` to drop the offending "
866
+ "chart rows automatically (use with care — this discards plot "
867
+ "data)."
773
868
  )
774
869
  if tree_minus_chart and not prune_tree_to_chart:
775
870
  parts.append(
@@ -783,7 +878,7 @@ def _format_strain_mismatch(
783
878
  )
784
879
  parts.append("Sample chart_strain_field values: " f"{sorted(chart_strains)[:5]}")
785
880
  parts.append("Sample tree_strain_field values: " f"{sorted(tree_strains)[:5]}")
786
- if chart_minus_tree:
881
+ if chart_minus_tree and not prune_chart_to_tree:
787
882
  parts.append(f"Sample chart-only values: {sorted(chart_minus_tree)[:5]}")
788
883
  if tree_minus_chart and not prune_tree_to_chart:
789
884
  parts.append(f"Sample tree-only values: {sorted(tree_minus_chart)[:5]}")
@@ -263,6 +263,7 @@ def _stack_config_options(command: Any) -> Any:
263
263
  "Save the result as HTML / JSON / PNG / SVG / PDF "
264
264
  "(format is dispatched on --output's extension)."
265
265
  ),
266
+ context_settings={"help_option_names": ["-h", "--help"]},
266
267
  )
267
268
  @click.option(
268
269
  "--tree",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tree-annotated-plot
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: Annotate the axis of an Altair / Vega-Lite plot with a phylogenetic tree.
5
5
  Project-URL: Documentation, https://jbloomlab.github.io/tree-annotated-plot/
6
6
  Project-URL: Homepage, https://github.com/jbloomlab/tree-annotated-plot
@@ -30,10 +30,12 @@ License-File: LICENSE
30
30
  Classifier: License :: OSI Approved :: MIT License
31
31
  Classifier: Operating System :: OS Independent
32
32
  Classifier: Programming Language :: Python :: 3
33
+ Classifier: Programming Language :: Python :: 3.11
34
+ Classifier: Programming Language :: Python :: 3.12
33
35
  Classifier: Programming Language :: Python :: 3.13
34
36
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
35
37
  Classifier: Topic :: Scientific/Engineering :: Visualization
36
- Requires-Python: >=3.13
38
+ Requires-Python: >=3.11
37
39
  Requires-Dist: altair<7,>=6.0
38
40
  Requires-Dist: click>=8.0
39
41
  Requires-Dist: pandas>=2.0
@@ -66,7 +68,7 @@ See [https://jbloomlab.github.io/tree-annotated-plot/](https://jbloomlab.github.
66
68
 
67
69
  ## Installation
68
70
 
69
- Released on [PyPI](https://pypi.org/project/tree-annotated-plot/). Requires Python 3.13+.
71
+ Released on [PyPI](https://pypi.org/project/tree-annotated-plot/). Requires Python 3.11+.
70
72
 
71
73
  ```bash
72
74
  pip install tree-annotated-plot
@@ -0,0 +1,11 @@
1
+ tree_annotated_plot/__init__.py,sha256=R39gjVnl3K2VjLba6zwtwPGADFmtNOE2UYNk1y9_z6k,351
2
+ tree_annotated_plot/_color.py,sha256=nOZp-KIPy6s-6gBkzW36oHrwDk_SmvVNsMiIJ-dY1Io,26519
3
+ tree_annotated_plot/_config.py,sha256=Xs9XfuPAaIhGuffYbsDOfcJW_f4etpgPW4ombHNuIAg,12630
4
+ tree_annotated_plot/_plot.py,sha256=a-sxcZ83Sp-bc0swwisOlvhV7L6bhJQWeh5kppANMX0,76901
5
+ tree_annotated_plot/_tree.py,sha256=Bj-ABt8aooSwV7wAVTbwFGMmCnTPGOKweUWAdfb7aEI,13563
6
+ tree_annotated_plot/cli.py,sha256=OG6Qmzk5BaHUNrfOopX20FbzBN2ahr_SBxHpaCk6Z5c,9848
7
+ tree_annotated_plot-0.3.0.dist-info/METADATA,sha256=oQGwnxJxdq3g_fx_gfn6mjsskj0RHpBcMw2pGcJNZPo,11455
8
+ tree_annotated_plot-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
+ tree_annotated_plot-0.3.0.dist-info/entry_points.txt,sha256=K7edTocRbwmtM2OeZmCy3DLjmY3MR-q2EJmQQq4rj0E,69
10
+ tree_annotated_plot-0.3.0.dist-info/licenses/LICENSE,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
11
+ tree_annotated_plot-0.3.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- tree_annotated_plot/__init__.py,sha256=R39gjVnl3K2VjLba6zwtwPGADFmtNOE2UYNk1y9_z6k,351
2
- tree_annotated_plot/_color.py,sha256=nOZp-KIPy6s-6gBkzW36oHrwDk_SmvVNsMiIJ-dY1Io,26519
3
- tree_annotated_plot/_config.py,sha256=XwokRKY-ytNXelxwuapRKedTBaEedDi_erUp3su7zUs,12498
4
- tree_annotated_plot/_plot.py,sha256=GCdcPyfTlem9YTlPkgdkJGPb5g4w2Vt0fevJcHEo9HI,72720
5
- tree_annotated_plot/_tree.py,sha256=Bj-ABt8aooSwV7wAVTbwFGMmCnTPGOKweUWAdfb7aEI,13563
6
- tree_annotated_plot/cli.py,sha256=yXI5fcLoIoLf6EjgPLmIQIAzDfkVoS5mSZ26_HSHVmk,9786
7
- tree_annotated_plot-0.2.2.dist-info/METADATA,sha256=8yAZMK69Tos2TmV-_Gb359Xm6OlmC8Elzzgzdp3yM04,11353
8
- tree_annotated_plot-0.2.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
9
- tree_annotated_plot-0.2.2.dist-info/entry_points.txt,sha256=K7edTocRbwmtM2OeZmCy3DLjmY3MR-q2EJmQQq4rj0E,69
10
- tree_annotated_plot-0.2.2.dist-info/licenses/LICENSE,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
11
- tree_annotated_plot-0.2.2.dist-info/RECORD,,