tree-annotated-plot 0.2.2__py3-none-any.whl → 0.4.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.
@@ -68,6 +68,15 @@ class PlotConfig:
68
68
  "(x-encoded).",
69
69
  ] = None
70
70
 
71
+ spacing: Annotated[
72
+ int,
73
+ "Pixels of blank space between the tree panel and the chart panel. "
74
+ "Default 0, which butts the two together. Distinct from "
75
+ "`shift_tree_loc`, which moves the tree within its own panel by "
76
+ "resizing the label strip; `spacing` sets the gap between the panels "
77
+ "and applies whether or not `connect_leader_to_label` is on.",
78
+ ] = 0
79
+
71
80
  tree_line_width: Annotated[
72
81
  float,
73
82
  "Stroke width (px) for the tree's branch lines. Default 2.",
@@ -109,9 +118,14 @@ class PlotConfig:
109
118
  "When off (default), tree tips not present in the chart's strain "
110
119
  "set are a fatal error. When on, those tips (and any internal "
111
120
  "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.",
121
+ "with single-child internals collapsed into their kept child.",
122
+ ] = False
123
+
124
+ prune_chart_to_tree: Annotated[
125
+ bool,
126
+ "When off (default), chart strains not present in the tree are a "
127
+ "fatal error. When on, chart rows whose `chart_strain_field` "
128
+ "value isn't a tree tip are filtered out before drawing.",
115
129
  ] = False
116
130
 
