Having a hard time with Makie Recipes

Current Struggle

I am currently building an extension for both Makie.jl and Plots.jl. While I have managed to get it working in Plots.jl, I have not had the same pleasant experience with Makie. I keep coming back to it and continue to run into issues.

The package I hope to create is found here: Dimensions.jl

Since the objects that I want to create recipes for are relatively straight forward, I will use them as the mwe…

Summary of Package

The package basically is centered around two structs and one is contained within the other.

 struct TopDimensions
    xs # vector of floats
    ys # vector of floats
    labels # see Labels struct below
    minor_lines # will be mapped to yerror bars (high)
    major_lines # will be mapped to yerror bars (low)
end

struct Labels
    xs # vector of floats
    ys # vector of floats
    lbls # vector of strings
end

The idea is to plot dimension lines like so:

And you can see it is working with Plots.jl.

Here were the recipes used to create the dimensions shown:

TopDimensions Recipe

@recipe function f(dims::TopDimensions; with_mask=true, dim_color=:black) 
    
    legend := false

    # points for dimensions
    dim_xs, dim_ys = dims.xs, dims.ys

    # plot dimensions
    @series begin
        seriestype  :=  :path
        linecolor := dim_color
        markercolor := dim_color
        yerror --> (dims.major_lines, dims.minor_lines)
        markersize := 0
        
        dim_xs, dim_ys
    end

    # plot labels
    @series begin
        dims.labels
    end
end

Labels Recipe

@recipe function f(lbls::Labels; with_mask=true, font_color=:black, font_size=5)
    
    legend := false

    # points for labels
    x_lbls, y_lbls, annos = labels_for_plots(lbls, with_mask, font_color, font_size)

    # plot labels
    @series begin
        seriestype:= :scatter
        markersize := 0
        annotations --> annos
        x_lbls, y_lbls
    end
end

I can then do plot!(top_dims) and I will see my dimensions plotted on top of the things I want to dimension.

Need Help

I have looked through the Makie docs and I believe I have the ext folder/ Project.toml file set up properly, but I don’t really have an idea of how to get the recipes working. Could anyone provide help or a solution?

Thanks

Check the Makie recipes docs:

For a concrete example, check the MeshesMakie.jl extension:

Thanks,

These were actually the sources I was looking at, but it is pretty hard to wrap my head around it.

Things like this:

function Makie.plot!(plot::Viz{<:Tuple{Mesh}})
  # retrieve mesh and dimensions
  mesh = plot[:object]
  M = Makie.@lift manifold($mesh)
  pdim = Makie.@lift paramdim($mesh)
  edim = Makie.@lift embeddim($mesh)
  vizmesh!(plot, M[], Val(pdim[]), Val(edim[]))
end

It looked like a bunch of Observable stuff and then I wasn’t familiar with $mesh, but now realizing that probably has to do with the @lift macro… Also wasn’t sure what Val was doing. I guess it was a little advanced for me to figure out. I will try to look into it again, but yeah that is kind of a road I have been down already.

  1. Do you think I will need to create a new plot type?

You can dispatch existing recipes with the convert_arguments approach. The new plot type is for when you need to introduce new options or plot objects that require more than a single Makie recipe call.

See also Wrapping existing recipes for new types | Makie or SpecApi | Makie

For what it’s worth, this is not the best example. Here a bunch of observables are created (lifted from existing observables) but then their values unwrapped with [] which removes the observables again.

If you don’t intend to use interactive or otherwise dynamic updates of your data, you can start writing a Makie recipe without observables. Of course the data in the plot! function is always given to you as observables so you have to unwrap them with [] but after that you can continue with the values without any observables complication.

Once you get that working you can still try to make your recipe update on input data changes. Then, instead of unwrapping each input observable and using the raw values inside for your computations, you use lift and friends to make chains of connected values which update each other. This sometimes results in a little more code complexity if you want to avoid double updates etc. but the basic logic of your recipe shouldn’t change.

1 Like

In that case we are unwrapping the values to an internal function for dispatch. You recommend dispatching with observables instead? Our plots work with observables already, the unwrap doesnt affect interactivity as far as I understand it.

Okay I looked into the SpecAPI and was able to come up with something that is plotting, but the SpecApi looks like it may be subject to change.

Here is what is somewhat working:

function Makie.convert_arguments(::Type{<:AbstractPlot}, obj::TopDimensions)
    plots = PlotSpec[]
	xs = obj.xs
	ys = obj.ys

	# major and minor lines can be single digit or vector
	# convert to vector
	minor_lines = xs .* 0 .+ obj.minor_lines
	major_lines = xs .* 0 .+ obj.major_lines

	# Plot dimension lines
    push!(plots, S.Lines(xs, ys; color = :black))

	# plot extension lines
    for (x, y, minor, major) in zip(xs, ys, minor_lines, major_lines)
		err_x = [x, x]
		err_y = [y + minor, y - major]
		push!(plots, S.Lines(err_x, err_y; color = :black))
	end

	# pull label info
	lbl_x = obj.labels.xs
	lbl_y = obj.labels.ys
	annos = obj.labels.lbls

	# create blank labels
	n = length.(annos)
    blanks = vcat("█".^n #=.* "█"=#)

	# plot blank labels
	push!(plots, S.Text(lbl_x, lbl_y; text=blanks, align=(:center, :center), color=:white))

	# plot labels
    push!(plots, S.Text(lbl_x, lbl_y; text=annos, align=(:center, :center)))
    return plots
end

I am then getting dimensions to show up. Awesome!

However, I need to write the code this way:

f = Figure()
ax = Axis(f[1,1], autolimitaspect=1)
lines!.(ax, xx, yy, color=:lightgrey) # plots shapes
plot!(ax, tdd) # plots dimensions
f

I would like to write it like this, but the plot does not show up:

lines!.(ax, xx, yy, color=:lightgrey) # plots shapes
plot!(ax, tdd) # plots dimensions
  1. How would I write this in the original way of writing recipes?
  2. Can the original way result in just one plot!(dimension) call, or do I need to create a custom name like meshes did with viz?

I may create a custom name, but keeping plot seems like the most simple for users.