Skip to content

Multi-tier spanners

The following example shows how layouts with several tiers of column spanners can be achieved.

The data

We will use the PalmerPenguins data.

julia
using DataFrames, Chain, StyledTables
using Statistics: mean

df = DataFrame(StyledTables.penguins())

describe(df)
7×7 DataFrame
Rowvariablemeanminmedianmaxnmissingeltype
SymbolUnion…AnyUnion…AnyInt64DataType
1speciesAdelieGentoo0String
2islandBiscoeTorgersen0String
3bill_length_mm43.992832.144.559.60Float64
4bill_depth_mm17.164913.117.321.50Float64
5flipper_length_mm200.967172197.02310Int64
6body_mass_g4207.0627004050.063000Int64
7sexfemalemale0String

Our goal is to summarise the data by island, species, and sex.

julia
bill_cols = [:bill_length_mm, :bill_depth_mm]
number_cols = [string.(bill_cols)..., "flipper_length_mm"] # , "body_mass_g"
male_ordered = ["male_" * colname for colname in number_cols]
female_ordered = ["female_" * colname for colname in number_cols]

summary = @chain df begin
    select(_, :island, :species, :sex, number_cols...)
    dropmissing(_)
    groupby(_, [:island, :species, :sex])
    combine(_, Cols(r"_mm$|_g$") .=> mean => identity)
    stack(_, number_cols)
    transform(_, [:sex, :variable] => ByRow((s, v) -> join([s, v], "_")) => :sex_variable)
    select(_, Not(:sex, :variable))
    unstack(_, :sex_variable, :value)
    transform(_, :island => ByRow(x -> "$x Is.") => identity)
    select(_, :island, :species, male_ordered..., female_ordered...)
    sort(_, :island)
end
5×8 DataFrame
Rowislandspeciesmale_bill_length_mmmale_bill_depth_mmmale_flipper_length_mmfemale_bill_length_mmfemale_bill_depth_mmfemale_flipper_length_mm
StringStringFloat64?Float64?Float64?Float64?Float64?Float64?
1Biscoe Is.Adelie40.590919.0364190.40937.359117.7045187.182
2Biscoe Is.Gentoo49.473815.718221.54145.563814.2379212.707
3Dream Is.Adelie40.071418.8393191.92936.911117.6185187.852
4Dream Is.Chinstrap51.094119.2529199.91246.573517.5882191.735
5Torgersen Is.Adelie40.58719.3913194.91337.554217.55188.292

The table we want to create will feature island as the row group, and the species present on each island are listed in each group. As some of the length measurements we summarised describe the bill, we will group these as a column spanner. Finally, a higher-order column spanner will indicate which measurements are from female and which from male penguins.

Step 1: Row groups

julia
tbl = StyledTable(summary)
tab_rowgroup!(tbl, :island)
cols_hide!(tbl, :island)
render(tbl)
species male_bill_length_mm male_bill_depth_mm male_flipper_length_mm female_bill_length_mm female_bill_depth_mm female_flipper_length_mm
Biscoe Is.
Adelie 40.6 19 190 37.4 17.7 187
Gentoo 49.5 15.7 222 45.6 14.2 213
Dream Is.
Adelie 40.1 18.8 192 36.9 17.6 188
Chinstrap 51.1 19.3 200 46.6 17.6 192
Torgersen Is.
Adelie 40.6 19.4 195 37.6 17.6 188

Step 2: Level one spanner

julia
tab_spanner!(tbl, "Bill measures" => "male_" .* string.(bill_cols))
tab_spanner!(tbl, "Bill measures" => "female_" .* string.(bill_cols))
render(tbl)
Bill measures Bill measures
species male_bill_length_mm male_bill_depth_mm male_flipper_length_mm female_bill_length_mm female_bill_depth_mm female_flipper_length_mm
Biscoe Is.
Adelie 40.6 19 190 37.4 17.7 187
Gentoo 49.5 15.7 222 45.6 14.2 213
Dream Is.
Adelie 40.1 18.8 192 36.9 17.6 188
Chinstrap 51.1 19.3 200 46.6 17.6 192
Torgersen Is.
Adelie 40.6 19.4 195 37.6 17.6 188

Step 3: Level two spanner

julia
tab_spanner!(tbl, "Male" => male_ordered, level = 2)
tab_spanner!(tbl, "Female" => female_ordered, level = 2)
render(tbl)
Male Female
Bill measures Bill measures
species male_bill_length_mm male_bill_depth_mm male_flipper_length_mm female_bill_length_mm female_bill_depth_mm female_flipper_length_mm
Biscoe Is.
Adelie 40.6 19 190 37.4 17.7 187
Gentoo 49.5 15.7 222 45.6 14.2 213
Dream Is.
Adelie 40.1 18.8 192 36.9 17.6 188
Chinstrap 51.1 19.3 200 46.6 17.6 192
Torgersen Is.
Adelie 40.6 19.4 195 37.6 17.6 188

Step 4: Add column labels

julia
label_dict = Dict(
    :species => "Species",
    :male_bill_length_mm => "Length",
    :male_bill_depth_mm => "Depth",
    :male_flipper_length_mm => "Flipper length",
    :female_bill_length_mm => "Length",
    :female_bill_depth_mm => "Depth",
    :female_flipper_length_mm => "Flipper length",
)

cols_label!(tbl, label_dict)
fmt_integer!(tbl, [male_ordered..., female_ordered...])
render(tbl)
Male Female
Bill measures Bill measures
Species Length Depth Flipper length Length Depth Flipper length
Biscoe Is.
Adelie 41 19 190 37 18 187
Gentoo 49 16 222 46 14 213
Dream Is.
Adelie 40 19 192 37 18 188
Chinstrap 51 19 200 47 18 192
Torgersen Is.
Adelie 41 19 195 38 18 188