117
131
  strict_version: Annotated[
@@ -150,8 +164,9 @@ class PlotConfig:
150
164
  shift_tree_loc: Annotated[
151
165
  int,
152
166
  "Pixels by which to shift the tree toward (positive) or away from "
153
- "(negative) the chart. Default 0. Has no effect when "
154
- "connect_leader_to_label is off.",
167
+ "(negative) the chart, by resizing the label strip inside the tree "
168
+ "panel. Default 0. Has no effect when connect_leader_to_label is off; "
169
+ "to set the gap between the tree and chart panels use `spacing`.",
155
170
  ] = 0
156
171
 
157
172
  color_tree_by: Annotated[
@@ -49,12 +49,14 @@ def plot(
49
49
  branch_length: Literal["div", "num_date"],
50
50
  tree_size: int = 100,
51
51
  tree_location: TreeLocation | None = None,
52
+ spacing: int = 0,
52
53
  tree_line_width: float = 2.0,
53
54
  tree_node_size: float = 45,
54
55
  leader_line_width: float = 1.0,
55
56
  scale_bar: bool = False,
56
57
  branch_length_units: str | None = None,
57
58
  prune_tree_to_chart: bool = False,
59
+ prune_chart_to_tree: bool = False,
58
60
  strict_version: bool = True,
59
61
  connect_leader_to_label: bool = False,
60
62
  strain_label_font_size: float = 10.0,
@@ -76,12 +78,14 @@ def plot(
76
78
  branch_length=branch_length,
77
79
  tree_size=tree_size,
78
80
  tree_location=tree_location,
81
+ spacing=spacing,
79
82
  tree_line_width=tree_line_width,
80
83
  tree_node_size=tree_node_size,
81
84
  leader_line_width=leader_line_width,
82
85
  scale_bar=scale_bar,
83
86
  branch_length_units=branch_length_units,
84
87
  prune_tree_to_chart=prune_tree_to_chart,
88
+ prune_chart_to_tree=prune_chart_to_tree,
85
89
  strict_version=strict_version,
86
90
  connect_leader_to_label=connect_leader_to_label,
87
91
  strain_label_font_size=strain_label_font_size,
@@ -168,12 +172,33 @@ def _build(
168
172
 
169
173
  chart_strains = _extract_chart_strains(spec, axis_hits, config.chart_strain_field)
170
174
 
175
+ if config.prune_chart_to_tree and (set(chart_strains) - set(tip_names)):
176
+ _prune_chart_spec_to_strains(
177
+ spec,
178
+ chart_strain_field=config.chart_strain_field,
179
+ keep_strains=set(tip_names),
180
+ )
181
+ chart = alt.Chart.from_dict(spec)
182
+ chart_strains = _extract_chart_strains(
183
+ spec, axis_hits, config.chart_strain_field
184
+ )
185
+ if not chart_strains:
186
+ raise ValueError(
187
+ "prune_chart_to_tree=True dropped every chart row: no "
188
+ "chart strain matched any tree tip under "
189
+ f"chart_strain_field={config.chart_strain_field!r} / "
190
+ f"tree_strain_field={config.tree_strain_field!r}. Pruning "
191
+ "is meant for charts that bundle a superset of strains; "
192
+ "an empty intersection suggests a wrong field choice."
193
+ )
194
+
171
195
  _reconcile_tips_and_strains(
172
196
  tree_strains=tip_names,
173
197
  chart_strains=chart_strains,
174
198
  chart_strain_field=config.chart_strain_field,
175
199
  tree_strain_field=config.tree_strain_field,
176
200
  prune_tree_to_chart=config.prune_tree_to_chart,
201
+ prune_chart_to_tree=config.prune_chart_to_tree,
177
202
  chart_spec=spec,
178
203
  tree_source=tree,
179
204
  )
@@ -258,7 +283,10 @@ def _build(
258
283
  hoisted_config, hoisted_other = _pop_toplevel_only_attrs(new_chart)
259
284
 
260
285
  combined = _concat_for_location(
261
- tree_chart=tree_chart, user_chart=new_chart, location=location
286
+ tree_chart=tree_chart,
287
+ user_chart=new_chart,
288
+ location=location,
289
+ spacing=config.spacing,
262
290
  )
263
291
  _apply_combined_config(combined, hoisted_config)
264
292
  for k, v in hoisted_other.items():
@@ -297,6 +325,7 @@ def _concat_for_location(
297
325
  tree_chart: alt.TopLevelMixin,
298
326
  user_chart: alt.TopLevelMixin,
299
327
  location: TreeLocation,
328
+ spacing: int,
300
329
  ) -> alt.HConcatChart | alt.VConcatChart:
301
330
  """Concat tree and chart in the order implied by the tree's location.
302
331
 
@@ -310,19 +339,19 @@ def _concat_for_location(
310
339
  user-chart marks whose color values aren't in the tree's domain.
311
340
  """
312
341
  if location == "left":
313
- return alt.hconcat(tree_chart, user_chart, spacing=0).resolve_scale(
342
+ return alt.hconcat(tree_chart, user_chart, spacing=spacing).resolve_scale(
314
343
  y="independent", color="independent"
315
344
  )
316
345
  if location == "right":
317
- return alt.hconcat(user_chart, tree_chart, spacing=0).resolve_scale(
346
+ return alt.hconcat(user_chart, tree_chart, spacing=spacing).resolve_scale(
318
347
  y="independent", color="independent"
319
348
  )
320
349
  if location == "top":
321
- return alt.vconcat(tree_chart, user_chart, spacing=0).resolve_scale(
350
+ return alt.vconcat(tree_chart, user_chart, spacing=spacing).resolve_scale(
322
351
  x="independent", color="independent"
323
352
  )
324
353
  if location == "bottom":
325
- return alt.vconcat(user_chart, tree_chart, spacing=0).resolve_scale(
354
+ return alt.vconcat(user_chart, tree_chart, spacing=spacing).resolve_scale(
326
355
  x="independent", color="independent"
327
356
  )
328
357
  raise ValueError(f"unreachable: tree_location={location!r}")
@@ -641,6 +670,70 @@ def _extract_chart_strains(
641
670
  return _extract_field_values_from_spec_data(spec, chart_strain_field)
642
671
 
643
672
 
673
+ def _prune_chart_spec_to_strains(
674
+ spec: dict, *, chart_strain_field: str, keep_strains: set[str]
675
+ ) -> None:
676
+ """Filter a Vega-Lite spec in place to drop rows outside `keep_strains`.
677
+
678
+ Mutates three kinds of structure:
679
+ - top-level `datasets` entries (each a list of row-dicts).
680
+ - inline `data.values` lists anywhere in the spec tree.
681
+ - explicit `sort` lists on encoding channels bound to
682
+ `chart_strain_field` (any other `sort` is left alone).
683
+
684
+ Rows that don't carry `chart_strain_field` at all are preserved
685
+ (we have no signal to drop them). URL-backed data raises — we
686
+ can't fetch + filter at plot time, mirroring `_extract_chart_strains`.
687
+ """
688
+
689
+ def row_kept(row: Any) -> bool:
690
+ if not isinstance(row, dict):
691
+ return True
692
+ if chart_strain_field not in row:
693
+ return True
694
+ return row[chart_strain_field] in keep_strains
695
+
696
+ datasets = spec.get("datasets") if isinstance(spec, dict) else None
697
+ if isinstance(datasets, dict):
698
+ for name, rows in list(datasets.items()):
699
+ if isinstance(rows, list):
700
+ datasets[name] = [row for row in rows if row_kept(row)]
701
+
702
+ def walk(node: Any) -> None:
703
+ if isinstance(node, dict):
704
+ data = node.get("data")
705
+ if isinstance(data, dict):
706
+ if "url" in data:
707
+ raise ValueError(
708
+ f"chart references data via URL ({data['url']!r}); "
709
+ "URL data is not supported, so prune_chart_to_tree "
710
+ "cannot filter it. Materialize the data inline "
711
+ "(via alt.Chart(df) with a pandas DataFrame) before "
712
+ "saving the chart."
713
+ )
714
+ if "values" in data and isinstance(data["values"], list):
715
+ data["values"] = [row for row in data["values"] if row_kept(row)]
716
+ encoding = node.get("encoding")
717
+ if isinstance(encoding, dict):
718
+ for channel in encoding.values():
719
+ if not isinstance(channel, dict):
720
+ continue
721
+ if channel.get("field") != chart_strain_field:
722
+ continue
723
+ sort = channel.get("sort")
724
+ if isinstance(sort, list):
725
+ channel["sort"] = [s for s in sort if s in keep_strains]
726
+ for k, v in node.items():
727
+ if k in ("data", "datasets"):
728
+ continue
729
+ walk(v)
730
+ elif isinstance(node, list):
731
+ for item in node:
732
+ walk(item)
733
+
734
+ walk(spec)
735
+
736
+
644
737
  def _extract_field_values_from_spec_data(spec: dict, field: str) -> list[str]:
645
738
  """Walk spec for inline / named data and return distinct values of `field`.
646
739
 
@@ -708,13 +801,16 @@ def _reconcile_tips_and_strains(
708
801
  chart_strain_field: str,
709
802
  tree_strain_field: str,
710
803
  prune_tree_to_chart: bool,
804
+ prune_chart_to_tree: bool,
711
805
  chart_spec: dict,
712
806
  tree_source: Any,
713
807
  ) -> None:
714
808
  """Verify tree strains and chart strains are reconcilable.
715
809
 
716
810
  Three asymmetries:
717
- - chart strains not in tree → always fatal.
811
+ - chart strains not in tree → fatal unless `prune_chart_to_tree=True`
812
+ (in which case the chart spec has already been pre-filtered upstream
813
+ and this set is expected to be empty by the time we get here).
718
814
  - tree tips not in chart → fatal unless `prune_tree_to_chart=True`.
719
815
  - (duplicate tree_strain_field values across tips → handled by the
720
816
  separate `_check_no_duplicate_tip_strains`.)
@@ -728,7 +824,9 @@ def _reconcile_tips_and_strains(
728
824
  chart_minus_tree = chart_set - tree_set
729
825
  tree_minus_chart = tree_set - chart_set
730
826
 
731
- if not chart_minus_tree and (not tree_minus_chart or prune_tree_to_chart):
827
+ chart_ok = not chart_minus_tree
828
+ tree_ok = not tree_minus_chart or prune_tree_to_chart
829
+ if chart_ok and tree_ok:
732
830
  return
733
831
 
734
832
  hints = _candidate_field_hints(
@@ -748,6 +846,7 @@ def _reconcile_tips_and_strains(
748
846
  chart_minus_tree=chart_minus_tree,
749
847
  tree_minus_chart=tree_minus_chart,
750
848
  prune_tree_to_chart=prune_tree_to_chart,
849
+ prune_chart_to_tree=prune_chart_to_tree,
751
850
  hints=hints,
752
851
  )
753
852
  )
@@ -762,14 +861,16 @@ def _format_strain_mismatch(
762
861
  chart_minus_tree: set[str],
763
862
  tree_minus_chart: set[str],
764
863
  prune_tree_to_chart: bool,
864
+ prune_chart_to_tree: bool,
765
865
  hints: list[str],
766
866
  ) -> str:
767
867
  parts: list[str] = []
768
- if chart_minus_tree:
868
+ if chart_minus_tree and not prune_chart_to_tree:
769
869
  parts.append(
770
870
  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)."
871
+ "tree. Pass `prune_chart_to_tree=True` to drop the offending "
872
+ "chart rows automatically (use with care — this discards plot "
873
+ "data)."
773
874
  )
774
875
  if tree_minus_chart and not prune_tree_to_chart:
775
876
  parts.append(
@@ -783,7 +884,7 @@ def _format_strain_mismatch(
783
884
  )
784
885
  parts.append("Sample chart_strain_field values: " f"{sorted(chart_strains)[:5]}")
785
886
  parts.append("Sample tree_strain_field values: " f"{sorted(tree_strains)[:5]}")
786
- if chart_minus_tree:
887
+ if chart_minus_tree and not prune_chart_to_tree:
787
888
  parts.append(f"Sample chart-only values: {sorted(chart_minus_tree)[:5]}")
788
889
  if tree_minus_chart and not prune_tree_to_chart:
789
890
  parts.append(f"Sample tree-only values: {sorted(tree_minus_chart)[:5]}")
@@ -1380,6 +1481,11 @@ def _build_scale_bar_layer(
1380
1481
 
1381
1482
  _LABEL_PAD_PX_MIN = 4
1382
1483
  _LABEL_PAD_RATIO = 0.4 # `LABEL_PAD_PX = max(MIN, font_size * RATIO)`
1484
+ # Gap between the labels and the chart, kept separate from the tree-facing pad
1485
+ # above because it clears the chart's frame rather than the deepest tip, and so
1486
+ # wants a small fixed clearance rather than the pad's larger, faster-growing one.
1487
+ _LABEL_CHART_GAP_PX_MIN = 3
1488
+ _LABEL_CHART_GAP_RATIO = 0.2 # `LABEL_CHART_GAP_PX = max(MIN, font_size * RATIO)`
1383
1489
  _LABEL_CHAR_PX_RATIO = 0.6 # rough proportional sans-serif glyph-width estimate
1384
1490
  _LABEL_HALO_RATIO = 0.6 # white-halo strokeWidth as a fraction of font_size
1385
1491
 
@@ -1519,9 +1625,15 @@ def _build_tree_chart(
1519
1625
  max_name_len = max((len(n) for n in names), default=0)
1520
1626
  char_px = strain_label_font_size * _LABEL_CHAR_PX_RATIO
1521
1627
  label_pad_px = max(_LABEL_PAD_PX_MIN, strain_label_font_size * _LABEL_PAD_RATIO)
1628
+ chart_gap_px = max(
1629
+ _LABEL_CHART_GAP_PX_MIN, strain_label_font_size * _LABEL_CHART_GAP_RATIO
1630
+ )
1522
1631
  # Strip needs to fit the longest label plus `halo_px / 2` of halo
1523
- # extension on the leader-facing side, plus a small fixed pad.
1524
- label_pixel_width = label_pad_px + max_name_len * char_px + halo_px / 2
1632
+ # extension on the leader-facing side, a pad between the tree and the
1633
+ # text, and a smaller gap between the text and the chart.
1634
+ label_pixel_width = (
1635
+ label_pad_px + chart_gap_px + max_name_len * char_px + halo_px / 2
1636
+ )
1525
1637
  strip_pixel_width = label_pixel_width - shift_tree_loc
1526
1638
  if strip_pixel_width <= 0:
1527
1639
  raise ValueError(
@@ -1534,9 +1646,13 @@ def _build_tree_chart(
1534
1646
  per_pixel = branch_span / tree_size if tree_size else 0.0
1535
1647
  extra_branch_units = strip_pixel_width * per_pixel
1536
1648
  chart_edge_branch = branch_max + extra_branch_units
1537
- tips_df = tips_df.assign(x_label=chart_edge_branch)
1538
- leader_df = tips_df[tips_df["x"] < chart_edge_branch].assign(
1539
- x2=chart_edge_branch
1649
+ # Anchor the labels `chart_gap_px` *inside* the chart-facing edge rather
1650
+ # than on it, so the text does not run up against the chart's frame. The
1651
+ # leaders stop at the same place, where the text begins.
1652
+ label_anchor_branch = chart_edge_branch - chart_gap_px * per_pixel
1653
+ tips_df = tips_df.assign(x_label=label_anchor_branch)
1654
+ leader_df = tips_df[tips_df["x"] < label_anchor_branch].assign(
1655
+ x2=label_anchor_branch
1540
1656
  )
1541
1657
  extended_tree_size = tree_size + strip_pixel_width
1542
1658
  else:
@@ -1585,9 +1701,9 @@ def _build_tree_chart(
1585
1701
  # When connect_leader_to_label is on, the branch domain is extended
1586
1702
  # past `branch_max` to `chart_edge_branch` so the label strip has
1587
1703
  # data-units to occupy; tips at `branch_max` still sit at pixel
1588
- # `tree_size` on the panel. Each label's chart-facing edge is
1589
- # anchored at `chart_edge_branch` and aligned outward (right for
1590
- # tree on the left, left for tree on the right).
1704
+ # `tree_size` on the panel. Each label's chart-facing edge sits
1705
+ # `chart_gap_px` inside `chart_edge_branch`, aligned outward (right
1706
+ # for tree on the left, left for tree on the right).
1591
1707
  if tree_location == "left":
1592
1708
  branch_domain = [branch_min, chart_edge_branch]
1593
1709
  text_align = "right"
@@ -1688,7 +1804,8 @@ def _build_tree_chart(
1688
1804
  # strip at bottom, opposite the chart above).
1689
1805
  # tree_location="bottom" → root at bottom → branch_max at top (label
1690
1806
  # strip at top, opposite the chart below).
1691
- # Each label's chart-facing edge is anchored at `chart_edge_branch`.
1807
+ # Each label's chart-facing edge sits `chart_gap_px` inside
1808
+ # `chart_edge_branch`.
1692
1809
  # The text mark is rotated 270° (reads bottom-to-top), which maps
1693
1810
  # pre-rotation `align="right"` to a top anchor (text extends down)
1694
1811
  # and `align="left"` to a bottom anchor (text extends up). For tree
@@ -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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: tree-annotated-plot
3
- Version: 0.2.2
3
+ Version: 0.4.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
@@ -41,7 +43,7 @@ Requires-Dist: vl-convert-python>=1.9
41
43
  Provides-Extra: dev
42
44
  Requires-Dist: black>=24; extra == 'dev'
43
45
  Requires-Dist: pytest>=8; extra == 'dev'
44
- Requires-Dist: ruff>=0.6; extra == 'dev'
46
+ Requires-Dist: ruff<0.17,>=0.16; extra == 'dev'
45
47
  Provides-Extra: docs
46
48
  Requires-Dist: mkdocs-click>=0.8; extra == 'docs'
47
49
  Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
@@ -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=0ib600JaMpMz4ZjFYtvFu3nhUhliJRexTJcFpDKVOD0,13171
4
+ tree_annotated_plot/_plot.py,sha256=pqhR9PgQUML2-taP_k1FEidpIdBgJ6tulMEpFJbD8WI,77969
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.4.0.dist-info/METADATA,sha256=JQNY2-mmZIQA7TjABRX_YkvoZXxqix-Ixs5n23EQ1FY,11462
8
+ tree_annotated_plot-0.4.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ tree_annotated_plot-0.4.0.dist-info/entry_points.txt,sha256=K7edTocRbwmtM2OeZmCy3DLjmY3MR-q2EJmQQq4rj0E,69
10
+ tree_annotated_plot-0.4.0.dist-info/licenses/LICENSE,sha256=UIp30ue1HZit7tMmSK0SS3swJBqOcLLnLJn5LY5YdNE,1036
11
+ tree_annotated_plot-0.4.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.32.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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,,