Customizing Legend Key Size and Appearance¶
Filled 2D geoms can use guideLegend() aesthetic overrides to adjust legend key appearance:
sizechanges the legend key border width.size = 0hides the legend key border.widthandheightchange the relative key size.
In [1]:
%useLatestDescriptors
%use dataframe
%use lets-plot
In [2]:
LetsPlot.getInfo()
Out[2]:
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]:
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]:
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]:
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]:
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]:
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]:
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]:
In [11]:
// In this case, decreasing `size` makes the visible filled area larger.
barPlot + guides(fill = guideLegend(size = 0.5))
Out[11]:
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]:
In [13]:
horizontalTile +
guides(fill = guideLegend(width = 2.5, height = 0.8)) +
ggtitle("Horizontal legend with wider keys")
Out[13]:
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]: