Errorline like plot for histograms

I would like to create an envelope plot for multiple histogram (input as matrix) something like what is done with errorline in StatsPlots. (I did not find a Makie equivalent of errorline).

Basically, what I want would look like

dist = Exponential()
N = 100 # number of sample used in each histogram
N_hist = 1000 # number of histogram
yc = rand(dist, N_hist, N)
begin 
    plot()
    [stephist!(y, label = :none, c = :gray, alpha = 0.1, bins = 0:1:15) for y in eachrow(yc)]
    plot!()
end

In this example, the “interval” is generated with each of the 1000 stephist lines. This is a bit heavy to display and it would be better to have a fill between the min and max.

Using the erroline I tried building a plot receipt but something is wrong

@userplot ErrorLineHist

Plots.group_as_matrix(g::ErrorLineHist) = true

@recipe function f(p::ErrorLineHist)
    _, v = StatsPlots.grouped_xy(p.args...)
    bins = get(plotattributes, :bins, :auto)
    normed = get(plotattributes, :normalize, false)
    weights = get(plotattributes, :weights, nothing)

    # compute edges from ungrouped data
    h = Plots._make_hist((vec(copy(v)),), bins; normed = normed, weights = weights)
    nbins = length(h.weights)
    edges = h.edges[1]
    bar_width --> mean(map(i -> edges[i + 1] - edges[i], 1:nbins))
    x = map(i -> (edges[i] + edges[i + 1]) / 2, 1:nbins)

    ngroups = size(v, 2)
    ntot = count(x -> !isnan(x), v)
    # compute weights (frequencies) by group using those edges
    y = fill(NaN, nbins, ngroups)
    for i in 1:ngroups
        v_i = filter(x -> !isnan(x), v[:, i])
        w_i = isnothing(weights) ? nothing : weights[groupinds]
        h_i = Plots._make_hist((v_i,), h.edges; normed = false, weights = w_i)
        StatsBase.normalize!(h_i, mode = Plots._hist_norm_mode(normed))
        y[:, i] .= h_i.weights
    end

    StatsPlots.ErrorLine((x, y))
end
errorlinehist!(yc, label = "not quite that")

Not sure what I missed. Does someone have an idea? (or know if similar feature is already implemented somewhere)?

(uploading does not work currently for me)

Hmm, it looks like you want something like a fan plot?

If all you want is some plot at each x coordinate, that’s pretty easy to do via Makie’s SpecAPI.

If I understand, what you are refering to is equivalent (maybe a bit fancier) to errorline in StatsPlots?
What I would like is build a receipt to convert a matrix pxN into a p histograms and then represent with fan plot or errorlines. Ideally, this receipt would accept the same keywords as histograms and errolines.

Do you have a reference to a visualization in the wild? in hep we take envelope of histograms very often but I haven’t seen a very canonical way to visualize the “envelope”

Usually I think what people do is after take per-bin minimal/maximum, we simply plot the final spread as the error using shading or something, see the bottom of this example page: Makie Plotting · FHist.jl

Unfortunately, I don’t find a good reference.

I agree that the per bin min/max is what people do. I was just looking for a method to do that. I tried FHist.jl but could not get exactly what I wanted with StackHist (which BTW error when staking more than 7 hist)

1 Like

that’s because the Wong() color only has 7 colors, if you need more you need to supply the colors manually.

I think I got my Plots.jl recipe to work in case someone need that.
Mostly permutation of the input was needed.

# grouped histogram
@userplot ErrorLineHist

Plots.group_as_matrix(g::ErrorLineHist) = true

@recipe function f(p::ErrorLineHist)
    _, v = StatsPlots.grouped_xy(p.args...)
    v = permutedims(v)
    bins = get(plotattributes, :bins, :auto)
    normed = get(plotattributes, :normalize, false)
    weights = get(plotattributes, :weights, nothing)

    vs = filter.(isfinite, (v,))
    edges = Plots._hist_edges(vs, bins)
    nbins = length(edges[1]) .- 1
    x = map(i -> (edges[1][i] + edges[1][i + 1]) / 2, 1:nbins)

    ngroups = size(v, 2)

    # compute weights (frequencies) by group using those edges
    y = zeros(nbins, ngroups)
    for i in 1:ngroups
        v_i = filter(isfinite, v[:, i])
        w_i = weights #isnothing(weights) ? nothing : weights[groupinds]
        h_i = Plots._make_hist((v_i,), edges; normed = normed, weights = w_i)
        y[:, i] += h_i.weights
    end

    StatsPlots.ErrorLine((x, y))
end

Then this works i.e. enveloppe is the same as all plots.

dist = Exponential()
N = 100 # number of sample used in each histogram
N_hist = 1000 # number of histogram
yc = rand(dist, N_hist, N)

edges = 0:0.05:11.1 # to guarantee same bins (histogram looks different for different bins)
begin 
    p = plot()
    [stephist!(y, label = :none, c = :gray, alpha = 0.1, norm = :pdf, bins = edges) for y in eachrow(yc)]
    errorlinehist!(yc, label = "it works", errortype = :percentile, percentiles = [0,100], fillalpha = 0.1, normalize = :pdf, bins = edges)
end