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

Feature request: contour labels #832

Closed
briochemc opened this issue Feb 1, 2021 · 22 comments · Fixed by #2496
Closed

Feature request: contour labels #832

briochemc opened this issue Feb 1, 2021 · 22 comments · Fixed by #2496

Comments

@briochemc
Copy link
Contributor

I don't think one can annotate contours with values in Makie (yet), for example (matplotlib):

I could not find an existing issue for this, so I thought I'd create one! 😃


FWIW, I only found this discourse post with a potential workaround, with this plot:

@jkrumbiegel
Copy link
Member

For this we would need this PR JuliaPlots/AbstractPlotting.jl#430 to be merged, which is still stuck at the GLMakie implementation

@carstenbauer
Copy link
Contributor

Came up on discourse again: https://discourse.julialang.org/t/how-to-add-value-labels-on-top-of-contours-in-makie/59242

Now that #430 is merged this should be possible somehow?

@SimonDanisch
Copy link
Member

Yeah it does work nicely now:

using GLMakie

np = 500
nc = 200
f(x) = 2*x+1
x = LinRange(-1,1,f(np))
y = rand(f(np))*2 .- 1
x_c = LinRange(-nc/np,nc/np,f(nc))
y_c = LinRange(-nc/np,nc/np,f(nc))
c_mat = @. x_c^2 + y_c'^2
fig, ax, hp = heatmap(x_c, y_c, c_mat)
cplot = contour!(ax,x_c,y_c,c_mat, linewidth=2, color=:white)

beginnings = Point2f0[]; colors = RGBAf0[]
# First plot in contour is the line plot, first arguments are the points of the contour
segments = cplot.plots[1][1][]
for (i, p) in enumerate(segments)
    # the segments are separated by NaN, which signals that a new contour starts
    if isnan(p)
        push!(beginnings, segments[i-1])
    end
end
sc = scatter!(ax, beginnings, markersize=50, color=(:white, 0.1), strokecolor=:white)
translate!(sc, 0, 0, 1)
# Reshuffle the plot order, so that the scatter plot gets drawn before the line plot
delete!(ax, sc)
delete!(ax, cplot)
push!(ax.scene, sc)
push!(ax.scene, cplot)
anno = text!(ax, [(string(i), p) for (i, p) in enumerate(beginnings)], 
                   align=(:center, :center))
                    
# move, so that text is in front
translate!(anno, 0, 0, 2)

display(fig)

image

But this approach only works nicely with glmakie right now...

@SimonDanisch
Copy link
Member

For CairoMakie transparent scatters don't block the line from being drawn, so it would need to be opaque:
image

@aramirezreyes
Copy link
Contributor

aramirezreyes commented Apr 14, 2021

There is a missing piece in this solution: Adding a colorbar shows that the labels do not correspond to the values in the contour:

fig = Figure(backgroundcolor =RGBf0(0.98, 0.98, 0.98))
ax = fig[1,1] = Axis(fig, title = "Heatmap and contours")

np = 500
nc = 200
f(x) = 2*x+1
x = LinRange(-1,1,f(np))
y = rand(f(np))*2 .- 1
x_c = LinRange(-nc/np,nc/np,f(nc))
y_c = LinRange(-nc/np,nc/np,f(nc))
c_mat = @. x_c^2 + y_c'^2
hp = heatmap!(x_c, y_c, c_mat)
cplot = contour!(ax,x_c,y_c,c_mat, linewidth=2, color=:white)

beginnings = Point2f0[]; colors = RGBAf0[]
# First plot in contour is the line plot, first arguments are the points of the contour
segments = cplot.plots[1][1][]
for (i, p) in enumerate(segments)
    # the segments are separated by NaN, which signals that a new contour starts
    if isnan(p)
        push!(beginnings, segments[i-1])
    end
end
sc = scatter!(ax, beginnings, markersize=50, color=(:white, 0.1), strokecolor=:white)
translate!(sc, 0, 0, 1)
# Reshuffle the plot order, so that the scatter plot gets drawn before the line plot
delete!(ax, sc)
delete!(ax, cplot)
push!(ax.scene, sc)
push!(ax.scene, cplot)
anno = text!(ax, [(string(i), p) for (i, p) in enumerate(beginnings)], 
                   align=(:center, :center))
                    
# move, so that text is in front
translate!(anno, 0, 0, 2)


cbar = fig[1,2] = Colorbar(fig,hp,label = "Values")
cbar.width = 15
cbar.height = Relative(4/5)


display(fig)

image

