Showing percentage different between bar of grouped bar chart in Plotly

Hello all,
As you can see in the example below, I would like to label my grouped bar chart with the % differential between each group of bars. Could anyone point me to documentation or examples that would cover that?

Here is the dataframe:

df_hr = DataFrame(
    year = [2017, 2018, 2019, 2020, 2021, 2017, 2018, 2019, 2020, 2021],
    mean_hrpaid = [3.7, 4.08, 4.77, 4.69, 5.04, 6.73, 6.9, 6.91, 6.48, 6.3],
    type = ["Countryside", "Countryside", "Countryside", "Countryside", "Countryside", "SIB", "SIB", "SIB", "SIB", "SIB"]
)

I got the chart with the following code:

p_hr = Plot(df_hr, 
                x=:year, 
                y=:mean_hrpaid,
                color=:type,
                text=:mean_hrpaid,
                kind="bar",
                textposition="auto",
                labels=Dict(
                    :year => "Year",
                    :mean_hrpaid => "Monthly income (\$)",
                    :type => "Area",
                ),
                Layout(barmode = "group", 
                font_family = "Cambria",
                separators = ",",
                font = attr(size=17),
                legend = attr(
                    x=1,
                    y=1.02,
                    yanchor="bottom",
                    xanchor="right",
                    orientation="h"
                )
                )
)

Here is the example chart that I want to get:


Thank all!

julia> span(v) = round(100*(maximum(v) - minimum(v))/maximum(v); digits=2)

julia> df_change = combine(groupby(df_hr, :year), :mean_hrpaid => span => :percent_change)
5×2 DataFrame
 Row │ year   percent_change 
     │ Int64  Float64        
─────┼───────────────────────
   1 │  2017         45.0223
   2 │  2018         40.8696
   3 │  2019         30.9696
   4 │  2020         27.6235
   5 │  2021         20.0

Not sure how to add this to the plot directly however. The best way might be to just have a separate sublot:

p_ch = Plot(df_change, 
                x=:year, 
                y=:percent_change,
                text=:percent_change,
                kind="bar",
                textposition="auto",
                name="% change",
                labels=Dict(
                    :year => "Year",
                    :percent_change => "% change Monthly income (\$)",
                    :type => "Area",
                ),
                )
)

[ p_hr
  p_ch]

3 Likes
code
julia> using CairoMakie, DataFrames

julia> df_hr = DataFrame(
           year = [2017, 2018, 2019, 2020, 2021, 2017, 2018, 2019, 2020, 2021],
           mh = [3.7, 4.08, 4.77, 4.69, 5.04, 6.73, 6.9, 6.91, 6.48, 6.3],
           type = ["Countryside", "Countryside", "Countryside", "Countryside", "Countryside", "SIB", "SIB", "SIB", "SIB", "SIB"]
       )
10×3 DataFrame
 Row │ year   mh       type        
     │ Int64  Float64  String      
─────┼─────────────────────────────
   1 │  2017     3.7   Countryside
   2 │  2018     4.08  Countryside
   3 │  2019     4.77  Countryside
   4 │  2020     4.69  Countryside
   5 │  2021     5.04  Countryside
   6 │  2017     6.73  SIB
   7 │  2018     6.9   SIB
   8 │  2019     6.91  SIB
   9 │  2020     6.48  SIB
  10 │  2021     6.3   SIB

julia> udf=unstack(df_hr,:year,:type,:mh)
5×3 DataFrame
 Row │ year   Countryside  SIB      
     │ Int64  Float64?     Float64? 
─────┼──────────────────────────────
   1 │  2017         3.7       6.73
   2 │  2018         4.08      6.9
   3 │  2019         4.77      6.91
   4 │  2020         4.69      6.48
   5 │  2021         5.04      6.3

julia> span(sib,cs) = @. round(100*(sib - cs)/sib; digits=2)
span (generic function with 1 method)

julia> transform!(udf,[[3,2]].=>[span, .-].=>[:deltapc, :delta])
5×5 DataFrame
 Row │ year   Countryside  SIB       deltapc  delta ⋯
     │ Int64  Float64?     Float64?  Float64  Float ⋯
