Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text boxes instead of text labels #57

Closed
roland-KA opened this issue Feb 11, 2022 · 16 comments · Fixed by #128
Closed

Text boxes instead of text labels #57

roland-KA opened this issue Feb 11, 2022 · 16 comments · Fixed by #128
Labels
enhancement New feature or request

Comments

@roland-KA
Copy link

I'm using GraphMakie to plot trees. Basically I get what I want, but the text labels on the nodes don't really look good, because they get plotted underneath the edges (or the node markers):
tree

I would like to have some rectangles around the text labels and the edges attaching to these rectangles. More like this:
Skizzen - Frame 1

How can I achieve this using GraphMakie?

@hexaeder
Copy link
Collaborator

Currently not really, no. Each graph plot is based on a scatter + some connecting lines in GraphMakie. You might want to check out GraphRecipes for that. I think, they have this kind of feature.
However you could use the propertie nlabels_align on a per-node basis to place the labels on the right sides of the nodes (see Julia AST example). With nlabels_offset and nlabels_distance you may further tweak the position of the labels.

@roland-KA
Copy link
Author

Thank's for the hints!

Do GraphRecipes also work with Makie? I've only used them with Plots and I didn't find any references to Makie in the documentation.

@hexaeder
Copy link
Collaborator

No they don't. It is Plots only...

@roland-KA
Copy link
Author

Ok thank's, then I have all the information needed so far!

@hexaeder hexaeder added the enhancement New feature or request label Feb 17, 2022
@hexaeder
Copy link
Collaborator

I'll keep this open because eventually I'd like to expand GraphMakie in that direction. This won't happen to soon though.

There are some things which need to be done

  • implement some kind of textbox recipe which will take the node labels and places them in shapes drawn as poly
  • potentially update some layouts that can handle a "node size" parameter (avoid placing nodes to close to each other which would lead to overlapping polys)
  • think about anchors on the textboxes, ie where should the edges start and end

Finally i am not sure whether this can be achieved as part of the graphplot recipe. Maybe it would be wise to create a new recipe for that.

@hexaeder hexaeder reopened this Feb 17, 2022
@roland-KA
Copy link
Author

Oh, it would be fantastic, if you could realize these plans. I'm looking forward to it.

@greimel
Copy link
Collaborator

greimel commented Mar 16, 2022

I needed something similar, so I've written code for it. It's only good for one character labels, though. And doesn't cover anything that @hexaeder has thought about above: No interaction, no handling of overlapping nodes, not even proper text boxes.

I thought I share it here, in case somebody finds this useful.

image

image

code
# ╔═╡ 0de55944-f737-4a4f-849d-3a925b1eceb3
using Makie: automatic, Figure, Axis, hidedecorations!, hidespines!, text!

# ╔═╡ 7fd33644-b26a-439b-8a63-29535abc1dd3
using GraphMakie: graphplot!

# ╔═╡ c2bf3f66-b017-44ac-844e-cd2350828ddb
using NetworkLayout: Spring

# ╔═╡ 40ef5e6a-5b73-4963-bc71-5faad1948081
using Graphs: vertices, nv

# ╔═╡ 9fbfe652-e31b-4a09-99ed-62db24bc03c5
function numbered_graphplot(g; minimal = true, figure = (;), axis = (;), kwargs...)
	fig = Figure(; figure...)
	
	ax = Axis(fig[1,1]; axis...)
	
	numbered_graphplot!(ax, g; minimal, kwargs...)

	
	fig
end

# ╔═╡ a5be84d8-d9d5-4c1d-8b2c-7eca3857f2fb
function numbered_graphplot!(ax, g; 
	minimal=false, extend_limits=automatic,
	nlabels = vertices(g),
	layout = Spring(), node_color = (:lightgray, 1), node_attr = (;), kwargs...)
	n = nv(g)
	
	node_attr = (; node_attr..., marker = :circle, color = node_color, strokewidth = 1, markersize = n < 10 ? 35 : 45)

	if minimal
		hidedecorations!(ax)
		hidespines!(ax)
	end

	if extend_limits !== automatic
		positions = layout(g)
		ax.limits[] = extended_box(positions, extend_limits)
	else
		ax.limits[] = (nothing, nothing, nothing, nothing)
	end
	
	#poly!(ax, Circle.(layout(g), 0.07px), color = (:white, 0))
	graphplot!(ax, g; node_attr, layout, kwargs...) #node_marker = '1':'5')

	text!(ax, string.(nlabels); position=layout(g), align = (:center, :center))
