Collecting Guides in gggrid()¶

The guides parameter controls how legends and colorbars are handled when arranging plots in a grid.

guides="auto" (default)
Keep guides in subplots by default. However, if this grid is nested inside another grid that uses guides="collect", pass the guides up for collection at that higher level.

guides="collect"
Collect all guides (legends and colorbars) from subplots and place them alongside the grid figure, automatically removing duplicates.

guides="keep"
Keep guides in their original subplots. No collection occurs at this level, even if an outer grid requests collection.

Duplicate Detection¶

Guides are compared by their visual appearance:

For legends:
Two legends are considered duplicates if they have identical

  • title
  • labels
  • all aesthetic values (colors, shapes, sizes, line types, etc.)

For colorbars:
Two colorbars are considered duplicates if they have identical

  • title
  • domain limits
  • breaks (tick positions)
  • color gradient

Note: Colorbars from different data ranges typically have different limits and will not merge without manual harmonization.

In [1]:
%useLatestDescriptors
%use dataframe
%use lets-plot(output="js, png")
In [2]:
LetsPlot.getInfo()
Out[2]:
Lets-Plot Kotlin API v.4.12.0. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.4.8.1.
Outputs: Web (HTML+JS), Static PNG (hidden)
In [3]:
// Helper functions

fun <T> DataFrame<T>.sample(n: Int, randomState: Long? = null): DataFrame<T> {
    val rng = randomState?.let { java.util.Random(it) } ?: java.util.Random()
    return this.rows().shuffled(rng).take(n).toDataFrame()
}

fun <T> DataFrame<T>.shape(): Pair<Int, Int> =
    Pair(this.rowsCount(), this.columnsCount())
In [4]:
val df = DataFrame.readCSV("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/diamonds.csv")
println("Source: ${df.shape()}")

val sampleDf = df.sample(1_000, randomState = 42)
val diamonds = sampleDf.toMap()

println("Sampled: ${sampleDf.shape()}")
sampleDf.head()
Source: (53940, 10)
Sampled: (1000, 10)
Out[4]:

DataFrame: rowsCount = 5, columnsCount = 10

caratcutcolorclaritydepthtablepricexyz
1.410000GoodGSI163.70000057.00000077387.1300007.0300004.510000
1.210000PremiumFSI261.80000059.00000044726.8200006.7700004.200000
0.700000Very GoodESI260.10000056.00000022905.7600005.7900003.470000
0.710000IdealIVS262.10000059.00000023155.7300005.7000003.550000
0.350000Very GoodHVVS263.00000055.0000007064.4800004.5000002.830000

1. Collecting Legends¶

In [5]:
// Create three plots that share the same color aesthetic ('clarity')

val clarityP =
    letsPlot(diamonds) { y = "price"; color = "clarity" } +
        scaleColorHue() +
        themeClassic()

val clarityP0 = clarityP + geomPoint { x = "carat" }
val clarityP1 = clarityP + geomPoint { x = "depth" }
val clarityP2 = clarityP + geomPoint { x = "color" }

// By default, each plot keeps its own legend.

gggrid( listOf(
    clarityP0, 
    clarityP1, 
    clarityP2
))
Out[5]:
image
In [6]:
// Now collect the legends into a single shared legend.

// Note: 
// All 3 legends are visually identical (same title, labels, and aesthetic values), 
// so duplicates are removed and only one legend is shown.

gggrid(
    listOf(
        clarityP0,
        clarityP1 + theme(axisTitleY = "blank"),
        clarityP2 + theme(axisTitleY = "blank")
    ),
    
    guides = "collect"              // <-- collect legends from subplots

) + theme().legendPositionBottom()  // <-- also adjust the legend position 
Out[6]:
image

2. Collecting Colorbars¶

In [7]:
fun <T> DataFrame<T>.filterByColumnValue(columnName: String, value: Any?): DataFrame<T> =
    this.filter { it[columnName] == value }

val idealDiamonds = sampleDf.filterByColumnValue("cut", "Ideal").toMap()
val fairDiamonds = sampleDf.filterByColumnValue("cut", "Fair").toMap()

val priceP =
    letsPlot { x = "carat"; y = "depth"; color = "price" } +
        scaleColorViridis() +
        themeGrey() +
        theme(plotTitle = elementText(hjust = 0.5))

val priceP0 = priceP + geomPoint(data = idealDiamonds, size = 6.0) + ggtitle("Ideal Cut")
val priceP1 = priceP + geomPoint(data = fairDiamonds,  size = 6.0) + ggtitle("Fair Cut")

// Arrange two plots in a grid with guides="collect".

// Note: 
// The colorbars have different domain limits and breaks (due to different data ranges),
// so both are retained as separate colorbars.

gggrid(
    listOf(priceP0, priceP1),
    sharex = true, sharey = true,
    guides = "collect"
)
Out[7]:
image
In [8]:
// Apply the same color scale limits to both subplots.
// Now both colorbars are visually identical, so the duplicate is removed.

fun List<Any?>.intMin() = this.minOf { it.toString().toInt() }
fun List<Any?>.intMax() = this.maxOf { it.toString().toInt() }

val priceLists = listOf(
    idealDiamonds["price"]!!,
    fairDiamonds["price"]!!
)

val priceMin = priceLists.minOf { it.intMin() }
val priceMax = priceLists.maxOf { it.intMax() }

val priceLims = scaleColorViridis(limits = listOf(priceMin, priceMax))

gggrid(
    listOf(
        priceP0 + priceLims, 
        priceP1 + priceLims
    ),
    sharex = true, sharey = true,
    guides = "collect"
)
Out[8]:
image

3. Collecting Guides in Nested Grids¶

In [9]:
// Create nested grids and collect all guides at the top level.

val clarityGrid = gggrid(listOf(
    clarityP0, 
    clarityP1, 
    clarityP2
))

val priceGrid = gggrid(listOf(
    priceP0 + priceLims, 
    priceP1 + priceLims
))

// Top-level collects from both nested grids
gggrid(
    listOf(clarityGrid, priceGrid),
    ncol = 1,
    guides = "collect"  //  <-- collects from all nested grids
    
) + theme().legendPositionBottom() + ggsize(800, 600)
Out[9]:
image
In [10]:
// A nested grid can override this behavior by collecting guides at its own level,
// keeping them separate from guides collected by the upper level.

val clarityGridLocalCollect = gggrid(listOf(
        clarityP0, 
        clarityP1, 
        clarityP2
    ),
                                     
    guides = "collect"      // <-- collect 'clarity' legends at this level
)

gggrid(listOf(
    clarityGridLocalCollect, 
    priceGrid
),
    ncol = 1,
    guides = "collect"
       
) + theme().legendPositionBottom() + ggsize(800, 600)
Out[10]:
image