@jkrumbiegel
Copy link
Member

I think they are just numbers from 1 to n for testing

@aramirezreyes
Copy link
Contributor

aramirezreyes commented Apr 14, 2021

Ah yes, thanks. Just wanted to point out that this is not the solution to the feature request yet but is a stepping stone. For some other person that arrives to this issue while looking for labeled contours.

@aramirezreyes
Copy link
Contributor

Is there an easy way to extract the values each segment corresponds to in the snippet above so that we can create the labels accordingly?

@briochemc
Copy link
Contributor Author

Here is to a gentle bump! It would be great to see a MWE that looks good with CairoMakie 😄 (for printing to PDF quality!)

@briochemc
Copy link
Contributor Author

I just tried to reproduce this workaround using Makie v0.15.0 with GLMakie v0.4.4, but copy-pasting the codes above do not reproduce the plot for me. The annotations don't show for some reason:

Screen Shot 2021-08-02 at 1 13 09 pm

FWIW I'm on OSX so maybe that's an issue?

@briochemc
Copy link
Contributor Author

FWIW, reshuffling the order more (including the annotations) fixed that part for me. Here is a slightly smaller MWE:

using GLMakie

x = range(-3, 3, length=200)
y = range(-2, 2, length=100)
z = @. x^2 + y'^2
fig, ax, hp = heatmap(x, y, z)
levels = 0:1:100
cplot = contour!(ax, x, y, z, color=:black, levels=levels)

beginnings = Point2f0[]; colors = RGBAf0[]
# First plot in contour is the line plot, first arguments are the points of the contour
segments = cplot.plots[1][1][]
for (i, p) in enumerate(segments)
    # the segments are separated by NaN, which signals that a new contour starts
    if isnan(p)
        push!(beginnings, segments[i-1])
    end
end
sc = scatter!(ax, beginnings, markersize=30, color=(:white, 0.001), strokecolor=:white)
anno = text!(ax, [(string(float(i)), Point3(p..., 2f0)) for (i, p) in enumerate(beginnings)], align=(:center, :center), color=:black)
translate!(sc, 0, 0, 1)
translate!(anno, 0, 0, 2)
# Reshuffle the plot order, so that the scatter plot gets drawn before the line plot
delete!(ax, sc)
delete!(ax, cplot)
delete!(ax, anno)
push!(ax.scene, anno)
push!(ax.scene, sc)
push!(ax.scene, cplot)
# move, so that text is in front

fig

gives me (with GLMakie)

Screen Shot 2021-08-02 at 2 28 11 pm

Maybe a list of remaining issues is useful?

  • making it work with CairoMakie
  • annotating with the level value (not just the iterator) as asked by @aramirezreyes
  • clipping the contours more accurately than using circles (maybe depending on the label to be displayed, use a rectangle matching the pixel size of the contour label?)
  • Improving contour label placement
  • porting the workaround into a function/recipe/kwarg

@aramirezreyes
Copy link
Contributor

aramirezreyes commented Oct 25, 2021

I worked a little bit in a workaround which I think is not elegant at all but It kind of works:

using CairoMakie
function name_contours!(ax,cplot,value)
    beginnings = Point2f0[]; colors = RGBAf0[]
    # First plot in contour is the line plot, first arguments are the points of the contour
    segments = cplot.plots[1][1][]
    #@info segments[1]
    for (i, p) in enumerate(segments)
        # the segments are separated by NaN, which signals that a new contour starts
        if isnan(p)
            push!(beginnings, segments[i-1])
        end
    end
    sc = scatter!(ax, beginnings, markersize=30, color=(:white, 0.1), strokecolor=:white)
    translate!(sc, 0, 0, 1)
    # Reshuffle the plot order, so that the scatter plot gets drawn before the line plot
    delete!(ax, sc)
    delete!(ax, cplot)
    push!(ax.scene, sc)
    push!(ax.scene, cplot)
    anno = text!(ax, [(string(value), p) for (i, p) in enumerate(beginnings)], 
                       align=(:center, :center), textsize=10)

    # move, so that text is in front
    translate!(anno, 0, 0, 2) 
end