end

# ╔═╡ 0442bc7c-9c74-4325-9f8f-5f77b169a495
md"""
## Utils
"""

# ╔═╡ da154cf8-0939-4505-a8b7-3cb979a0a371
function extended_extrema(points, margin = 0.05)
	min, max = extrema(points)
	ε = (max - min) * margin

	if ε > 0
		return min - ε, max + ε
	else
		return nothing, nothing
	end
end

# ╔═╡ 6cb5f932-ee5c-40db-bed7-ec592532cfff
function extended_box(points, margin = 0.05)
	(extended_extrema(first.(points), margin)..., 
	 extended_extrema(last.(points), margin)...)
end

# ╔═╡ c05bf8a6-d5db-43d7-a855-ed1a98d71057
md"""
## Tests
"""

# ╔═╡ 53ac8dd8-26d1-4c8b-ba50-580a7086dbf4
using Graphs: complete_digraph, cycle_graph, cycle_digraph

# ╔═╡ d10d10bf-ae56-480c-b60c-232c0e0ad3ce
using NetworkLayout: Shell

# ╔═╡ fbd2ab88-7f46-4e64-8954-99539a7fb75e
using CairoMakie: DataAspect

# ╔═╡ 5554c7be-7e00-47c1-b1cb-fc11c84acd9f
begin
	g = complete_digraph(5)
	
	fig = Figure(font = "CMU")
	ax = Axis(fig[1,1], aspect = DataAspect())
	
	#hidedecorations!(ax)

	numbered_graphplot!(ax, g; layout = Shell(), arrow_size = 15)

	fig
end

# ╔═╡ 3a5e4d4e-204f-44d4-924f-9d85abef79c1
let
	g = cycle_digraph(5)
	
	numbered_graphplot(g, arrow_size = 15)
end

# ╔═╡ 700d5452-c2ce-4ab1-aa40-2a8ffaa0f6bf
let
	g = cycle_graph(5)
	
	numbered_graphplot(g, nlabels = 'A':'E')
end

@roland-KA
Copy link
Author

Ah that looks really good! ... and demonstrates again the need for such solutions.

I'm currently playing around with Plots and GraphRecipes, using the TreePlot recipe for plotting trees. But some variants have serious issues (JuliaPlots/GraphRecipes.jl#172) and even the variants that work have some potential for improvement. E.g. the edges connecting the nodes do not attach exactly to the node boundaries. You can control them a bit using the shorten parameter. But on some nodes they don't connect at all and on other nodes they reach inside the node box (see: JuliaForMLTutorial; there "ML-Tutorial 3 .." in folder "notebooks" for some examples).

@asinghvi17
Copy link
Member

asinghvi17 commented May 11, 2022

@greimel, it seems like you only need to get the boundingbox and maximum width of the text. I suggest the following basic modification, which allows you to automatically find the text size:

Code
function text_bbox(textstring::AbstractString, textsize::Union{AbstractVector, Number}, font, align, rotation, justification, lineheight)
    glyph_collection = Makie.layout_text(
            textstring, textsize,
            font, align, rotation, justification, lineheight,
            RGBAf(0,0,0,0), RGBAf(0,0,0,0), 0f0
        )

    return Rect2f(Makie.boundingbox(glyph_collection, Point3f(0), Makie.to_rotation(rotation)))
end