─────┼───────────────────────────────────────────────
   1 │  2017         3.7       6.73    45.02     3. ⋯
   2 │  2018         4.08      6.9     40.87     2.  
   3 │  2019         4.77      6.91    30.97     2.  
   4 │  2020         4.69      6.48    27.62     1.  
   5 │  2021         5.04      6.3     20.0      1. ⋯
                                     1 column omitted

julia> sdf=stack(udf, [2,3,5])
15×4 DataFrame
 Row │ year   deltapc  variable     value            
     │ Int64  Float64  String       Float64?
─────┼───────────────────────────────────────        
   1 │  2017    45.02  Countryside      3.7
   2 │  2018    40.87  Countryside      4.08
   3 │  2019    30.97  Countryside      4.77
   4 │  2020    27.62  Countryside      4.69
   5 │  2021    20.0   Countryside      5.04
   6 │  2017    45.02  SIB              6.73
   7 │  2018    40.87  SIB              6.9
   8 │  2019    30.97  SIB              6.91
   9 │  2020    27.62  SIB              6.48
  10 │  2021    20.0   SIB              6.3
  11 │  2017    45.02  delta            3.03
  12 │  2018    40.87  delta            2.82
  13 │  2019    30.97  delta            2.14
  14 │  2020    27.62  delta            1.79
  15 │  2021    20.0   delta            1.26

julia> sort!(sdf,:year)
15×4 DataFrame
 Row │ year   deltapc  variable     value            
     │ Int64  Float64  String       Float64?
─────┼───────────────────────────────────────        
   1 │  2017    45.02  Countryside      3.7
   2 │  2017    45.02  SIB              6.73
   3 │  2017    45.02  delta            3.03
   4 │  2018    40.87  Countryside      4.08
   5 │  2018    40.87  SIB              6.9
   6 │  2018    40.87  delta            2.82
   7 │  2019    30.97  Countryside      4.77
   8 │  2019    30.97  SIB              6.91
   9 │  2019    30.97  delta            2.14
  10 │  2020    27.62  Countryside      4.69
  11 │  2020    27.62  SIB              6.48
  12 │  2020    27.62  delta            1.79
  13 │  2021    20.0   Countryside      5.04
  14 │  2021    20.0   SIB              6.3
  15 │  2021    20.0   delta            1.26

julia> fig=Figure();

julia> ax = Axis(
           fig[2,1],
           ylabel = "Montly Income (\$)",
           xlabel = "Year"
       )
Axis with 0 plots:


julia> bar_lbls=begin
               bl=[]
               for r in eachrow(udf)
                       size_arr = "="^Int(trunc(r.delta))
                       push!(bl,r.Countryside,r.SIB,"")
               end
               bl
       end
15-element Vector{Any}:
 3.7
 6.73
  ""
 4.08
 6.9
  ""
 4.77
 6.91
  ""
 4.69
 6.48
  ""
 5.04
 6.3
  ""

julia> barplot!(sdf.year, sdf.value,
               dodge = repeat([1, 2, 1],5),
               stack = repeat([1, 2, 3],5),
               color = repeat([1, 2, 3],5),
               colormap = [:red, :blue,:white],      
               bar_labels=bar_lbls,
               label_rotation= pi/2,
               label_offset=-75,
               label_size=repeat([20,20,15],5),      
               label_color=repeat([:white,:white,:blue],5),
               xticks = 2017:2021
               )
Combined{Makie.barplot, Tuple{Vector{Point{2, Float32}}}}

julia> size_arr = @. "="^Int(trunc(udf.delta))       
5-element Vector{String}:
 "==="
 "=="
 "=="
 "="
 "="

julia> bl=@. "<"*size_arr *"|  " *string(udf.deltapc)*"%"
5-element Vector{String}:
 "<===|  45.02%"
 "<==|  40.87%"
 "<==|  30.97%"
 "<=|  27.62%"
 "<=|  20.0%"

