Makie.jl
Makie.jl copied to clipboard
Add Dendrogram recipe
Description
Fixes #398
I've begun working on a dendrogram recipe. The primary goal is to be able to plot the output of hclust objects from Clustering.jl, and I made a recipe for StatsPlots.jl that did that a few years ago, but here, I thought I would try to make something a bit more generic.
Design
My initial thought is that a dendrogram is essentially a graph structure - to represent it in a plot, you need nodes with (x, y) coordinates, and links between them. But a dendrogram is a special-case, since it is a directed acyclic graph with a single root node, and nodes can have either 0 children (leaf nodes) or 2 children (all others).
This makes plotting relatively simple, since you can start with the root node, and then recursively draw children until you get to the leaves.
This is still very much work-in-progress, its not even a recipe yet, but I just wanted to throw it up to solicit feedback before I go too far down the path in the wrong direction.
My initial design is as follows:
- a struct
DNode
that has a node index,x
andy
coordinates, and aTuple
of child indexes (orNothing
if a leaf) - a function
find_merge
that takes 2(x,y)
tuples or 2DNode
s, and finds thex
andy
coordinates where their parent node should be -
get_tree_coordinates()
andget_box_coordinates()
to get the shapes for lines connecting a series of nodes (see below). The idea here is that there may be different ways to connect nodes (in the future, one might one a radial tree for example, and hopefully this would be extensible) -
recursive_draw_dendrogram!()
which takes a dictionary of nodes, and a starting node, and does the drawing
I also included an hcl_nodes()
for a proof-of-concept on how one might pull the right structure from a HClust
- I do not anticipate pulling in Clustering in as a dependency, but rather would add the recipe there, or else make a separate MakieDendrograms
package or something.
Showcase
Basic dendrograms
leaves = Point2f.([
(1,0),
(2,0.5),
(3,1),
(4,2),
(5,0)
])
merges = [
(1, 2), # 6
(6, 3), # 7
(4, 5), # 8
(7, 8), # 9
]
dendrogram(leaves, merges)
dendrogram(leaves, merges; branch_shape = :box)
Converting from HClust
using Clustering
using Distances
n = 20
mat = zeros(Int, n, n)
for i in 1:n
last = minimum([i+Int(floor(n/5)), n])
for j in i:last
mat[i,j] = 1
end
end
mat = mat[:, randperm(n)]
dm = pairwise(Euclidean(), mat; dims=2)
hcl = hclust(dm; linkage=:average, branchorder=:optimal)
heatmap(mat[:,hcl.order]) # see https://github.com/JuliaStats/Clustering.jl/pull/170
nodes = get_hcl(hcl)
dendrogram(nodes)
dendrogram(nodes; branch_shape=:box)
Planned features
- [ ] Specify heights of parent nodes. Right now, each parent is exactly 1 higher than the highest child
- [ ] Enable grouping. Because of the structure, it should be possible to specify color groups, and color branches if all leaves belong to the same group.
Help / Feedback requested
- I think I need some guidance on how to plug into the recipe system. I started from the boxplot recipe because I thought I understood all of those components, but I really don't understand the internals here. I plan to do a bunch of reading this week to see if I can get a further handle on it, but any pointers would be appreciated.
- Does this make sense to people? A different idea I had was to do something like use the
z
coordinate as a node index, but this seems like abusing the idea ofz
, and one would need to specify connections separately anyway. - Would it be better to just try a PR to
GraphMakie.jl
that provides a conversion fromHClust
to aGraphs.jl
graph? Part of me things that a primitive representation would be useful, but another part is worried I'm inventing a chiseled-rock wheel when Firestone tires exist. - Bike shedding on all the names. I feel 0 attachment to anything :laughing:
Type of change
Delete options that do not apply:
- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
Checklist
- [ ] Added an entry in NEWS.md (for new features and breaking changes)
- [ ] Added or changed relevant sections in the documentation
- [ ] Added unit tests for new algorithms, conversion methods, etc.
- [ ] Added reference image tests for new plotting functions, recipes, visual options, etc.
Sorry for the deluge of comments - here's the last one!
I couldn't help myself and refactored the code to fit the Makie schema a bit more. Hopefully it makes sense.
(This is basically the sum of all my comments + an interface to allow extending the branch shape + compatibility with Observables). It would be nice to give DNode
some sort of color
attribute, so we could potentially specialize on that as well.
"""
dendrogram(x, y; kwargs...)
Draw a [dendrogram](https://en.wikipedia.org/wiki/Dendrogram),
with leaf nodes specified by `x` and `y` coordinates,
and parent nodes identified by `merges`.
# Arguments
- `x`: x positions of leaf nodes
- `y`: y positions of leaf nodes (default = 0)
# Keywords
- `merges`: specifies connections between nodes (see below)
- `treestyle`: one of `:??`, `:??`. Overload `dendrogram_connectors(::Val{:mystyle}, parent, child1, child2)` to define a new style.
"""
@recipe(Dendrogram, nodes) do scene
Theme(
weights = Makie.automatic,
branch_shape = :box,
linewidth = Makie.inherit(scene, :linewidth, 1.0),
color = Makie.inherit(scene, :color, :black),
colormap = Makie.inherit(scene, :colormap, :viridis),
colorrange = Makie.automatic,
orientation = :vertical,
cycle = [:color => :patchcolor],
inspectable = Makie.inherit(scene, :inspectable, false),
xautolimits = Makie.inherit(scene, :xautolimits, true),
yautolimits = Makie.inherit(scene, :yautolimits, true),
)
end
function recursive_dendrogram_points(node, nodes, ret_points = Point2f[], ret_colors = []; branch_shape=:tree)
isnothing(node.children) && return nothing
child1 = nodes[node.children[1]]
child2 = nodes[node.children[2]]
l = dendrogram_connectors(Val(branch_shape), node, child1, child2)
# even if the inputs are 2d, the outputs should be 3d - this is what `to_ndim` does.
append!(ret_points, Makie.to_ndim.(Point3f, l, 0))
push!(ret_points, Point3f(NaN)) # separate segments
append!(ret_colors, [Float32(node.idx) for _ in 1:length(l)])
push!(ret_colors, NaN32) # separate segments
recursive_dendrogram_points(child1, nodes, ret_points, ret_colors; branch_shape)
recursive_dendrogram_points(child2, nodes, ret_points, ret_colors; branch_shape)
return ret_points, ret_colors
end
function Makie.plot!(plot::Dendrogram{<: Tuple{<: Dict{<: Integer, <: Union{DNode{2}, DNode{3}}}}})
# args = @extract plot (weights, width, range, show_outliers, whiskerwidth, show_notch, orientation, gap, dodge, n_dodge, dodge_gap)
points_vec = Observable{Any}()
colors_vec = Observable{Any}()
lift(plot[1], plot.branch_shape) do nodes, branch_shape
# this pattern is basically first updating the values of the observables,
points_vec.val, colors_vec.val = recursive_dendrogram_points(nodes[maximum(keys(nodes))], nodes; branch_shape)
# then propagating the signal, so that there is no error with differing lengths.
notify(points_vec); notify(colors_vec)
end
lines!(plot, points_vec; color = colors_vec, colormap = plot.colormap, colorrange = plot.colorrange, linewidth = plot.linewidth, inspectable = plot.inspectable, xautolimits = plot.xautolimits, yautolimits = plot.yautolimits)
end
# branching styles
struct DNode{N}
idx::Int
position::Point{N, Float32}
children::Union{Tuple{Int,Int}, Nothing}
end
function DNode(idx::Int, point::Point{N}, children::Union{Tuple{Int,Int}, Nothing}) where N
return DNode{N}(idx, point, children)
end
function dendrogram_connectors(::Val{:tree}, parent, child1, child2)
return [child1.position, parent.position, child2.position]
end
function dendrogram_connectors(::Val{:box}, parent::DNode{2}, child1::DNode{2}, child2::DNode{2})
yp = parent.position[2]
x1 = child1.position[1]
x2 = child2.position[1]
return Point2f[(x1, child1.position[2]), (x1, yp), (x2, yp), (x2, child2.position[2])]
end
function dendrogram_connectors(::Val{:box}, parent::DNode{3}, child1::DNode{3}, child2::DNode{3})
yp = parent.position[2]
x1 = child1.position[1]
x2 = child2.position[1]
return Point3f[
(x1, child1.position[2], child1.position[3]),
(x1, yp, (parent.position[3] + child1.position[3])./2),
(x2, yp, (parent.position[3] + child2.position[3])./2),
(x2, child2.position[2], child2.position[3])
]
end
# convert utils
function find_merge(n1, n2; height=1)
newx = min(n1[1], n2[1]) + abs((n1[1] - n2[1])) / 2
newy = max(n1[2], n2[2]) + height
return Point2f(newx, newy)
end
function find_merge(n1::DNode{2}, n2::DNode{2}; height=1, index=max(n1.idx, n2.idx)+1)
newx = min(n1.position[1], n2.position[1]) + abs((n1.position[1] - n2.position[1])) / 2
newy = max(n1.position[2], n2.position[2]) + height
return DNode{2}(index, Point2f(newx, newy), (n1.idx, n2.idx))
end
function find_merge(n1::DNode{3}, n2::DNode{3}; height=1, index=max(n1.idx, n2.idx)+1)
newx = min(n1.position[1], n2.position[1]) + abs((n1.position[1] - n2.position[1])) / 2
newy = max(n1.position[2], n2.position[2]) + height
newz = min(n1.position[3], n2.position[3]) + abs((n1.position[3] - n2.position[3])) / 2
return DNode{3}(index, Point3f(newx, newy, newz), (n1.idx, n2.idx))
end
function Makie.convert_arguments(::Type{<: Dendrogram}, leaves::Vector{<: Point}, merges::Vector{<: Tuple{<: Integer, <: Integer}})
nodes = Dict(i => DNode(i, n, nothing) for (i,n) in enumerate(leaves))
nm = maximum(keys(nodes))
for m in merges
nm += 1
nodes[nm] = find_merge(nodes[m[1]], nodes[m[2]]; index = nm)
end
return (nodes,)
end
function hcl_nodes(hcl; useheight=false)
nleaves = length(hcl.order)
nodes = Dict(i => DNode(i, x, 0, nothing) for (i,x) in enumerate(invperm(hcl.order)))
nm = maximum(keys(nodes))
for (m1, m2) in eachrow(hcl.merges)
nm += 1
m1 = m1 < 0 ? -m1 : m1 + nleaves
m2 = m2 < 0 ? -m2 : m2 + nleaves
nodes[nm] = find_merge(nodes[m1], nodes[m2]; index=nm)
end
return nodes
end
This looks great! Just wondering if it would make sense to have this in GraphMakie? I guess a dendrogram is really a NetworkLayout.buchheimtree with custom line drawing?
This looks great! Just wondering if it would make sense to have this in GraphMakie? I guess a dendrogram is really a NetworkLayout.buchheimtree with custom line drawing?
See my bullet 3 under help/feedback βΊοΈ. I agree that that might make the most sense.
One consideration (with the caveat that I'm not very familiar with GraphMakie, so it might be moot) is that it did not seem super obvious to me how to fix the points of the leaves. Since my primary use case at the moment is attaching dendrograms to heatmaps, that's a critical bit.
@asinghvi17 holy crap, thanks! I will need some time to study this, but it looks great! π
So, this is working a bit now - I've been trying to sort out how to deal with color. I figured out that I can replace colors_vec.val
with a vector of eg :black
. But I'm trying to get grouping working to add different colors, and I'm a bit stuck. I created recursive_leaf_groups()
, which enables checking the groups of all leaves from a node. For example:
leaves = Point2f.([
(1,0),
(2,0.5),
(3,1),
(4,2),
(5,0)
])
merges = [
(1, 2), # 6
(6, 3), # 7
(4, 5), # 8
(7, 8), # 9
]
nodes, = Makie.convert_arguments(Dendrogram, leaves, merges)
groups = [1,1,2,3,3]
recursive_leaf_groups(nodes[9], nodes, groups) # [1,1,2,3,3]
recursive_leaf_groups(nodes[6], nodes, groups) # [1,1]
recursive_leaf_groups(nodes[7], nodes, groups) # [1,1,2]
recursive_leaf_groups(nodes[8], nodes, groups) # [3,3]
The idea here is that, if all of the children of a given node are in the same group, that node and all its descendants can have the same color. I'd like to be able to, for example, do
dendrogram(leaves, merges; groups = [1,1,2,3,3], color=[:blue, :orange, :purple], fallback_color=:gray)
What I have here sort of works for just assigning numbers, eg dendrogram(leaves, merges; groups=[1,1,2,3,3])
gets me
while dendrogram(leaves, merges; groups=[1,1,1,3,3])
gets me
Gonna take a little break and come back to it.
@kescobo were you planning to add labels to the leaf nodes? That should be pretty easy to do :)
I was not - I typically use xticks for labeling if I need them, but happy to reconsider.
Also still need to implement heights and axis switching (eg orientation=:vertical
). Hoping to work a bit more on this tomorrow.
Sorry I've stalled on this a bit - other work stuff came up :-(
OK, so I'm making progress, I think, but not sure what to do next. I made a function that can find whether a node belongs to a group, so eg:
leaves = Point2f.([
(1,0),
(2,0.5),
(3,1),
(4,2),
(5,0)
])
merges = [
(1, 2), # 6
(6, 3), # 7
(4, 5), # 8
(7, 8), # 9
]
dendrogram(leaves, merges; groups = [1,1,1,2,2])
gives
but
dendrogram(leaves, merges; groups = [1,1,3,2,2])
gives
But I'm not sure how to
- Set a default color - should be black. At the moment, if I don't pass a
groups
argument, I getERROR: Can't interpolate in a range where cmin == cmax. This can happen, for example, if a colorrange is set automatically but there's only one unique value present.
- Allow passing a vector of colors (eg
[:red, :blue, :orange]
- Right now, group 3 in the second example above ends up with the default color (
0
) because it has only 1 member. Not sure how to set it as a group if groups only have 1 member.
- Set a default color - should be black. At the moment, if I don't pass a groups argument, I get ERROR: Can't interpolate in a range where cmin == cmax. This can happen, for example, if a colorrange is set automatically but there's only one unique value present.
An idea comes to mind, and this should help with (2) as well: let groups
accept either a vector of colors, or a vector of reals. If reals, then do the colormap stuff. If colors, simply plot using group_colors = to_color.(groups)
. Multiple dispatch based on eltype(groups)
should help here.
- Allow passing a vector of colors (eg [:red, :blue, :orange]
As above.
- Right now, group 3 in the second example above ends up with the default color (0) because it has only 1 member. Not sure how to set it as a group if groups only have 1 member.
Not sure about this one - will take some thought, I think.
This looks great! :) Could you add a file dentrogram.md
e.g. like this one: https://github.com/MakieOrg/Makie.jl/blob/master/docs/examples/plotting_functions/arc.md (same folder etc, it should get picked up by the plot function example page).
Also a reference test like this one would be nice:
https://github.com/MakieOrg/Makie.jl/blob/master/ReferenceTests/src/tests/short_tests.jl#L195-L197
Best with thick lines so that problems are quickly visible, and multiple plots in one figure to test different attributes would be nice :)
I guess adding the plots you posted on this PR to docs/tests would be all we need!
so sad that this dendrogram PR has stalled. a hugely popular plot type which i need now! here's hoping that @kescobo has time to add docs and tests so it can be merged. i simple example of how to attach it to a heatmap would be great too. in particular, the merges
kwarg is not documented well. the docstring says "see below", and i don't see anything below.
Yeah, totally my bad. I actually think it needs more than just docs and tests - my last iteration wasn't able to pass a vector of colors, but I definitely need to come back to this.
Also, FWIW, I'm fine if someone else wants to take up the mantel. I just had a baby and even I do back to work I'll be swamped, and I don't want to be a cookie licker.
so i'm trying to get this to work. saved the dendrogram.jl file in this PR locally, and then include
d it. get a bunch of errors when running the most recent code block above:
julia> using GLMakie
julia> include("src/dendrogram.jl")
recursive_leaf_groups (generic function with 1 method)
julia> leaves = Point2f.([
(1,0),
(2,0.5),
(3,1),
(4,2),
(5,0)
])
5-element Vector{Point{2, Float32}}:
[1.0, 0.0]
[2.0, 0.5]
[3.0, 1.0]
[4.0, 2.0]
[5.0, 0.0]
julia> merges = [
(1, 2), # 6
(6, 3), # 7
(4, 5), # 8
(7, 8), # 9
]
4-element Vector{Tuple{Int64, Int64}}:
(1, 2)
(6, 3)
(4, 5)
(7, 8)
julia> dendrogram(leaves, merges; groups = [1,1,1,2,2])
1 : #version 410
2 :
3 :
4 :
5 : struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data
6 : bool _; //empty structs are not allowed
7 : };
8 :
9 : #define FAST_PATH
10 :
11 : in vec2 vertex;
12 :
13 : in float lastlen;
14 : in float valid_vertex;
15 :
16 : in float color;
17 : uniform Nothing color_map;
18 : uniform Nothing intensity;
19 : uniform Nothing color_norm;
20 : uniform float thickness;
21 :
22 : vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len);
23 : vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len);
24 : vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len);
25 : vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len);
26 :
27 : uniform mat4 projectionview, model;
28 : uniform uint objectid;
29 : uniform int total_length;
30 :
31 : out uvec2 g_id;
32 : out vec4 g_color;
33 : out float g_lastlen;
34 : out int g_valid_vertex;
35 : out float g_thickness;
36 :
37 : vec4 getindex(sampler2D tex, int index);
38 : vec4 getindex(sampler1D tex, int index);
39 :
40 : vec4 to_vec4(vec3 v){return vec4(v, 1);}
41 : vec4 to_vec4(vec2 v){return vec4(v, 0, 1);}
42 :
43 : int get_valid_vertex(float se){return int(se);}
44 : int get_valid_vertex(Nothing se){return 1;}
45 :
46 : uniform float depth_shift;
47 :
48 : void main()
49 : {
50 : g_lastlen = lastlen;
51 : int index = gl_VertexID;
52 : g_id = uvec2(objectid, index+1);
53 : g_valid_vertex = get_valid_vertex(valid_vertex);
54 : g_thickness = thickness;
55 :
56 : g_color = _color(color, intensity, color_map, color_norm, index, total_length);
57 : #ifdef FAST_PATH
58 : gl_Position = projectionview * model * to_vec4(vertex);
59 : #else
60 : gl_Position = to_vec4(vertex);
61 : #endif
62 : gl_Position.z += gl_Position.w * depth_shift;
63 : }
64 :
β Warning: shader /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/lines.vert didn't compile.
β ERROR: 0:56: No matching function for call to _color(float, Nothing, Nothing, Nothing, int, int)
β @ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLShader.jl:129
1 : #version 410
2 :
3 :
4 :
5 : struct Nothing{ //Nothing type, to encode if some variable doesn't contain any data
6 : bool _; //empty structs are not allowed
7 : };
8 :
9 : #define FAST_PATH
10 :
11 : in vec2 vertex;
12 :
13 : in float lastlen;
14 : in float valid_vertex;
15 :
16 : in float color;
17 : uniform Nothing color_map;
18 : uniform Nothing intensity;
19 : uniform Nothing color_norm;
20 : uniform float thickness;
21 :
22 : vec4 _color(vec3 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len);
23 : vec4 _color(vec4 color, Nothing intensity, Nothing color_map, Nothing color_norm, int index, int len);
24 : vec4 _color(Nothing color, float intensity, sampler1D color_map, vec2 color_norm, int index, int len);
25 : vec4 _color(Nothing color, sampler1D intensity, sampler1D color_map, vec2 color_norm, int index, int len);
26 :
27 : uniform mat4 projectionview, model;
28 : uniform uint objectid;
29 : uniform int total_length;
30 :
31 : out uvec2 g_id;
32 : out vec4 g_color;
33 : out float g_lastlen;
34 : out int g_valid_vertex;
35 : out float g_thickness;
36 :
37 : vec4 getindex(sampler2D tex, int index);
38 : vec4 getindex(sampler1D tex, int index);
39 :
40 : vec4 to_vec4(vec3 v){return vec4(v, 1);}
41 : vec4 to_vec4(vec2 v){return vec4(v, 0, 1);}
42 :
43 : int get_valid_vertex(float se){return int(se);}
44 : int get_valid_vertex(Nothing se){return 1;}
45 :
46 : uniform float depth_shift;
47 :
48 : void main()
49 : {
50 : g_lastlen = lastlen;
51 : int index = gl_VertexID;
52 : g_id = uvec2(objectid, index+1);
53 : g_valid_vertex = get_valid_vertex(valid_vertex);
54 : g_thickness = thickness;
55 :
56 : g_color = _color(color, intensity, color_map, color_norm, index, total_length);
57 : #ifdef FAST_PATH
58 : gl_Position = projectionview * model * to_vec4(vertex);
59 : #else
60 : gl_Position = to_vec4(vertex);
61 : #endif
62 : gl_Position.z += gl_Position.w * depth_shift;
63 : }
64 :
β Warning: shader /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/lines.vert didn't compile.
β ERROR: 0:56: No matching function for call to _color(float, Nothing, Nothing, Nothing, int, int)
β @ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLShader.jl:129
Error showing value of type Makie.FigureAxisPlot:
ERROR: program 47 not linked. Error in:
/Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/fragment_output.frag or /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/util.vert or /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/lines.vert or /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/lines.geom or /Users/arthurb/.julia/packages/GLMakie/45A8G/assets/shader/lines.frag
ERROR: One or more attached shaders not successfully compiled
Stacktrace:
[1] error(::String, ::String, ::String, ::String)
@ Base ./error.jl:44
[2] compile_program(shaders::Vector{GLMakie.GLAbstraction.Shader}, fragdatalocation::Vector{Tuple{Int64, String}})
@ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLShader.jl:188
[3] (::GLMakie.GLAbstraction.var"#70#75"{GLMakie.GLAbstraction.ShaderCache, Vector{Vector{String}}, Vector{Vector{String}}, NTuple{5, String}})()
@ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLShader.jl:261
[4] get!(default::GLMakie.GLAbstraction.var"#70#75"{GLMakie.GLAbstraction.ShaderCache, Vector{Vector{String}}, Vector{Vector{String}}, NTuple{5, String}}, h::Dict{Any, GLMakie.GLAbstraction.GLProgram}, key::Tuple{NTuple{5, String}, Vector{Vector{String}}})
@ Base ./dict.jl:468
[5] gl_convert(cache::GLMakie.GLAbstraction.ShaderCache, lazyshader::GLMakie.GLVisualizeShader, data::Dict{Symbol, Any})
@ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLShader.jl:252
[6] gl_convert
@ ~/.julia/packages/GLMakie/45A8G/src/glshaders/visualize_interface.jl:88 [inlined]
[7] GLMakie.GLAbstraction.RenderObject(data::Dict{Symbol, Any}, program::GLMakie.GLVisualizeShader, pre::GLMakie.GLAbstraction.StandardPrerender, post::GLFW.Window, context::GLFW.Window)
@ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLTypes.jl:458
[8] GLMakie.GLAbstraction.RenderObject(data::Dict{Symbol, Any}, program::GLMakie.GLVisualizeShader, pre::GLMakie.GLAbstraction.StandardPrerender, post::GLFW.Window)
@ GLMakie.GLAbstraction ~/.julia/packages/GLMakie/45A8G/src/GLAbstraction/GLTypes.jl:404
[9] assemble_shader(data::Dict{Symbol, Any})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/glshaders/visualize_interface.jl:108
[10] draw_lines(screen::Any, position::Observable{Vector{Point{2, Float32}}}, data::Dict)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/glshaders/lines.jl:119
[11] (::GLMakie.var"#203#206"{GLMakie.Screen{GLFW.Window}, Lines{Tuple{Vector{Point{2, Float32}}}}})(gl_attributes::Dict{Symbol, Any})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:309
[12] (::GLMakie.var"#177#180"{GLMakie.var"#203#206"{GLMakie.Screen{GLFW.Window}, Lines{Tuple{Vector{Point{2, Float32}}}}}, GLMakie.Screen{GLFW.Window}, Scene, Lines{Tuple{Vector{Point{2, Float32}}}}})()
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:135
[13] get!(default::GLMakie.var"#177#180"{GLMakie.var"#203#206"{GLMakie.Screen{GLFW.Window}, Lines{Tuple{Vector{Point{2, Float32}}}}}, GLMakie.Screen{GLFW.Window}, Scene, Lines{Tuple{Vector{Point{2, Float32}}}}}, h::Dict{UInt64, GLMakie.GLAbstraction.RenderObject}, key::UInt64)
@ Base ./dict.jl:468
[14] cached_robj!(robj_func::GLMakie.var"#203#206"{GLMakie.Screen{GLFW.Window}, Lines{Tuple{Vector{Point{2, Float32}}}}}, screen::GLMakie.Screen{GLFW.Window}, scene::Scene, x::Lines{Tuple{Vector{Point{2, Float32}}}})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:103
[15] draw_atomic
@ ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:278 [inlined]
[16] insert!(screen::GLMakie.Screen{GLFW.Window}, scene::Scene, x::Lines{Tuple{Vector{Point{2, Float32}}}})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:151
[17] (::GLMakie.var"#183#184"{GLMakie.Screen{GLFW.Window}, Scene})(x::Lines{Tuple{Vector{Point{2, Float32}}}})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:156
[18] foreach(f::GLMakie.var"#183#184"{GLMakie.Screen{GLFW.Window}, Scene}, itr::Vector{AbstractPlot})
@ Base ./abstractarray.jl:3075
[19] insert!(screen::GLMakie.Screen{GLFW.Window}, scene::Scene, x::Combined{dendrogram, Tuple{Dict{Int64, DNode{2}}}})
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/drawing_primitives.jl:153
[20] insertplots!(screen::GLMakie.Screen{GLFW.Window}, scene::Scene)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/screen.jl:447
[21] insertplots!(screen::GLMakie.Screen{GLFW.Window}, scene::Scene) (repeats 2 times)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/screen.jl:450
[22] display_scene!(screen::GLMakie.Screen{GLFW.Window}, scene::Scene)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/screen.jl:379
[23] GLMakie.Screen(scene::Scene, config::GLMakie.ScreenConfig; visible::Nothing, start_renderloop::Bool)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/screen.jl:396
[24] GLMakie.Screen(scene::Scene, config::GLMakie.ScreenConfig)
@ GLMakie ~/.julia/packages/GLMakie/45A8G/src/screen.jl:392
[25] getscreen(::Module, ::Scene; screen_config::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ Makie ~/.julia/packages/Makie/1hq9u/src/display.jl:408
[26] getscreen
@ ~/.julia/packages/Makie/1hq9u/src/display.jl:391 [inlined]
[27] display(figlike::Makie.FigureAxisPlot; backend::Module, inline::Bool, update::Bool, screen_config::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ Makie ~/.julia/packages/Makie/1hq9u/src/display.jl:161
[28] display(figlike::Makie.FigureAxisPlot)
@ Makie ~/.julia/packages/Makie/1hq9u/src/display.jl:131
[29] print_response(errio::IO, response::Any, show_value::Bool, have_color::Bool, specialdisplay::Union{Nothing, AbstractDisplay})
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:0
[30] (::REPL.var"#57#58"{REPL.LineEditREPL, Pair{Any, Bool}, Bool, Bool})(io::Any)
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:287
[31] with_repl_linfo(f::Any, repl::REPL.LineEditREPL)
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:557
[32] print_response(repl::REPL.AbstractREPL, response::Any, show_value::Bool, have_color::Bool)
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:285
[33] (::REPL.var"#do_respond#80"{Bool, Bool, REPL.var"#93#103"{REPL.LineEditREPL, REPL.REPLHistoryProvider}, REPL.LineEditREPL, REPL.LineEdit.Prompt})(s::REPL.LineEdit.MIState, buf::Any, ok::Bool)
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:899
[34] #invokelatest#2
@ ./essentials.jl:819 [inlined]
[35] invokelatest
@ ./essentials.jl:816 [inlined]
[36] run_interface(terminal::REPL.Terminals.TextTerminal, m::REPL.LineEdit.ModalInterface, s::REPL.LineEdit.MIState)
@ REPL.LineEdit ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/LineEdit.jl:2647
[37] run_frontend(repl::REPL.LineEditREPL, backend::REPL.REPLBackendRef)
@ REPL ~/.julia/juliaup/julia-1.9.3+0.aarch64.apple.darwin14/share/julia/stdlib/v1.9/REPL/src/REPL.jl:1300
[38] (::REPL.var"#62#68"{REPL.LineEditREPL, REPL.REPLBackendRef})()
@ REPL ./task.jl:514
this is with julia 1.9.3, GLMakie 0.8.10, on an apple M2.
@bjarthur I just invited you to my fork, so you can push directly to my dendrogram
branch to work on this, if you'd like.
great, thanks! will push directly going forward. for now though, does the suggested change above make sense to you? if so, easiest to probably just merge it.
Thanks @bjarthur for taking this up and @asinghvi17 for the review!
I am back to work, but swamped as expected - if I can be of some assistance, please let me know.
@kescobo for months now i have been using a cut & paste of this code in production. as noted above, colors don't work, so i simply hacked it by replacing
lines!(plot, points_vec; color = colors_vec, colormap = plot.colormap, colorrange = plot.colorrange ...)
with
lines!(plot, points_vec; color = :black ...)
personally i'd be happy if that change were permanent, so from my standpoint all that's needed is docs and tests.
i ofc am swamped too :). all i've done so far to this PR is to squash all the commits into one.