x = range(-3, 3, length=200)
y = range(-2, 2, length=100)
z = 10(@. x^2 + y'^2)
fig, ax, hp = heatmap(x, y, z)
levels = 0:10:100
for contour_value in levels
        contour_plot = contour!(ax,x,y, z, color = :black, levels = [contour_value])
        name_contours!(ax,contour_plot,contour_value)
end
fig

This code produces:

index

This the second point in @briochemc's list by calling contour on each specified level
and doesn't solve any of the other four.

It is not much, but it is honest work.

@ericphanson
Copy link
Contributor

gentle bump to say this would be nice to have in Makie!

@t-bltg
Copy link
Collaborator

t-bltg commented Dec 13, 2022

This would be a great addition.

Here is the workaround adapted to latest Makie:

using CairoMakie

name_contours!(ax, cplot, value) = begin
  beginnings = Point2f[]; colors = RGBAf[]

  # first plot in contour is the line plot, first arguments are the points of the contour
  segments = cplot.plots[1][1][]
  # @info segments[1]
  for (i, p) in enumerate(segments)
    # the segments are separated by NaN, which signals that a new contour starts
    isnan(p) && push!(beginnings, segments[i-1])
  end
  sc = scatter!(ax, beginnings, marker = :rect, markersize=20, color=(:white, .1), strokecolor=:white)
  translate!(sc, 0, 0, 1)

  # reshuffle the plot order, so that the scatter plot gets drawn before the line plot
  delete!(ax, sc)
  delete!(ax, cplot)
  push!(ax.scene, sc)
  push!(ax.scene, cplot)
  ann = text!(ax, [(string(value), p) for (i, p) in enumerate(beginnings)], align=(:center, :center), fontsize=10)

  # move, so that text is in front
  translate!(ann, 0, 0, 2)
end

main() = begin
  cases = (
    paraboloid = (x, y) -> 10(x^2 + y^2),
    # en.wikipedia.org/wiki/Himmelblau%27s_function
    himmelblau = (x, y) -> (x^2 + y - 11)^2 + (x + y^2 - 7)^2,
    # en.wikipedia.org/wiki/Rosenbrock_function
    rosenbrock = (x, y) -> (1 - x^2) + 100(y - x^2)^2,
  )

  x = range(-3, 3; length=100)
  y = range(-3, 3; length=200)
  z = cases[1].(x, y')

  fig, ax, hm = heatmap(x, y, z; interpolate = true)
  Colorbar(fig[1, 2], hm)

  for level  0:10:100
    contour_plot = contour!(ax, x, y, z, color=:black, levels=[level])
    name_contours!(ax, contour_plot, level)
  end

  save("contour_labels.png", fig)
  fig
end

main()

@jkrumbiegel
Copy link
Member

I think the tricky part is that after text locations have been chosen, the contour lines have to be hidden where they intersect the label bounding boxes. But this calculation needs to happen in screen space, so it needs to update whenever the projection changes.

@t-bltg
Copy link
Collaborator

t-bltg commented Dec 13, 2022

Is there a way to get the plot background color (here the heatmap color) so that we can add a label with a custom opaque background allowing to hide the contour line ? That would allow an exact bounding box based on the label fontsize...

@jkrumbiegel
Copy link
Member

jkrumbiegel commented Dec 13, 2022

Often, contour lines are plotted on top of filled contours or heatmaps, so this would look bad in all those cases. I think for a good solution, you can't get around removing the actual lines.. (It would be enough to NaN all unwanted points I guess)

@jkrumbiegel
Copy link
Member

If anyone wants to start playing around with this, my approach would be:

  • don't pass computed contour lines directly to lines plot element, make another observable first which will hold a copy of the arrays with NaNs where the lines should be hidden
  • compute text positions and strings, add those to a new text object.
  • listen to projection matrix of parent scene, compare to hvlines or so for implementation
  • whenever projection, input data, any text style data that influences boundingbox like fontsize, change, compute all label boundingboxes, convert from screen coordinates into data coordinates using current projection, NaN out all points in all contours in the copied observable that intersect with those boundingboxes

@t-bltg
Copy link
Collaborator

t-bltg commented Dec 13, 2022

Thanks for the comments, this is exactly what I'm doing at the moment (but lower level, not using scatter or translate ...).

@jkrumbiegel
Copy link
Member

you can get the boundingboxes for each separate text by going into the text child plot object with a Vector{GlyphCollection} as input (textplotobj.plots[1]) and calling boundingbox on each glyphcollection in there

@jkrumbiegel
Copy link
Member

don't know if there's currently an easier, official way

@t-bltg
Copy link
Collaborator

t-bltg commented Dec 13, 2022

Great, yeah I can make the labels work now, but masking is a bit trickier because of those bounding boxes...
I think I'm going to try an unefficient way before trying to use text!(plot, all_labels)...

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

Successfully merging a pull request may close this issue.

7 participants