julia> text!(
               bl,
               position = Point2f.([2017,2018,2019,2020,2021].-0.2,5.8),
               color = :black,
               rotation= [pi/2,pi/2,pi/2,pi/2,pi/2], 
               align = (:center, :center)
               )
MakieCore.Text{Tuple{Vector{String}}}

julia> labels = unique(sdf.variable)
3-element Vector{String}:
 "Countryside"
 "SIB"
 "delta"

julia> elements = [PolyElement(polycolor = cgrad(:tab10)[[1,2]][i]) for i in 1:2]
2-element Vector{PolyElement}:
 PolyElement(Attributes with 1 entry:
  polycolor => RGBA{Float64}(0.1216,0.4667,0.7059,1.0))
 PolyElement(Attributes with 1 entry:
  polycolor => RGBA{Float64}(1.0,0.498,0.0549,1.0))  

julia> el=[elements..., PolyElement(color=:black,markercolor = :transparent, polystrokecolor = :transparent, polypoints = Point{2, Float32}[[0.0, 0.4], [0.0, 0.6], [0.5, 0.6], [0.5,1.0], [1.0, 0.5],[0.5,0.0],[0.5,0.4]])]
3-element Vector{PolyElement}:
 PolyElement(Attributes with 1 entry:
  polycolor => RGBA{Float64}(0.1216,0.4667,0.7059,1.0))
 PolyElement(Attributes with 1 entry:
  polycolor => RGBA{Float64}(1.0,0.498,0.0549,1.0))  
 PolyElement(Attributes with 4 entries:
  markercolor => transparent
  polycolor => black
  polypoints => Point{2, Float32}[[0.0, 0.4], [0.0, 0.6], [0.5, 0.6], [0.5, 1.0], [1.0, 0.5], [0.5, 0.0], [0.5, 0.4]]
  polystrokecolor => transparent)

julia> Legend(fig[1,1], el, labels, orientation=:horizontal, tellwidth = false, tellheight = true)        
Legend()

julia> fig

julia> save("normal.png", fig) # size = 800 x 600 px 
CairoMakie.Screen{IMAGE}

elements = [PolyElement(polycolor = [:red,:blue][[1,2]][i]) for i in 1:2]
pos=(udf.Countryside.+udf.SIB)./2
text!(
        bl,
        position = Point2f.([2017,2018,2019,2020,2021].-0.2,pos),
        color = :black,
        rotation= pi/2, # [pi/2,pi/2,pi/2,pi/2,pi/2],
        align = (:center, :center)
        )

2 Likes
Summary
udf=unstack(df_hr,:year,:type,:mh)
span(sib,cs) = @. round(100*(sib - cs)/sib; digits=2)

transform!(udf,[[3,2]].=>[span, .-].=>[:Deltapc, :Delta], 2=>identity=>:Cs2)

sdf=stack(udf, [2,3,5, 6])
sort!(sdf,[:year,:variable])
fig=Figure();
ax = Axis(
    fig[1,1],
    ylabel = "Montly Income (\$)",
    xlabel = "Year",
    title= " Lustrum income",
    titlesize=20
)
bar_lbls=begin
        bl=[]
        for r in eachrow(udf)
                push!(bl,r.Countryside,"", string(trunc(Int, r.Deltapc))*"%",r.SIB)
        end
        bl
end

barplot!(sdf.year, sdf.value,
        dodge = repeat([1, 2, 2, 3],5),
        stack = repeat([1, 2, 3, 4],5),
        color = repeat([1, 2, 3, 4],5),
        colormap = [:red, :white, :gray, :blue],
         bar_labels=bar_lbls,
         label_rotation= pi/2,
         label_offset=-75,
        # label_size=repeat([20,20,15],5),
         label_color=:white,
        xticks = 2017:2021
        )

labels = names(udf)[[2,4,3]]
elements = [PolyElement(polycolor = [:red,:gray,:blue][[1,2,3]][i]) for i in 1:3]
Legend(fig[1,2], elements, labels, orientation=:vertical, tellwidth = true, tellheight = false)
fig
save("lustrum income.png", fig) 

1 Like