# ╔═╡ a5be84d8-d9d5-4c1d-8b2c-7eca3857f2fb
function numbered_graphplot!(ax, g; 
	minimal=false, extend_limits=automatic,
	nlabels = vertices(g),
	layout = Spring(), node_color = (:lightgray, 1), node_attr = (;), node_markersize = automatic, node_marker = :rect, node_font = Makie.defaultfont(), node_textsize = 16, kwargs...)
	n = nv(g)

    # Extract text sizes and layout accordingly
    label_sizes = [widths(text_bbox(string(label), node_textsize, node_font, (:center, :center), 0f0, 0, 0)) for label in nlabels]

    # label_diags = collect(sqrt.(sum.([label_size .^ 2 for label_size in label_sizes])))
    # max_size = maximum(first.(label_sizes))# .+ node_textsize

    # We can specify markersize as a Vec2f, which is the eltype of label_sizes.
    # Thus, we can explicitly cause the node drawing to be large enough to accomodate
    # the marker size.
	node_attr = (; 
        node_attr...,
        marker = node_marker, color = node_color, 
        strokewidth = 1, markersize = node_markersize == automatic ? map(x -> x .+ node_textsize, label_sizes) : node_markersize, 
        markerspace = :pixel,
    )

	if minimal
		hidedecorations!(ax)
		hidespines!(ax)
	end

	if extend_limits !== automatic
		positions = layout(g)
		ax.limits[] = extended_box(positions, extend_limits)
	else
		ax.limits[] = (nothing, nothing, nothing, nothing)
	end
	
	#poly!(ax, Circle.(layout(g), 0.07px), color = (:white, 0))
	graphplot!(ax, g; node_attr, layout, kwargs...) #node_marker = '1':'5')

	text!(ax, string.(nlabels); position=layout(g), font = node_font, textsize = node_textsize, align = (:center, :center))
end

Basically, what this does is it computes the text's bounding box by using Makie's layout utilities, and passes the computed widths to markersize.

This means that all markers are sized independently.

With this,

let
    g = cycle_digraph(15)
    numbered_graphplot(g, arrow_size = 15, nlabels = string.(100 .* (1:15)))
end

iTerm2 Ze2xHw

Setting node_marker=:circle, the layout is a little tighter, but still theoretically good. There are definitely a lot of ways you can play with the markersize in order to make it look as good as possible.
iTerm2 Rl5KO0

@greimel
Copy link
Collaborator

greimel commented Jun 9, 2022

@asinghvi17 thanks, that looks great! Is there a way to have proper circles (non-squished) with your way?

@asinghvi17
Copy link
Member

Yea, just select the maximum width instead of using both :)

@greimel
Copy link
Collaborator

greimel commented Jun 9, 2022

Phantastic, thanks!

@greimel
Copy link
Collaborator

greimel commented Jun 10, 2022

@hexaeder This looks very good, doesn't it? Still it doesn't address all of your concerns above.

There are some things which need to be done

  • implement some kind of textbox recipe which will take the node labels and places them in shapes drawn as poly
  • potentially update some layouts that can handle a "node size" parameter (avoid placing nodes to close to each other which would lead to overlapping polys)
  • think about anchors on the textboxes, ie where should the edges start and end

What do you think about this now? Also, what about

Finally i am not sure whether this can be achieved as part of the graphplot recipe. Maybe it would be wise to create a new recipe for that.

One could integrate this using existing keywords. E.g.

nlabel_align = :inside
node_size = :text_size

Do would you think about a draft PR that tries to integrate this into GraphMakie?

@hexaeder
Copy link
Collaborator

I think this would be a great addition, even without having all the features. I think there are good reasons for both: extending the current recipe and creating a second recipe.

Extending the recipe comes with a less clear interface. For example, what should happen if you combine nlabels_align = (:top, :center) with node_size = :text_size? This means that you really have to know your keywords to achieve the style. Having a new recipe would help in providing sane defaults for this specific kind of graphplot, something like textboxgraphplot(g, nlabels; kwargs...) could work out of the box.

On the other hand, the different parts of the graph plots need to be more modular in order to play well with different root recipes. I started this by creating a separate edgeplot recipe which is used internally, but for example the edge labels should move to that recipe as well, otherwise we'll end up with code duplication.

@hdavid16
Copy link
Collaborator

@greimel @asinghvi17 are either of you planning on writing a PR for this?

@Nosferican
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants