Customizing Legend Key Size and Appearance

Filled 2D geoms can use guideLegend() aesthetic overrides to adjust legend key appearance:

  • size changes the legend key border width.
  • size = 0 hides the legend key border.
  • width and height change the relative key size.
In [1]:
%useLatestDescriptors
%use dataframe
%use lets-plot
kotlin-logging: initializing... active logger factory: Slf4jLoggerFactory
In [2]:
LetsPlot.getInfo()
Out[2]:
Lets-Plot Kotlin API v.4.14.0. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.4.10.1.
Outputs: Web (HTML+JS), Kotlin Notebook (Swing), Static SVG (hidden)
In [3]:
val mpgDf = DataFrame.readCSV("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/mpg.csv")
val mpg = mpgDf.toMap()

mpgDf.head(3)
Out[3]:

DataFrame: rowsCount = 3, columnsCount = 12

untitledmanufacturermodeldisplyearcyltransdrvctyhwyflclass
1audia41.80000019994auto(l5)f1829pcompact
2audia41.80000019994manual(m5)f2129pcompact
3audia42.00000020084manual(m6)f2031pcompact

size - Border Stroke Thickness

Each legend key for a filled 2D geom is a filled rectangle drawn with a border. By default, the border uses the geom's color with a fixed width.

In [4]:
// Stacked bar: vehicle class counts broken down by drivetrain type.
val p = ggplot(mpg) { x = "class"; fill = "drv" } +
    geomBar(color = "black", size = 1.6) +
    labs(x = "vehicle class", fill = "drivetrain") +
    ggsize(580, 340)

p + ggtitle("Default legend style")
Out[4]:
compact midsize minivan subcompact suv pickup 2seater 0 10 20 30 40 50 60 Default legend style count vehicle class drivetrain f 4 r
In [5]:
// Use `size` in `guideLegend()` to control the legend border thickness. You can make it match the plot.
p + guides(fill = guideLegend(size = 1.6)) +
    ggtitle("Legend key stroke adjusted to match the plot")
Out[5]:
compact midsize minivan subcompact suv pickup 2seater 0 10 20 30 40 50 60 Legend key stroke adjusted to match the plot count vehicle class drivetrain f 4 r
In [6]:
// Or you can hide the border completely by setting `size = 0` to avoid visual clutter.
p + guides(fill = guideLegend(size = 0)) +
    ggtitle("Borderless legend keys")
Out[6]:
compact midsize minivan subcompact suv pickup 2seater 0 10 20 30 40 50 60 Borderless legend keys count vehicle class drivetrain f 4 r
In [7]:
// Prepare tile data.
val vehicleClass = mpg["class"]!!.map { it.toString() }
val drv = mpg["drv"]!!.map { it.toString() }
val hwy = mpg["hwy"]!!.map { (it as Number).toDouble() }

val mileageLabels = listOf("up to 20", "20-25", "25-30", "30-40", "40+")

fun mileageBand(value: Double): String = when {
    value <= 20.0 -> "up to 20"
    value <= 25.0 -> "20-25"
    value <= 30.0 -> "25-30"
    value <= 40.0 -> "30-40"
    else -> "40+"
}

data class TileRow(val vehicleClass: String, val drv: String, val meanHwy: Double, val mileage: String)

val tileRows = vehicleClass.indices
    .groupBy { vehicleClass[it] to drv[it] }
    .map { (key, indices) ->
        val meanHwy = indices.map { hwy[it] }.average()
        TileRow(key.first, key.second, meanHwy, mileageBand(meanHwy))
    }
    .sortedWith(compareBy<TileRow> { it.vehicleClass }.thenBy { it.drv })

val tileData = mapOf(
    "class" to tileRows.map { it.vehicleClass },
    "drv" to tileRows.map { it.drv },
    "mean_hwy" to tileRows.map { it.meanHwy },
    "mileage" to tileRows.map { it.mileage }
)

val baseTile = ggplot(tileData) { x = "class"; y = "drv"; fill = "mileage" } +
    geomTile(color = "gray30", size = 1.6, width = 0.92, height = 0.92) +
    scaleFillBrewer(palette = "Pastel1", breaks = mileageLabels) +
    labs(x = "vehicle class", y = "drive train", fill = "mean highway mpg") +
    ggsize(760, 420) +
    theme(axisTextX = elementText(angle = 35, hjust = 1.0))
In [8]:
// Some geoms, such as `geomTile()`, have borderless legend keys by default.
baseTile + ggtitle("Default legend keys")
Out[8]:
2seater compact midsize minivan pickup subcompact suv r 4 f Default legend keys drive train vehicle class mean highway mpg up to 20 20-25 25-30 30-40 40+
In [9]:
// Adding a contrast border makes the legend colors read closer to the plot.
baseTile +
    guides(fill = guideLegend(size = 0.8)) +
    ggtitle("Legend key border adds visual contrast")
Out[9]:
2seater compact midsize minivan pickup subcompact suv r 4 f Legend key border adds visual contrast drive train vehicle class mean highway mpg up to 20 20-25 25-30 30-40 40+

White border

In [10]:
// A white geom border can make legend keys look smaller on a white background.
fun <T> subsetRows(data: Map<String, List<T>>, indices: List<Int>): Map<String, List<T>> =
    data.mapValues { (_, values) -> indices.map { values[it] } }

val selectedClasses = setOf("subcompact", "compact", "midsize", "suv", "pickup")
val barData = subsetRows(mpg, vehicleClass.indices.filter { vehicleClass[it] in selectedClasses })

val barPlot = ggplot(barData) { x = "class"; fill = "drv" } +
    geomBar(size = 2.0) +
    scaleFillBrewer(palette = "Set2") +
    ggtitle("Bar legend keys with a white border") +
    ggsize(620, 360)

barPlot + guides(fill = guideLegend(size = 2.0))
Out[10]:
compact midsize subcompact suv pickup 0 10 20 30 40 50 60 Bar legend keys with a white border count class drv f 4 r
In [11]:
// In this case, decreasing `size` makes the visible filled area larger.
barPlot + guides(fill = guideLegend(size = 0.5))
Out[11]:
compact midsize subcompact suv pickup 0 10 20 30 40 50 60 Bar legend keys with a white border count class drv f 4 r

Relative Width and Height

A horizontal legend often reads better with wider, flatter keys. Use width and height in guideLegend() to change the relative key size.

In [12]:
val horizontalTile = baseTile + theme().legendPositionBottom()

horizontalTile + ggtitle("Horizontal legend with default key size")
Out[12]:
2seater compact midsize minivan pickup subcompact suv r 4 f Horizontal legend with default key size drive train vehicle class mean highway mpg up to 20 20-25 25-30 30-40 40+
In [13]:
horizontalTile +
    guides(fill = guideLegend(width = 2.5, height = 0.8)) +
    ggtitle("Horizontal legend with wider keys")
Out[13]:
2seater compact midsize minivan pickup subcompact suv r 4 f Horizontal legend with wider keys drive train vehicle class mean highway mpg up to 20 20-25 25-30 30-40 40+

Different Heights per Category

A list of height values can make the legend carry an extra ordered cue. Here, the key height follows engine size: 4, 6, and 8 cylinders.

In [14]:
val cylinderValues = listOf(4, 6, 8)
val cylinderLabels = cylinderValues.map { "$it cylinders" }
val cyl = mpg["cyl"]!!.map { (it as Number).toInt() }
val seenCylinderRows = mutableSetOf<Pair<String, String>>()

val cylinderIndices = vehicleClass.indices.filter { i ->
    cyl[i] in cylinderValues && seenCylinderRows.add(vehicleClass[i] to "${cyl[i]} cylinders")
}
val cylinderData = mapOf(
    "class" to cylinderIndices.map { vehicleClass[it] },
    "cylinders" to cylinderIndices.map { "${cyl[it]} cylinders" }
)

ggplot(cylinderData) { x = "class"; y = "cylinders"; fill = "cylinders" } +
    geomTile(color = "white", size = 0.5, width = 0.9, height = 0.9) +
    scaleFillManual(
        values = listOf("chocolate", "sea_green", "royal_blue"),
        breaks = cylinderLabels
    ) +
    guides(fill = guideLegend(height = listOf(0.8, 1.2, 1.6))) +
    labs(x = "vehicle class", y = "engine size", fill = "engine size") +
    ggtitle("Legend key height reflects cylinder count") +
    ggsize(760, 360) +
    theme(axisTextX = elementText(angle = 35, hjust = 1.0), legendKeySpacing = 4)
Out[14]:
compact midsize suv 2seater minivan pickup subcompact 4 cylinders 6 cylinders 8 cylinders Legend key height reflects cylinder count engine size vehicle class engine size 4 cylinders 6 cylinders 8 cylinders