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:
- redundancy: the legend is needlessly duplicated for each x-axis interval
- 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
An example plot with more representative and meaningful data
Probably not important, but maybe this helps.