From 5587b2696096f82bf7bd3148c4efb295e81b87c5 Mon Sep 17 00:00:00 2001 From: Yan Wong Date: Sun, 6 Oct 2024 23:33:29 +0100 Subject: [PATCH] Allow multi-sample tips for Tree.draw_svg(), created by passing a different postorder This additional use of "order" is deliberately not documented. --- python/tests/data/svg/internal_sample_ts.svg | 80 ++++---- python/tests/data/svg/tree.svg | 4 +- python/tests/data/svg/tree_both_axes.svg | 4 +- python/tests/data/svg/tree_muts.svg | 6 +- python/tests/data/svg/tree_muts_all_edge.svg | 6 +- .../tests/data/svg/tree_simple_collapsed.svg | 48 +++++ python/tests/data/svg/tree_subtree.svg | 44 +++++ .../data/svg/tree_subtrees_with_collapsed.svg | 110 +++++++++++ python/tests/data/svg/tree_timed_muts.svg | 6 +- python/tests/data/svg/tree_x_axis.svg | 6 +- python/tests/data/svg/tree_y_axis_rank.svg | 6 +- python/tests/data/svg/ts.svg | 30 +-- python/tests/data/svg/ts_max_trees.svg | 40 ++-- .../tests/data/svg/ts_max_trees_treewise.svg | 40 ++-- python/tests/data/svg/ts_multiroot.svg | 174 ++++++++--------- python/tests/data/svg/ts_mut_highlight.svg | 30 +-- python/tests/data/svg/ts_mut_times.svg | 30 +-- .../tests/data/svg/ts_mut_times_logscale.svg | 30 +-- python/tests/data/svg/ts_mut_times_titles.svg | 30 +-- python/tests/data/svg/ts_no_axes.svg | 30 +-- python/tests/data/svg/ts_plain.svg | 30 +-- python/tests/data/svg/ts_plain_no_xlab.svg | 30 +-- python/tests/data/svg/ts_plain_y.svg | 30 +-- python/tests/data/svg/ts_rank.svg | 30 +-- python/tests/data/svg/ts_x_lim.svg | 18 +- python/tests/data/svg/ts_xlabel.svg | 30 +-- python/tests/data/svg/ts_y_axis.svg | 30 +-- python/tests/data/svg/ts_y_axis_log.svg | 30 +-- python/tests/data/svg/ts_y_axis_regular.svg | 30 +-- python/tests/test_drawing.py | 80 +++++++- python/tskit/drawing.py | 178 +++++++++++++----- python/tskit/trees.py | 2 +- 32 files changed, 809 insertions(+), 463 deletions(-) create mode 100644 python/tests/data/svg/tree_simple_collapsed.svg create mode 100644 python/tests/data/svg/tree_subtree.svg create mode 100644 python/tests/data/svg/tree_subtrees_with_collapsed.svg diff --git a/python/tests/data/svg/internal_sample_ts.svg b/python/tests/data/svg/internal_sample_ts.svg index fc9c062b3c..4843cd70c4 100644 --- a/python/tests/data/svg/internal_sample_ts.svg +++ b/python/tests/data/svg/internal_sample_ts.svg @@ -99,16 +99,6 @@ - - - - 7 - - - - - 8 - @@ -123,7 +113,7 @@ - 4 + 4 @@ -143,7 +133,7 @@ 2 - 5 + 5 @@ -157,7 +147,17 @@ 1 - 9 + 9 + + + + + 7 + + + + + 8 @@ -187,7 +187,7 @@ 4 - 4 + 4 @@ -207,7 +207,7 @@ - 5 + 5 @@ -216,7 +216,7 @@ 5 - 7 + 7 @@ -227,16 +227,6 @@ - - - - 7 - - - - - 8 - @@ -251,7 +241,7 @@ - 4 + 4 @@ -266,11 +256,21 @@ - 5 + 5 - 6 + 6 + + + + + 7 + + + + + 8 @@ -290,7 +290,7 @@ - 4 + 4 @@ -310,11 +310,11 @@ - 5 + 5 - 7 + 7 @@ -325,11 +325,6 @@ - - - - 7 - @@ -344,7 +339,7 @@ - 4 + 4 @@ -359,11 +354,16 @@ - 5 + 5 - 8 + 8 + + + + + 7 diff --git a/python/tests/data/svg/tree.svg b/python/tests/data/svg/tree.svg index f578dcb89c..242cec60b3 100644 --- a/python/tests/data/svg/tree.svg +++ b/python/tests/data/svg/tree.svg @@ -19,7 +19,7 @@ - 4 + 4 @@ -34,7 +34,7 @@ - 5 + 5 8 diff --git a/python/tests/data/svg/tree_both_axes.svg b/python/tests/data/svg/tree_both_axes.svg index c0f14edcdf..c51a5e0bc8 100644 --- a/python/tests/data/svg/tree_both_axes.svg +++ b/python/tests/data/svg/tree_both_axes.svg @@ -73,7 +73,7 @@ - 4 + 4 @@ -88,7 +88,7 @@ - 5 + 5 8 diff --git a/python/tests/data/svg/tree_muts.svg b/python/tests/data/svg/tree_muts.svg index f02ef51002..d5e0ecaaa7 100644 --- a/python/tests/data/svg/tree_muts.svg +++ b/python/tests/data/svg/tree_muts.svg @@ -19,7 +19,7 @@ - 4 + 4 @@ -39,7 +39,7 @@ 2 - 5 + 5 @@ -53,7 +53,7 @@ 1 - 9 + 9 diff --git a/python/tests/data/svg/tree_muts_all_edge.svg b/python/tests/data/svg/tree_muts_all_edge.svg index 5e69e262d2..368d06020d 100644 --- a/python/tests/data/svg/tree_muts_all_edge.svg +++ b/python/tests/data/svg/tree_muts_all_edge.svg @@ -81,7 +81,7 @@ 4 - 4 + 4 @@ -106,7 +106,7 @@ - 5 + 5 @@ -115,7 +115,7 @@ 5 - 7 + 7 diff --git a/python/tests/data/svg/tree_simple_collapsed.svg b/python/tests/data/svg/tree_simple_collapsed.svg new file mode 100644 index 0000000000..9299313dc3 --- /dev/null +++ b/python/tests/data/svg/tree_simple_collapsed.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + +2 + 8 + + + + + + 2 + + + + + 3 + + + + 9 + + + + 10 + + + + + + +4 + 13 + + + 14 + + + + diff --git a/python/tests/data/svg/tree_subtree.svg b/python/tests/data/svg/tree_subtree.svg new file mode 100644 index 0000000000..76a80e404c --- /dev/null +++ b/python/tests/data/svg/tree_subtree.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + 0 + + + + + 1 + + + + 8 + + + + + + 2 + + + + + 3 + + + + 9 + + + 10 + + + + diff --git a/python/tests/data/svg/tree_subtrees_with_collapsed.svg b/python/tests/data/svg/tree_subtrees_with_collapsed.svg new file mode 100644 index 0000000000..3effa996ea --- /dev/null +++ b/python/tests/data/svg/tree_subtrees_with_collapsed.svg @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + +2 + 16 + + + + + + 2 + + + + + 3 + + + + 17 + + + + 18 + + + + + + + 4 + + + + + 5 + + + + 19 + + + + + + 6 + + + + + 7 + + + + 20 + + + + 21 + + + 22 + + + + + + + 8 + + + + + 9 + + + + 23 + + + + + + 10 + + + + + 11 + + + + 24 + + + 25 + + + + diff --git a/python/tests/data/svg/tree_timed_muts.svg b/python/tests/data/svg/tree_timed_muts.svg index d5b356fff7..27e1c8aaee 100644 --- a/python/tests/data/svg/tree_timed_muts.svg +++ b/python/tests/data/svg/tree_timed_muts.svg @@ -19,7 +19,7 @@ - 4 + 4 @@ -39,7 +39,7 @@ 2 - 5 + 5 @@ -53,7 +53,7 @@ 1 - 9 + 9 diff --git a/python/tests/data/svg/tree_x_axis.svg b/python/tests/data/svg/tree_x_axis.svg index 66492b4d84..4a6cca8e30 100644 --- a/python/tests/data/svg/tree_x_axis.svg +++ b/python/tests/data/svg/tree_x_axis.svg @@ -72,7 +72,7 @@ 4 - 4 + 4 @@ -92,7 +92,7 @@ - 5 + 5 @@ -101,7 +101,7 @@ 5 - 7 + 7 diff --git a/python/tests/data/svg/tree_y_axis_rank.svg b/python/tests/data/svg/tree_y_axis_rank.svg index fc72850c02..9eb3b49d03 100644 --- a/python/tests/data/svg/tree_y_axis_rank.svg +++ b/python/tests/data/svg/tree_y_axis_rank.svg @@ -67,7 +67,7 @@ 4 - 4 + 4 @@ -87,7 +87,7 @@ - 5 + 5 @@ -96,7 +96,7 @@ 5 - 7 + 7 diff --git a/python/tests/data/svg/ts.svg b/python/tests/data/svg/ts.svg index 71ba72fd55..13bc8db1cb 100644 --- a/python/tests/data/svg/ts.svg +++ b/python/tests/data/svg/ts.svg @@ -113,7 +113,7 @@ - 4 + 4 @@ -133,7 +133,7 @@ 2 - 5 + 5 @@ -147,7 +147,7 @@ 1 - 9 + 9 @@ -177,7 +177,7 @@ 4 - 4 + 4 @@ -197,7 +197,7 @@ - 5 + 5 @@ -206,7 +206,7 @@ 5 - 7 + 7 @@ -226,7 +226,7 @@ - 4 + 4 @@ -241,11 +241,11 @@ - 5 + 5 - 6 + 6 @@ -265,7 +265,7 @@ - 4 + 4 @@ -285,11 +285,11 @@ - 5 + 5 - 7 + 7 @@ -309,7 +309,7 @@ - 4 + 4 @@ -324,11 +324,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_max_trees.svg b/python/tests/data/svg/ts_max_trees.svg index ad7ab7f392..c50331492d 100644 --- a/python/tests/data/svg/ts_max_trees.svg +++ b/python/tests/data/svg/ts_max_trees.svg @@ -157,11 +157,11 @@ - 7 + 7 - 11 + 11 @@ -182,11 +182,11 @@ - 8 + 8 - 16 + 16 33 @@ -215,11 +215,11 @@ - 7 + 7 - 11 + 11 @@ -240,11 +240,11 @@ - 8 + 8 - 16 + 16 25 @@ -273,11 +273,11 @@ - 7 + 7 - 11 + 11 @@ -303,11 +303,11 @@ 1 - 8 + 8 - 16 + 16 30 @@ -357,15 +357,15 @@ - 6 + 6 - 7 + 7 - 10 + 10 @@ -380,7 +380,7 @@ - 15 + 15 42 @@ -415,11 +415,11 @@ - 6 + 6 - 7 + 7 @@ -428,7 +428,7 @@ 9 - 10 + 10 @@ -443,7 +443,7 @@ - 15 + 15 39 diff --git a/python/tests/data/svg/ts_max_trees_treewise.svg b/python/tests/data/svg/ts_max_trees_treewise.svg index 519d7f9ee4..055c1f3eaf 100644 --- a/python/tests/data/svg/ts_max_trees_treewise.svg +++ b/python/tests/data/svg/ts_max_trees_treewise.svg @@ -131,11 +131,11 @@ - 7 + 7 - 11 + 11 @@ -156,11 +156,11 @@ - 8 + 8 - 16 + 16 33 @@ -189,11 +189,11 @@ - 7 + 7 - 11 + 11 @@ -214,11 +214,11 @@ - 8 + 8 - 16 + 16 25 @@ -247,11 +247,11 @@ - 7 + 7 - 11 + 11 @@ -277,11 +277,11 @@ 1 - 8 + 8 - 16 + 16 30 @@ -331,15 +331,15 @@ - 6 + 6 - 7 + 7 - 10 + 10 @@ -354,7 +354,7 @@ - 15 + 15 42 @@ -389,11 +389,11 @@ - 6 + 6 - 7 + 7 @@ -402,7 +402,7 @@ 9 - 10 + 10 @@ -417,7 +417,7 @@ - 15 + 15 39 diff --git a/python/tests/data/svg/ts_multiroot.svg b/python/tests/data/svg/ts_multiroot.svg index 207ca6bf1b..0d8dd1f35a 100644 --- a/python/tests/data/svg/ts_multiroot.svg +++ b/python/tests/data/svg/ts_multiroot.svg @@ -229,7 +229,7 @@ 2 - 6 + 6 @@ -244,7 +244,7 @@ - 11 + 11 15 @@ -253,25 +253,6 @@ - - - - - 2 - - - - - 4 - - - - - 5 - - - 6 - @@ -296,11 +277,30 @@ - 11 + 11 12 + + + + + 2 + + + + + 4 + + + + + 5 + + + 6 + @@ -333,7 +333,7 @@ - 6 + 6 @@ -348,7 +348,7 @@ - 11 + 11 14 @@ -380,7 +380,7 @@ - 6 + 6 @@ -395,7 +395,7 @@ - 7 + 7 14 @@ -404,20 +404,6 @@ - - - - - 1 - - - - - 3 - - - 7 - @@ -453,20 +439,16 @@ - 6 + 6 - 10 + 10 13 - - - - - + @@ -480,6 +462,10 @@ 7 + + + + @@ -494,6 +480,20 @@ 13 + + + + + 1 + + + + + 3 + + + 7 + @@ -512,34 +512,6 @@ - - - - - 2 - - - - - - - 7 - - - 4 - - - - - 3 - - - 6 - - - - 1 - @@ -559,23 +531,23 @@ 8 - - - - + + + 1 + - + - - - - 9 - 2 - + + + + + 7 + 4 @@ -587,6 +559,10 @@ 6 + + + + @@ -606,11 +582,35 @@ - 8 + 8 9 + + + + + + + 9 + + + 2 + + + + + 4 + + + + + 3 + + + 6 + diff --git a/python/tests/data/svg/ts_mut_highlight.svg b/python/tests/data/svg/ts_mut_highlight.svg index 836a8bd617..54935aa939 100644 --- a/python/tests/data/svg/ts_mut_highlight.svg +++ b/python/tests/data/svg/ts_mut_highlight.svg @@ -113,7 +113,7 @@ - 4 + 4 @@ -133,7 +133,7 @@ 2 - 5 + 5 @@ -147,7 +147,7 @@ 1 - 9 + 9 @@ -177,7 +177,7 @@ 4 - 4 + 4 @@ -197,7 +197,7 @@ - 5 + 5 @@ -206,7 +206,7 @@ 5 - 7 + 7 @@ -226,7 +226,7 @@ - 4 + 4 @@ -241,11 +241,11 @@ - 5 + 5 - 6 + 6 @@ -265,7 +265,7 @@ - 4 + 4 @@ -285,11 +285,11 @@ - 5 + 5 - 7 + 7 @@ -309,7 +309,7 @@ - 4 + 4 @@ -324,11 +324,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_mut_times.svg b/python/tests/data/svg/ts_mut_times.svg index 5dce0fff66..a72e9c7a9c 100644 --- a/python/tests/data/svg/ts_mut_times.svg +++ b/python/tests/data/svg/ts_mut_times.svg @@ -113,7 +113,7 @@ - 4 + 4 @@ -133,7 +133,7 @@ 2 - 5 + 5 @@ -147,7 +147,7 @@ 1 - 9 + 9 @@ -177,7 +177,7 @@ 4 - 4 + 4 @@ -197,7 +197,7 @@ - 5 + 5 @@ -206,7 +206,7 @@ 5 - 7 + 7 @@ -226,7 +226,7 @@ - 4 + 4 @@ -241,11 +241,11 @@ - 5 + 5 - 6 + 6 @@ -265,7 +265,7 @@ - 4 + 4 @@ -285,11 +285,11 @@ - 5 + 5 - 7 + 7 @@ -309,7 +309,7 @@ - 4 + 4 @@ -324,11 +324,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_mut_times_logscale.svg b/python/tests/data/svg/ts_mut_times_logscale.svg index f2e576324e..a269eab0c2 100644 --- a/python/tests/data/svg/ts_mut_times_logscale.svg +++ b/python/tests/data/svg/ts_mut_times_logscale.svg @@ -113,7 +113,7 @@ - 4 + 4 @@ -133,7 +133,7 @@ 2 - 5 + 5 @@ -147,7 +147,7 @@ 1 - 9 + 9 @@ -177,7 +177,7 @@ 4 - 4 + 4 @@ -197,7 +197,7 @@ - 5 + 5 @@ -206,7 +206,7 @@ 5 - 7 + 7 @@ -226,7 +226,7 @@ - 4 + 4 @@ -241,11 +241,11 @@ - 5 + 5 - 6 + 6 @@ -265,7 +265,7 @@ - 4 + 4 @@ -285,11 +285,11 @@ - 5 + 5 - 7 + 7 @@ -309,7 +309,7 @@ - 4 + 4 @@ -324,11 +324,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_mut_times_titles.svg b/python/tests/data/svg/ts_mut_times_titles.svg index f4ec6bc226..78aea0d3e1 100644 --- a/python/tests/data/svg/ts_mut_times_titles.svg +++ b/python/tests/data/svg/ts_mut_times_titles.svg @@ -135,7 +135,7 @@ NoDe4! - 4 + 4 @@ -163,7 +163,7 @@ NoDe5! - 5 + 5 @@ -183,7 +183,7 @@ NoDe9! - 9 + 9 @@ -223,7 +223,7 @@ NoDe4! - 4 + 4 @@ -251,7 +251,7 @@ NoDe5! - 5 + 5 @@ -264,7 +264,7 @@ NoDe7! - 7 + 7 @@ -290,7 +290,7 @@ NoDe4! - 4 + 4 @@ -311,13 +311,13 @@ NoDe5! - 5 + 5 NoDe6! - 6 + 6 @@ -343,7 +343,7 @@ NoDe4! - 4 + 4 @@ -371,13 +371,13 @@ NoDe5! - 5 + 5 NoDe7! - 7 + 7 @@ -403,7 +403,7 @@ NoDe4! - 4 + 4 @@ -424,13 +424,13 @@ NoDe5! - 5 + 5 NoDe8! - 8 + 8 diff --git a/python/tests/data/svg/ts_no_axes.svg b/python/tests/data/svg/ts_no_axes.svg index 9897f1bcb8..5ce4804a63 100644 --- a/python/tests/data/svg/ts_no_axes.svg +++ b/python/tests/data/svg/ts_no_axes.svg @@ -21,7 +21,7 @@ - 4 + 4 @@ -41,7 +41,7 @@ 2 - 5 + 5 @@ -55,7 +55,7 @@ 1 - 9 + 9 @@ -85,7 +85,7 @@ 4 - 4 + 4 @@ -105,7 +105,7 @@ - 5 + 5 @@ -114,7 +114,7 @@ 5 - 7 + 7 @@ -134,7 +134,7 @@ - 4 + 4 @@ -149,11 +149,11 @@ - 5 + 5 - 6 + 6 @@ -173,7 +173,7 @@ - 4 + 4 @@ -193,11 +193,11 @@ - 5 + 5 - 7 + 7 @@ -217,7 +217,7 @@ - 4 + 4 @@ -232,11 +232,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_plain.svg b/python/tests/data/svg/ts_plain.svg index bedd75adf6..a5c01e3e0e 100644 --- a/python/tests/data/svg/ts_plain.svg +++ b/python/tests/data/svg/ts_plain.svg @@ -67,7 +67,7 @@ - 4 + 4 @@ -87,7 +87,7 @@ 2 - 5 + 5 @@ -101,7 +101,7 @@ 1 - 9 + 9 @@ -131,7 +131,7 @@ 4 - 4 + 4 @@ -151,7 +151,7 @@ - 5 + 5 @@ -160,7 +160,7 @@ 5 - 7 + 7 @@ -180,7 +180,7 @@ - 4 + 4 @@ -195,11 +195,11 @@ - 5 + 5 - 6 + 6 @@ -219,7 +219,7 @@ - 4 + 4 @@ -239,11 +239,11 @@ - 5 + 5 - 7 + 7 @@ -263,7 +263,7 @@ - 4 + 4 @@ -278,11 +278,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_plain_no_xlab.svg b/python/tests/data/svg/ts_plain_no_xlab.svg index 4a4cc4e348..ab4cc34cdf 100644 --- a/python/tests/data/svg/ts_plain_no_xlab.svg +++ b/python/tests/data/svg/ts_plain_no_xlab.svg @@ -64,7 +64,7 @@ - 4 + 4 @@ -84,7 +84,7 @@ 2 - 5 + 5 @@ -98,7 +98,7 @@ 1 - 9 + 9 @@ -128,7 +128,7 @@ 4 - 4 + 4 @@ -148,7 +148,7 @@ - 5 + 5 @@ -157,7 +157,7 @@ 5 - 7 + 7 @@ -177,7 +177,7 @@ - 4 + 4 @@ -192,11 +192,11 @@ - 5 + 5 - 6 + 6 @@ -216,7 +216,7 @@ - 4 + 4 @@ -236,11 +236,11 @@ - 5 + 5 - 7 + 7 @@ -260,7 +260,7 @@ - 4 + 4 @@ -275,11 +275,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_plain_y.svg b/python/tests/data/svg/ts_plain_y.svg index 9a63f2d30a..72f704f22b 100644 --- a/python/tests/data/svg/ts_plain_y.svg +++ b/python/tests/data/svg/ts_plain_y.svg @@ -96,7 +96,7 @@ - 4 + 4 @@ -116,7 +116,7 @@ 2 - 5 + 5 @@ -130,7 +130,7 @@ 1 - 9 + 9 @@ -160,7 +160,7 @@ 4 - 4 + 4 @@ -180,7 +180,7 @@ - 5 + 5 @@ -189,7 +189,7 @@ 5 - 7 + 7 @@ -209,7 +209,7 @@ - 4 + 4 @@ -224,11 +224,11 @@ - 5 + 5 - 6 + 6 @@ -248,7 +248,7 @@ - 4 + 4 @@ -268,11 +268,11 @@ - 5 + 5 - 7 + 7 @@ -292,7 +292,7 @@ - 4 + 4 @@ -307,11 +307,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_rank.svg b/python/tests/data/svg/ts_rank.svg index ac3c558d13..de9f9390e7 100644 --- a/python/tests/data/svg/ts_rank.svg +++ b/python/tests/data/svg/ts_rank.svg @@ -163,7 +163,7 @@ - 4 + 4 @@ -183,7 +183,7 @@ 2 - 5 + 5 @@ -197,7 +197,7 @@ 1 - 9 + 9 @@ -227,7 +227,7 @@ 4 - 4 + 4 @@ -247,7 +247,7 @@ - 5 + 5 @@ -256,7 +256,7 @@ 5 - 7 + 7 @@ -276,7 +276,7 @@ - 4 + 4 @@ -291,11 +291,11 @@ - 5 + 5 - 6 + 6 @@ -315,7 +315,7 @@ - 4 + 4 @@ -335,11 +335,11 @@ - 5 + 5 - 7 + 7 @@ -359,7 +359,7 @@ - 4 + 4 @@ -374,11 +374,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_x_lim.svg b/python/tests/data/svg/ts_x_lim.svg index 0bcdfc48df..dcc6c63032 100644 --- a/python/tests/data/svg/ts_x_lim.svg +++ b/python/tests/data/svg/ts_x_lim.svg @@ -69,7 +69,7 @@ - 4 + 4 @@ -84,11 +84,11 @@ - 5 + 5 - 9 + 9 @@ -118,7 +118,7 @@ 4 - 4 + 4 @@ -138,7 +138,7 @@ - 5 + 5 @@ -147,7 +147,7 @@ 5 - 7 + 7 @@ -167,7 +167,7 @@ - 4 + 4 @@ -182,11 +182,11 @@ - 5 + 5 - 6 + 6 diff --git a/python/tests/data/svg/ts_xlabel.svg b/python/tests/data/svg/ts_xlabel.svg index f83f3aeb0b..3bf4c97858 100644 --- a/python/tests/data/svg/ts_xlabel.svg +++ b/python/tests/data/svg/ts_xlabel.svg @@ -113,7 +113,7 @@ - 4 + 4 @@ -133,7 +133,7 @@ 2 - 5 + 5 @@ -147,7 +147,7 @@ 1 - 9 + 9 @@ -177,7 +177,7 @@ 4 - 4 + 4 @@ -197,7 +197,7 @@ - 5 + 5 @@ -206,7 +206,7 @@ 5 - 7 + 7 @@ -226,7 +226,7 @@ - 4 + 4 @@ -241,11 +241,11 @@ - 5 + 5 - 6 + 6 @@ -265,7 +265,7 @@ - 4 + 4 @@ -285,11 +285,11 @@ - 5 + 5 - 7 + 7 @@ -309,7 +309,7 @@ - 4 + 4 @@ -324,11 +324,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_y_axis.svg b/python/tests/data/svg/ts_y_axis.svg index ed0b57c2af..6e4cefac15 100644 --- a/python/tests/data/svg/ts_y_axis.svg +++ b/python/tests/data/svg/ts_y_axis.svg @@ -163,7 +163,7 @@ - 4 + 4 @@ -183,7 +183,7 @@ 2 - 5 + 5 @@ -197,7 +197,7 @@ 1 - 9 + 9 @@ -227,7 +227,7 @@ 4 - 4 + 4 @@ -247,7 +247,7 @@ - 5 + 5 @@ -256,7 +256,7 @@ 5 - 7 + 7 @@ -276,7 +276,7 @@ - 4 + 4 @@ -291,11 +291,11 @@ - 5 + 5 - 6 + 6 @@ -315,7 +315,7 @@ - 4 + 4 @@ -335,11 +335,11 @@ - 5 + 5 - 7 + 7 @@ -359,7 +359,7 @@ - 4 + 4 @@ -374,11 +374,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_y_axis_log.svg b/python/tests/data/svg/ts_y_axis_log.svg index 57a48517cd..9b60d4e6ae 100644 --- a/python/tests/data/svg/ts_y_axis_log.svg +++ b/python/tests/data/svg/ts_y_axis_log.svg @@ -163,7 +163,7 @@ - 4 + 4 @@ -183,7 +183,7 @@ 2 - 5 + 5 @@ -197,7 +197,7 @@ 1 - 9 + 9 @@ -227,7 +227,7 @@ 4 - 4 + 4 @@ -247,7 +247,7 @@ - 5 + 5 @@ -256,7 +256,7 @@ 5 - 7 + 7 @@ -276,7 +276,7 @@ - 4 + 4 @@ -291,11 +291,11 @@ - 5 + 5 - 6 + 6 @@ -315,7 +315,7 @@ - 4 + 4 @@ -335,11 +335,11 @@ - 5 + 5 - 7 + 7 @@ -359,7 +359,7 @@ - 4 + 4 @@ -374,11 +374,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/data/svg/ts_y_axis_regular.svg b/python/tests/data/svg/ts_y_axis_regular.svg index 95b1c9b160..d90bd53a7b 100644 --- a/python/tests/data/svg/ts_y_axis_regular.svg +++ b/python/tests/data/svg/ts_y_axis_regular.svg @@ -191,7 +191,7 @@ - 4 + 4 @@ -211,7 +211,7 @@ 2 - 5 + 5 @@ -225,7 +225,7 @@ 1 - 9 + 9 @@ -255,7 +255,7 @@ 4 - 4 + 4 @@ -275,7 +275,7 @@ - 5 + 5 @@ -284,7 +284,7 @@ 5 - 7 + 7 @@ -304,7 +304,7 @@ - 4 + 4 @@ -319,11 +319,11 @@ - 5 + 5 - 6 + 6 @@ -343,7 +343,7 @@ - 4 + 4 @@ -363,11 +363,11 @@ - 5 + 5 - 7 + 7 @@ -387,7 +387,7 @@ - 4 + 4 @@ -402,11 +402,11 @@ - 5 + 5 - 8 + 8 diff --git a/python/tests/test_drawing.py b/python/tests/test_drawing.py index 1b537f4a0c..9c46c7d8e1 100644 --- a/python/tests/test_drawing.py +++ b/python/tests/test_drawing.py @@ -336,19 +336,21 @@ def test_multiroot(self): def test_left_child(self): t = self.get_nonbinary_tree() - left_child = drawing.get_left_child(t, "postorder") + left_child = drawing.get_left_child(t, t.postorder()) for u in t.nodes(order="postorder"): if t.num_children(u) > 0: assert left_child[u] == t.children(u)[0] def test_null_node_left_child(self): t = self.get_nonbinary_tree() - left_child = drawing.get_left_child(t, "minlex_postorder") + arr = list(t.nodes(order="minlex_postorder")) + left_child = drawing.get_left_child(t, arr) assert left_child[tskit.NULL] == tskit.NULL def test_leaf_node_left_child(self): t = self.get_nonbinary_tree() - left_child = drawing.get_left_child(t, "minlex_postorder") + arr = list(t.nodes(order="minlex_postorder")) + left_child = drawing.get_left_child(t, arr) for u in t.samples(): assert left_child[u] == tskit.NULL @@ -1462,7 +1464,7 @@ class TestDrawSvgBase(TestTreeDraw, xmlunittest.XmlTestMixin): Base class for testing the SVG tree drawing method """ - def verify_basic_svg(self, svg, width=200, height=200, num_trees=1): + def verify_basic_svg(self, svg, width=200, height=200, num_trees=1, has_root=True): prefix = "{http://www.w3.org/2000/svg}" root = xml.etree.ElementTree.fromstring(svg) assert root.tag == prefix + "svg" @@ -1498,7 +1500,11 @@ def verify_basic_svg(self, svg, width=200, height=200, num_trees=1): for group in groups: assert "class" in group.attrib cls = group.attrib["class"] - assert re.search(r"\broot\b", cls) + # if a subtree plot, the top of the displayed topology is not a local root + if has_root: + assert re.search(r"\broot\b", cls) + else: + assert not re.search(r"\broot\b", cls) class TestDrawSvg(TestDrawSvgBase): @@ -2554,6 +2560,15 @@ def test_bad_x_regions(self): with pytest.raises(ValueError, match="Invalid coordinates"): ts.draw_svg(x_regions={(1, 0): "bad"}) + def test_bad_ts_order(self): + ts = msprime.sim_ancestry(1, sequence_length=100, random_seed=1) + with pytest.raises(ValueError, match="Unknown display order"): + ts.draw_svg(order=(ts.first().nodes(order="minlex_postorder"))) + + def test_good_tree_order(self): + ts = msprime.sim_ancestry(1, sequence_length=100, random_seed=1) + ts.first().draw_svg(order=(ts.first().nodes(order="minlex_postorder"))) + class TestDrawKnownSvg(TestDrawSvgBase): """ @@ -2955,6 +2970,61 @@ def test_known_max_num_trees_treewise(self, overwrite_viz, draw_plotbox): svg, "ts_max_trees_treewise.svg", overwrite_viz, width=200 * (max_trees + 1) ) + def test_known_svg_tree_collapsed(self, overwrite_viz, draw_plotbox): + tree = tskit.Tree.generate_balanced(8) + remove_nodes = set() + remove_nodes_below = {8, 13} + for u in remove_nodes_below: + subtree_nodes = set(tree.nodes(root=u)) - {u} + remove_nodes.update(subtree_nodes) + order = [ + u for u in tree.nodes(order="minlex_postorder") if u not in remove_nodes + ] + svg = tree.draw_svg(order=order, debug_box=draw_plotbox) + assert svg.count("multi") == len(remove_nodes_below) + assert svg.count(">+2<") == 1 # One tip has 2 samples below it + assert svg.count(">+4<") == 1 # Another tip has 4 samples below it + for u in order: + assert f'n{u}"' in svg or f"n{u} " in svg + for u in remove_nodes: + assert f'n{u}"' not in svg and f"n{u} " not in svg + self.verify_known_svg(svg, "tree_simple_collapsed.svg", overwrite_viz) + + def test_known_svg_tree_subtree(self, overwrite_viz, draw_plotbox): + tree = tskit.Tree.generate_balanced(8) + order = [u for u in tree.nodes(root=10, order="minlex_postorder")] + # The balanced tree has all descendants of nodes 10 with IDs < 10 + assert np.all(np.array(order) <= 10) + svg = tree.draw_svg(order=order, debug_box=draw_plotbox) + for u in order: + assert f'n{u}"' in svg or f"n{u} " in svg + for u in set(tree.nodes()) - set(order): + assert f'n{u}"' not in svg and f"n{u} " not in svg + self.verify_known_svg(svg, "tree_subtree.svg", overwrite_viz, has_root=False) + + def test_known_svg_tree_subtrees_with_collapsed(self, overwrite_viz, draw_plotbox): + # Two subtrees, one with a collapsed node below node 16 + tree = tskit.Tree.generate_balanced(16) + roots = [22, 25] + order = [] + remove_nodes_below = 16 + remove_nodes = set(tree.nodes(root=remove_nodes_below)) - {remove_nodes_below} + for root in roots: + order += [ + u + for u in tree.nodes(root=root, order="minlex_postorder") + if u not in remove_nodes + ] + svg = tree.draw_svg(order=order, debug_box=draw_plotbox) + assert svg.count("multi") == 1 # One tip representing multiple nodes + for u in order: + assert f'n{u}"' in svg or f"n{u} " in svg + for u in remove_nodes: + assert f'n{u}"' not in svg and f"n{u} " not in svg + self.verify_known_svg( + svg, "tree_subtrees_with_collapsed.svg", overwrite_viz, has_root=False + ) + class TestRounding: def test_rnd(self): diff --git a/python/tskit/drawing.py b/python/tskit/drawing.py index 558f913d20..b479cb5094 100644 --- a/python/tskit/drawing.py +++ b/python/tskit/drawing.py @@ -175,6 +175,10 @@ def check_order(order): "minlex": "minlex_postorder", "tree": "postorder", } + # Silently accept a tree traversal order as a valid order, so we can + # call this check twice if necessary + if order in traversal_orders.values(): + return order if order not in traversal_orders: raise ValueError( f"Unknown display order '{order}'. " @@ -1097,6 +1101,7 @@ def __init__( **kwargs, ) x_scale = check_x_scale(x_scale) + order = check_order(order) if node_labels is None: node_labels = {u: str(u) for u in range(ts.num_nodes)} if force_root_branch is None: @@ -1386,7 +1391,11 @@ def __init__( **kwargs, ) self.tree = tree - self.traversal_order = check_order(order) + if order is None or isinstance(order, str): + # Can't use the Tree.postorder array as we need minlex + self.postorder_nodes = list(tree.nodes(order=check_order(order))) + else: + self.postorder_nodes = order # Create some instance variables for later use in plotting self.node_mutations = collections.defaultdict(list) @@ -1686,12 +1695,21 @@ def assign_x_coordinates(self): # Set up x positions for nodes node_x_coord = {} leaf_x = 0 # First leaf starts at x=1, to give some space between Y axis & leaf - for u in self.tree.nodes(order=self.traversal_order): - if self.tree.is_leaf(u): + prev = self.tree.virtual_root + for u in self.postorder_nodes: + is_tip = self.tree.parent(prev) != u + prev = u + if is_tip: leaf_x += 1 node_x_coord[u] = leaf_x else: - child_coords = [node_x_coord[c] for c in self.tree.children(u)] + child_coords = [ + node_x_coord[c] for c in self.tree.children(u) if c in node_x_coord + ] + if len(child_coords) == 0: + raise ValueError( + "Nodes must be passed to the drawing function in postorder" + ) if len(child_coords) == 1: node_x_coord[u] = child_coords[0] else: @@ -1741,29 +1759,60 @@ def info_classes(self, focal_node_id): classes.add(f"s{mutation.site+ self.offsets.site}") return sorted(classes) + def text_transform(self, position, dy=0): + line_h = self.text_height + sym_sz = self.symbol_size + transforms = { + "below": f"translate(0 {rnd(line_h - sym_sz / 2 + dy)})", + "above": f"translate(0 {rnd(-(line_h - sym_sz / 2) + dy)})", + "above_left": f"translate({rnd(-sym_sz / 2)} {rnd(-line_h / 2 + dy)})", + "above_right": f"translate({rnd(sym_sz / 2)} {-rnd(line_h / 2 + dy)})", + "left": f"translate({-rnd(2 + sym_sz / 2)} {rnd(dy)})", + "right": f"translate({rnd(2 + sym_sz / 2)} {rnd(dy)})", + } + return transforms[position] + def draw_tree(self): + # Note: the displayed tree may not be the same as self.tree, e.g. if the nodes + # have been collapsed, or a subtree is being displayed. The node_x_coord + # dictionary keys gives the nodes of the displayed tree, in postorder. + NodeDrawInfo = collections.namedtuple("NodeDrawInfo", ["pos", "is_tip"]) dwg = self.drawing - node_coords = { - u: np.array([x, self.timescaling.transform(self.node_height[u])]) - for u, x in self.node_x_coord.items() - } tree = self.tree - left_child = get_left_child(tree, self.traversal_order) - - # Iterate over nodes, adding groups to reflect the tree hierarchy + left_child = get_left_child(tree, self.postorder_nodes) + parent_array = tree.parent_array + + node_info = {} + roots = [] # Roots of the displated tree + prev = tree.virtual_root + for u, x in self.node_x_coord.items(): # Node ids `u` returned in postorder + node_info[u] = NodeDrawInfo( + pos=np.array([x, self.timescaling.transform(self.node_height[u])]), + # Detect if this is a "tip" in the displayed tree, even if + # it is not a leaf in the original tree, by looking at the prev parent + is_tip=(parent_array[prev] != u), + ) + prev = u + if parent_array[u] not in self.node_x_coord: + roots.append(u) + # Iterate over displayed nodes, adding groups to reflect the tree hierarchy stack = [] - for u in tree.roots: - x, y = node_coords[u] + for u in roots: + x, y = node_info[u].pos grp = dwg.g( class_=" ".join(self.info_classes(u)), transform=f"translate({rnd(x)} {rnd(y)})", ) stack.append((u, self.get_plotbox().add(grp))) + + # Preorder traversal, so we can create nested groups while len(stack) > 0: u, curr_svg_group = stack.pop() - pu = node_coords[u] + pu, is_tip = node_info[u] for focal in tree.children(u): - fx, fy = node_coords[focal] - pu + if focal not in node_info: + continue + fx, fy = node_info[focal].pos - pu new_svg_group = curr_svg_group.add( dwg.g( class_=" ".join(self.info_classes(focal)), @@ -1773,31 +1822,30 @@ def draw_tree(self): stack.append((focal, new_svg_group)) o = (0, 0) - v = tree.parent(u) - - # Add edge first => on layer underneath anything else - if v != NULL: + v = parent_array[u] + + # Add edge above node first => on layer underneath anything else + draw_edge_above_node = False + try: + dx, dy = node_info[v].pos - pu + draw_edge_above_node = True + except KeyError: + # Must be a root + root_branch_l = self.min_root_branch_plot_length + if root_branch_l > 0: + if len(self.node_mutations[u]) > 0: + mtop = self.timescaling.transform( + self.node_mutations[u][0].time + ) + root_branch_l = max(root_branch_l, pu[1] - mtop) + dx, dy = 0, -root_branch_l + draw_edge_above_node = True + if draw_edge_above_node: add_class(self.edge_attrs[u], "edge") - dx, dy = node_coords[v] - pu path = dwg.path( [("M", o), ("V", rnd(dy)), ("H", rnd(dx))], **self.edge_attrs[u] ) curr_svg_group.add(path) - else: - root_branch_l = self.min_root_branch_plot_length - if root_branch_l > 0: - add_class(self.edge_attrs[u], "edge") - if len(self.node_mutations[u]) > 0: - mutation = self.node_mutations[u][0] # Oldest on this branch - root_branch_l = max( - root_branch_l, - pu[1] - self.timescaling.transform(mutation.time), - ) - path = dwg.path( - [("M", o), ("V", rnd(-root_branch_l)), ("H", 0)], - **self.edge_attrs[u], - ) - curr_svg_group.add(path) # Add mutation symbols + labels for mutation in self.node_mutations[u]: @@ -1824,37 +1872,63 @@ def draw_tree(self): if mutation_id in self.mutation_titles: symbol.set_desc(title=self.mutation_titles[mutation_id]) # Labels - if u == left_child[tree.parent(u)]: + if u == left_child[parent_array[u]]: mut_label_class = "lft" - transform = f"translate(-{rnd(2+self.symbol_size/2)} 0)" + transform = self.text_transform("left") else: mut_label_class = "rgt" - transform = f"translate({rnd(2+self.symbol_size/2)} 0)" + transform = self.text_transform("right") add_class(self.mutation_label_attrs[mutation_id], mut_label_class) self.mutation_label_attrs[mutation_id]["transform"] = transform mut_group.add(dwg.text(**self.mutation_label_attrs[mutation_id])) - # Add node symbol + label next (visually above the edge subtending this node) - # Symbols - if self.tree.is_sample(u): + # Add node symbol + label (visually above the edge subtending this node) + # -> symbols + if tree.is_sample(u): symbol = curr_svg_group.add(dwg.rect(**self.node_attrs[u])) else: symbol = curr_svg_group.add(dwg.circle(**self.node_attrs[u])) + multi_samples = None + if ( + is_tip and tree.num_samples(u) > 1 + ): # Multi-sample tip => trapezium shape + multi_samples = tree.num_samples(u) + trapezium_attrs = self.node_attrs[u].copy() + # Remove the shape-styling attributes + for unwanted_attr in ("size", "insert", "center", "r"): + trapezium_attrs.pop(unwanted_attr, None) + trapezium_attrs["points"] = [ # add a trapezium shape below the symbol + (self.symbol_size / 2, 0), + (self.symbol_size, self.symbol_size), + (-self.symbol_size, self.symbol_size), + (-self.symbol_size / 2, 0), + ] + add_class(trapezium_attrs, "multi") + curr_svg_group.add(dwg.polygon(**trapezium_attrs)) if u in self.node_titles: symbol.set_desc(title=self.node_titles[u]) - # Labels + # -> labels node_lab_attr = self.node_label_attrs[u] - if tree.is_leaf(u): - node_lab_attr["transform"] = f"translate(0 {self.text_height - 3})" - elif tree.parent(u) == NULL and self.min_root_branch_plot_length == 0: - node_lab_attr["transform"] = f"translate(0 -{self.text_height - 3})" + if is_tip and multi_samples is None: + node_lab_attr["transform"] = self.text_transform("below") + elif u in roots and self.min_root_branch_plot_length == 0: + node_lab_attr["transform"] = self.text_transform("above") else: + if multi_samples is not None: + curr_svg_group.add( + dwg.text( + text=f"+{multi_samples}", + transform=self.text_transform("below", dy=1), + font_style="italic", + class_="lab summary", + ) + ) if u == left_child[tree.parent(u)]: add_class(node_lab_attr, "lft") - node_lab_attr["transform"] = f"translate(-3 -{self.text_height/2})" + node_lab_attr["transform"] = self.text_transform("above_left") else: add_class(node_lab_attr, "rgt") - node_lab_attr["transform"] = f"translate(3 -{self.text_height/2})" + node_lab_attr["transform"] = self.text_transform("above_right") curr_svg_group.add(dwg.text(**node_lab_attr)) @@ -1980,14 +2054,14 @@ def find_neighbours(u, neighbour): return left_neighbour[:-1] -def get_left_child(tree, traversal_order): +def get_left_child(tree, postorder_nodes): """ Returns the left-most child of each node in the tree according to the - specified traversal order. If a node has no children or NULL is passed - in, return NULL. + traversal order listed in postorder_nodes. If a node has no children or + NULL is passed in, return NULL. """ left_child = np.full(tree.tree_sequence.num_nodes + 1, NULL, dtype=int) - for u in tree.nodes(order=traversal_order): + for u in postorder_nodes: parent = tree.parent(u) if parent != NULL and left_child[parent] == NULL: left_child[parent] = u diff --git a/python/tskit/trees.py b/python/tskit/trees.py index f7273e0fcb..c28c590e6b 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -1949,7 +1949,7 @@ def draw_svg( mutation_titles=mutation_titles, root_svg_attributes=root_svg_attributes, style=style, - order=order, + order=order, # NB undocumented: Tree.draw_svg can also take an iterable here force_root_branch=force_root_branch, symbol_size=symbol_size, x_axis=x_axis,