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

1 Like

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.