Legend for functions in a separate (stacked) plot

TLDR: In Gadfly, what’s the best way to put a legend for a plot into a separate (stacked) plot instead?

My problem

Current solution

I have a module like this for a specific plotting use case:

module Plotting

using Gadfly

const function_samples = 2500

plot_err_layer_func(fun::Fun, x_min::Float64, x_max::Float64) where {Fun <: Function} =
  layer(
    y = [fun], xmin = [x_min], xmax = [x_max],
    Stat.func(num_samples = function_samples), Geom.line(),
    color = [colorant"blue"],
  )

plot_err_layer_max(y_intercept::Float64) =
  layer(Geom.hline(color = [colorant"red"]), yintercept = [y_intercept])

y_label(::Val{:first}, ::Val{:error}) = "approximation error"
y_label(::Val{:first}, ::Val{:value}) = "function value"
y_label(::Val{:other}, ::Val) = nothing

y_ticks(::Val{:first}) = true
y_ticks(::Val{:other}) = false

plot_err_layers(
  fun::Fun, y_intercept::Real, coord, y_lab::Val, itv::NTuple{2, <:Real},
) where {Fun <: Function} =
  plot(
    plot_err_layer_func(Float64 ∘ fun, map(Float64, itv)...),
    plot_err_layer_max(Float64(y_intercept)),
    coord,
    Guide.xlabel(nothing),
    Guide.xticks(label = false),
    Guide.ylabel(y_label(y_lab, Val(:error)), :vertical),
    Guide.yticks(label = y_ticks(y_lab)),
  )

plot_val(
  fun::Fun1, app::Fun2, y_lab::Val, itv::NTuple{2, <:Real},
) where {Fun1 <: Function, Fun2 <: Function} =
  plot(
    y = [Float64 ∘ fun, Float64 ∘ app],
    xmin = [Float64(first(itv))], xmax = [Float64(last(itv))],
    Stat.func(num_samples = function_samples), Geom.line(),
    Guide.xlabel(nothing),
    Guide.ylabel(y_label(y_lab, Val(:value)), :vertical),
    Guide.yticks(label = y_ticks(y_lab)),
    Guide.colorkey(
      title = "Legend", labels = ["accurate function", "approximation"],
    ),
  )

plot_coordinates(y_max::Real) =
  Coord.cartesian(ymin = zero(Float64), ymax = Float64(y_max * 1.02))

const file_prefix = "/tmp/plots"

svg_file(id::AbstractString) = SVG(string(file_prefix, "/", id, ".svg"), 20cm, 16cm)

write_plot(p, id::String) = draw(svg_file(id), p)

function write_plot(
  fun::Fun1, app::Fun2, fun_err::Fun3,
  itvs::AbstractVector{<:NTuple{2, <:Real}}, y_intercept::Real,
  id::String,
) where {Fun1 <: Function, Fun2 <: Function, Fun3 <: Function}
  co = plot_coordinates(y_intercept)

  plots_leftmost = vcat(
    plot_val(fun, app, Val(:first), first(itvs)),
    plot_err_layers(fun_err, y_intercept, co, Val(:first), first(itvs)),
  )

  plots_top = permutedims([
    plot_val(fun, app, Val(:other), itvs[begin + i])
    for i ∈ 1:(length(itvs) - 1)
  ])

  plots_bottom = permutedims([
    plot_err_layers(fun_err, y_intercept, co, Val(:other), itvs[begin + i])
    for i ∈ 1:(length(itvs) - 1)
  ])

  plots_main = gridstack(hcat(plots_leftmost, vcat(plots_top, plots_bottom)))

  write_plot(plots_main, id)
end

end

When I use the module like this, for example (don’t interpret the data, this is just an example):

Plotting.write_plot(sind, cosd, tand, [(-3, 2), (5, 7), (9, 15)], 1.0, "foo")

I get this result (except in SVG):

Issues with the current solution

There’s two issues:

  1. redundancy: the legend is needlessly duplicated for each x-axis interval
  2. Each legend causes their respective plot to not be aligned as I want it to: I want each x-axis tick to be aligned with a corresponding x-axis tick in the plot below, however this doesn’t work because there’s no legend in the plot below

What I want instead

I believe a visually pleasing solution would be to remove each legend, and add a single one above the entire grid. What’s the best/easiest way to do this? If someone can think of a prettier solution that’s a bonus :grin:

An example plot with more representative and meaningful data

Probably not important, but maybe this helps.

facet plots? Compositing · Gadfly.jl

Oh, so the idea is to do away with Stat.func and sample my functions myself, which enables me to use xgroup and ygroup? Thank you for the idea.

In the end I realized there’s no natural/pretty place to put a legend, so, if I want my grid to look nice, I have to simply not have a legend. So that’s what I did, by using layers instead of an array of functions for Stat.func.

Faceting is then also not required.