Makie.jl icon indicating copy to clipboard operation
Makie.jl copied to clipboard

Add Dendrogram recipe

Open kescobo opened this issue 1 year ago β€’ 16 comments

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 and y coordinates, and a Tuple of child indexes (or Nothing if a leaf)
  • a function find_merge that takes 2 (x,y) tuples or 2 DNodes, and finds the x and y coordinates where their parent node should be
  • get_tree_coordinates() and get_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 of z, 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 from HClust to a Graphs.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.

kescobo avatar Mar 14 '23 02:03 kescobo

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

asinghvi17 avatar Mar 14 '23 03:03 asinghvi17

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?

mkborregaard avatar Mar 14 '23 07:03 mkborregaard

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! πŸ™‡

kescobo avatar Mar 14 '23 11:03 kescobo

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

image

while dendrogram(leaves, merges; groups=[1,1,1,3,3]) gets me

image

Gonna take a little break and come back to it.

kescobo avatar Mar 14 '23 21:03 kescobo

@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.

kescobo avatar Mar 15 '23 14:03 kescobo

Sorry I've stalled on this a bit - other work stuff came up :-(

kescobo avatar Mar 22 '23 20:03 kescobo

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 image but

dendrogram(leaves, merges; groups = [1,1,3,2,2])

gives

image

But I'm not sure how to

  1. 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.
  2. Allow passing a vector of colors (eg [:red, :blue, :orange]
  3. 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.

kescobo avatar Mar 27 '23 20:03 kescobo

  1. 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.

  1. Allow passing a vector of colors (eg [:red, :blue, :orange]

As above.

  1. 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.

asinghvi17 avatar Apr 09 '23 20:04 asinghvi17

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!

SimonDanisch avatar Jul 20 '23 15:07 SimonDanisch

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.

bjarthur avatar Sep 20 '23 12:09 bjarthur

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.

kescobo avatar Sep 20 '23 12:09 kescobo

so i'm trying to get this to work. saved the dendrogram.jl file in this PR locally, and then included 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 avatar Sep 28 '23 22:09 bjarthur

@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.

kescobo avatar Oct 06 '23 13:10 kescobo

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.

bjarthur avatar Oct 06 '23 13:10 bjarthur

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 avatar May 15 '24 19:05 kescobo

@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.

bjarthur avatar May 15 '24 20:05 bjarthur