From 961fdce6e09402dc987cfd21bcea02d88416d7b9 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Mon, 10 Sep 2018 23:14:23 +0200 Subject: [PATCH 01/11] First commit on plotting recipes --- src/Phylo.jl | 3 + src/plot.jl | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 src/plot.jl diff --git a/src/Phylo.jl b/src/Phylo.jl index bc0de187..9091f889 100644 --- a/src/Phylo.jl +++ b/src/Phylo.jl @@ -136,6 +136,9 @@ include("show.jl") include("trim.jl") export droptips!, keeptips! +# Plot recipes +#include("plot.jl") + # Path into package path(path...; dir::String = "test") = joinpath(@__DIR__, "..", dir, path...) diff --git a/src/plot.jl b/src/plot.jl new file mode 100644 index 00000000..529c30da --- /dev/null +++ b/src/plot.jl @@ -0,0 +1,170 @@ +using RecipesBase +using Phylo +using Plots # now necessary for the labels :-( + +@recipe function f(tree::Phylo.AbstractTree; treetype = :dendrogram, showtips = true, tipfont = (5,)) + + linecolor --> :black + grid --> false + framestyle --> :none + legend --> false + colorbar --> true + size --> (1000, 1000) + + d, h, n = _findxy(tree) + adj = 0.03maximum(values(d)) + tipannotations = map(x->(d[x] + adj, h[x], x), leafiter(tree)) + + x, y = Float64[], Float64[] + for node ∈ n + if hasinbound(tree, node) + push!(x, d[getparent(tree, node)], d[getparent(tree, node)], d[node], NaN) + push!(y, h[getparent(tree, node)], h[node], h[node], NaN) + end + end + + marker_x, marker_y = Float64[], Float64[] + if any(x->occursin(r"marker", String(x)), keys(plotattributes)) + f = nodenamefilter(!isleaf, tree) + append!(marker_x, getindex.(Ref(d), f)) + append!(marker_y, getindex.(Ref(h), f)) + end + + if treetype == :dendrogram + Dendrogram(x, y, tipannotations, marker_x, marker_y, showtips, tipfont) + elseif treetype == :fan + Fan(x, y, tipannotations, marker_x, marker_y, showtips, tipfont) + else + throw(ArgumentError("Unsupported `treetype`; valid values are `:dendrogram` or `:fan`")) + end +end + +struct Dendrogram; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end +struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end + +@recipe function f(d::Dendrogram) + @series begin + seriestype := :path + markersize := 0 + markershape := :none + d.x, d.y + end + if !isempty(d.marker_x) + @series begin + seriestype := :scatter + d.marker_x, d.marker_y + end + end + d.showtips && (annotations := map(x -> (x[1], x[2], text(x[3], :left, d.tipfont...)), d.tipannotations)) + [],[] +end + +@recipe function f(d::Fan) + adjust(y) = 2pi*y / (length(d.tipannotations) + 1) + @series begin + seriestype := :path + markersize := 0 + markershape := :none + x, y = _circle_transform_segments(d.x, adjust(d.y)) + x, y + end + if !isempty(d.marker_x) + @series begin + seriestype := :scatter + _xcirc.(adjust(d.marker_y), d.marker_x), _ycirc.(adjust(d.marker_y), d.marker_x) + end + end + aspect_ratio := 1 + mx = maximum(filter(isfinite, d.x)) + xlim --> (1.3 .* (-mx, mx)) + ylim --> (1.3 .* (-mx, mx)) + d.showtips && (annotations := map(x -> (_tocirc(x[1], adjust(x[2]))..., text(x[3], :left, rad2deg(adjust(x[2])), d.tipfont...)), d.tipannotations)) + [],[] +end + + +leafiter(tree) = nodenamefilter(isleaf, tree) + +function _findxy(tree::Phylo.AbstractTree) + + # two convenience recursive functions using captured variables + function findheights!(clade::String) + if !in(clade, keys(height)) + for subclade in getchildren(tree, clade) + findheights!(subclade) + end + end + if !isleaf(tree, clade) + ch_heights = [height[child] for child in getchildren(tree, clade)] + height[clade] = (maximum(ch_heights) + minimum(ch_heights)) / 2. + end + end + + function finddepths!(clade::String) + push!(names, clade) + if hasinbound(tree, clade) + depth[clade] = depth[getparent(tree, clade)] + getbranch(tree, getinbound(tree, clade)).length + end + for ch in getchildren(tree, clade) + finddepths!(ch) + end + end + + root = first(nodenamefilter(isroot, tree)) + height = Dict(tip => float(i) for (i, tip) in enumerate(leafiter(tree))) + sizehint!(height, length(nodeiter(tree))) + findheights!(root) + + depth = Dict{String, Float64}(root => 0) + names = String[] + sizehint!(depth, length(nodeiter(tree))) + sizehint!(names, length(nodeiter(tree))) + finddepths!(root) + + depth, height, names +end + +function _find_tips(depth, height, tree) + x, y, l = Float64[], Float64[], String[] + for k in keys(depth) + if isleaf(tree, k) + push!(x, depth[k]) + push!(y, height[k]) + push!(l, k) + end + end + x, y, l +end + +function _p_circ(start_θ, end_θ, r=1) + steps = range(start_θ, stop=end_θ, length = 1+ceil(Int, 60abs(end_θ - start_θ))) + retx = Array{Float64}(undef, length(steps)) + rety = similar(retx) + for u in eachindex(steps) + retx[u] = _xcirc(steps[u], r) + rety[u] = _ycirc(steps[u], r) + end + retx, rety +end + +_xcirc(x, r) = r*cos(x) +_ycirc(y, r) = r*sin(y) +_tocirc(x, y) = _xcirc(y, x), _ycirc(y, x) + +function _circle_transform_segments(xs, ys) + retx, rety = Float64[], Float64[] + function _transform_seg(_x, _y) + tmpx, tmpy = _p_circ(_y[1], _y[2], _x[1]) + append!(retx, tmpx) + append!(rety, tmpy) + push!(retx, _xcirc(_y[3], _x[3]), NaN) + push!(rety, _ycirc(_y[3], _x[3]), NaN) + end + i = 1 + while !(i === nothing) && i < length(xs) + j = findnext(isnan, xs, i) - 1 + _transform_seg(view(xs,i:j), view(ys, i:j)) + i = j + 2 + end + retx, rety +end From 0c9c4389dcad43fe02470f8c3ab4afd917c26d32 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:13:40 +0200 Subject: [PATCH 02/11] extend line attribute vectors for branches --- src/plot.jl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/plot.jl b/src/plot.jl index 529c30da..02d7ee83 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -47,6 +47,14 @@ struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end seriestype := :path markersize := 0 markershape := :none + + lc = _extend(get(plotattributes, :linecolor, nothing), d.x) + lc !== nothing && (linecolor := lc) + la = _extend(get(plotattributes, :linealpha, nothing), d.x) + la !== nothing && (linealpha := la) + lz = _extend(get(plotattributes, :line_z, nothing), d.x) + lz !== nothing && (line_z := lz) + d.x, d.y end if !isempty(d.marker_x) @@ -66,6 +74,12 @@ end markersize := 0 markershape := :none x, y = _circle_transform_segments(d.x, adjust(d.y)) + lc = _extend(get(plotattributes, :linecolor, nothing), x) + lc !== nothing && (linecolor := lc) + la = _extend(get(plotattributes, :linealpha, nothing), x) + la !== nothing && (linealpha := la) + lz = _extend(get(plotattributes, :line_z, nothing), x) + lz !== nothing && (line_z := lz) x, y end if !isempty(d.marker_x) @@ -82,6 +96,16 @@ end [],[] end +function _extend(tmp, x) + tmp isa AbstractVector && abs(length(tmp) - count(isnan, x)) < 2 || return nothing + ret = similar(x, eltype(tmp)) + j = 1 + length(tmp) - count(isnan, x) + for i in eachindex(x) + ret[i] = tmp[j] + isnan(x[i]) && (j += 1) + end + return ret +end leafiter(tree) = nodenamefilter(isleaf, tree) From 73fc3e85cc68164d99f6bfd58e07835daf8464e7 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:14:25 +0200 Subject: [PATCH 03/11] show tipannotations for dendrogram --- src/plot.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/plot.jl b/src/plot.jl index 02d7ee83..2be0eeb7 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -90,9 +90,12 @@ end end aspect_ratio := 1 mx = maximum(filter(isfinite, d.x)) - xlim --> (1.3 .* (-mx, mx)) - ylim --> (1.3 .* (-mx, mx)) - d.showtips && (annotations := map(x -> (_tocirc(x[1], adjust(x[2]))..., text(x[3], :left, rad2deg(adjust(x[2])), d.tipfont...)), d.tipannotations)) + if d.showtips + xlim --> (1.3 .* (-mx, mx)) + ylim --> (1.3 .* (-mx, mx)) + annotations := map(x -> (_tocirc(x[1], adjust(x[2]))..., text(x[3], :left, + rad2deg(adjust(x[2])), d.tipfont...)), d.tipannotations) + end [],[] end From e28cb031d81eb046fd0ef5eef29603afcf509186 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:15:41 +0200 Subject: [PATCH 04/11] allow series_annotations for dendrogram --- src/plot.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/plot.jl b/src/plot.jl index 2be0eeb7..77e474d5 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -43,10 +43,13 @@ struct Dendrogram; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end @recipe function f(d::Dendrogram) + + sa = get(plotattributes, :series_annotations, nothing) @series begin seriestype := :path markersize := 0 markershape := :none + series_annotations := nothing lc = _extend(get(plotattributes, :linecolor, nothing), d.x) lc !== nothing && (linecolor := lc) @@ -57,9 +60,10 @@ struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end d.x, d.y end - if !isempty(d.marker_x) + if !isempty(d.marker_x) || sa !== nothing @series begin seriestype := :scatter + sa !== nothing && (series_annotations := sa) d.marker_x, d.marker_y end end From 39b1c231a2f7137b759a2c64df41596f1b63e972 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:16:02 +0200 Subject: [PATCH 05/11] add map_depthfirst function --- src/plot.jl | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/plot.jl b/src/plot.jl index 77e474d5..fcde2232 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -199,3 +199,19 @@ function _circle_transform_segments(xs, ys) end retx, rety end + + +# a function to update a value successively from the root to the tips +function map_depthfirst(FUN, start, tree, eltype = nothing) + root = first(nodenamefilter(isroot, tree)) + eltype === nothing && (eltype = typeof(FUN(start, root))) + ret = Vector{eltype}() + function local!(val, node) + push!(ret, val) + for ch in getchildren(tree, node) + local!(FUN(val, node), ch) + end + end + local!(start, root) + ret +end From ca26cc6541336fdf682036d70c5fd073bd7af1a8 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:16:30 +0200 Subject: [PATCH 06/11] temporarily don't set linecolor due to clashes with `seriescolor` in Plots --- src/plot.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot.jl b/src/plot.jl index fcde2232..4098b5a2 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -4,7 +4,7 @@ using Plots # now necessary for the labels :-( @recipe function f(tree::Phylo.AbstractTree; treetype = :dendrogram, showtips = true, tipfont = (5,)) - linecolor --> :black + #linecolor --> :black grid --> false framestyle --> :none legend --> false From 4095f90041d8e2ec589002a55cc5c424b9e3d430 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 10:19:08 +0200 Subject: [PATCH 07/11] allow series_annotations for Fan --- src/plot.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plot.jl b/src/plot.jl index 4098b5a2..3943693f 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -73,10 +73,14 @@ end @recipe function f(d::Fan) adjust(y) = 2pi*y / (length(d.tipannotations) + 1) + + sa = get(plotattributes, :series_annotations, nothing) @series begin seriestype := :path markersize := 0 markershape := :none + series_annotations := nothing + x, y = _circle_transform_segments(d.x, adjust(d.y)) lc = _extend(get(plotattributes, :linecolor, nothing), x) lc !== nothing && (linecolor := lc) @@ -86,9 +90,10 @@ end lz !== nothing && (line_z := lz) x, y end - if !isempty(d.marker_x) + if !isempty(d.marker_x) || sa !== nothing @series begin seriestype := :scatter + a !== nothing && (series_annotations := sa) _xcirc.(adjust(d.marker_y), d.marker_x), _ycirc.(adjust(d.marker_y), d.marker_x) end end From 315c5db18f975b9f12538babf8ae06e6a5f9c5e3 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 15:32:21 +0200 Subject: [PATCH 08/11] add ladderize! Does not work yet --- src/plot.jl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/plot.jl b/src/plot.jl index 3943693f..64b1dd76 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -121,6 +121,26 @@ end leafiter(tree) = nodenamefilter(isleaf, tree) + + +function ladderize!(tree::AbstractTree) + function loc!(clade::String) + if isleaf(tree, clade) + return 1 + end + + sizes = map(loc!, getchildren(tree, clade)) + node = getnode(tree, clade) + node.outbounds .= node.outbounds[sortperm(sizes)] + sum(sizes) + 1 + end + + loc!(first(nodenamefilter(isroot, tree))) + tree +end + + + function _findxy(tree::Phylo.AbstractTree) # two convenience recursive functions using captured variables From 0ddb2c5f94d2423ecbac834c84f566dd6e351d7a Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Tue, 11 Sep 2018 23:51:07 +0200 Subject: [PATCH 09/11] rename ladderize! to a Base.sort! method --- src/plot.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plot.jl b/src/plot.jl index 64b1dd76..9c57adf7 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -123,7 +123,7 @@ leafiter(tree) = nodenamefilter(isleaf, tree) -function ladderize!(tree::AbstractTree) +function Base.sort!(tree::AbstractTree) function loc!(clade::String) if isleaf(tree, clade) return 1 From 08ce8b4278da763dbc487642982395242e572cb2 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Wed, 2 Jan 2019 17:44:18 +0100 Subject: [PATCH 10/11] don't call text --- src/plot.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plot.jl b/src/plot.jl index 9c57adf7..3bae67ad 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -67,7 +67,7 @@ struct Fan; x; y; tipannotations; marker_x; marker_y; showtips; tipfont; end d.marker_x, d.marker_y end end - d.showtips && (annotations := map(x -> (x[1], x[2], text(x[3], :left, d.tipfont...)), d.tipannotations)) + d.showtips && (annotations := map(x -> (x[1], x[2], (x[3], :left, d.tipfont...)), d.tipannotations)) [],[] end @@ -102,7 +102,7 @@ end if d.showtips xlim --> (1.3 .* (-mx, mx)) ylim --> (1.3 .* (-mx, mx)) - annotations := map(x -> (_tocirc(x[1], adjust(x[2]))..., text(x[3], :left, + annotations := map(x -> (_tocirc(x[1], adjust(x[2]))..., (x[3], :left, rad2deg(adjust(x[2])), d.tipfont...)), d.tipannotations) end [],[] From 54310adc4556ef6b459e7856c855866d05aa2119 Mon Sep 17 00:00:00 2001 From: Michael Krabbe Borregaard Date: Wed, 2 Jan 2019 17:44:42 +0100 Subject: [PATCH 11/11] depend on RecipesBase, not Plots --- REQUIRE | 1 + src/Phylo.jl | 2 +- src/plot.jl | 2 -- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/REQUIRE b/REQUIRE index 8b8567ac..7bf657ec 100644 --- a/REQUIRE +++ b/REQUIRE @@ -7,3 +7,4 @@ Missings IterableTables Requires DataFrames +RecipesBase diff --git a/src/Phylo.jl b/src/Phylo.jl index 9091f889..54407a70 100644 --- a/src/Phylo.jl +++ b/src/Phylo.jl @@ -137,7 +137,7 @@ include("trim.jl") export droptips!, keeptips! # Plot recipes -#include("plot.jl") +include("plot.jl") # Path into package path(path...; dir::String = "test") = joinpath(@__DIR__, "..", dir, path...) diff --git a/src/plot.jl b/src/plot.jl index 3bae67ad..579cde9d 100644 --- a/src/plot.jl +++ b/src/plot.jl @@ -1,6 +1,4 @@ using RecipesBase -using Phylo -using Plots # now necessary for the labels :-( @recipe function f(tree::Phylo.AbstractTree; treetype = :dendrogram, showtips = true, tipfont = (5,))