Legend Wrap¶

When the legend contains many items, it automatically wraps to prevent overlap - up to 15 rows for vertical legends and 5 columns for horizontal ones.

In [1]:
%useLatestDescriptors
%use lets-plot
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), Kotlin Notebook (Swing), Static SVG (hidden)
In [3]:
// Prepare a dataset

import kotlin.math.roundToInt
import kotlin.random.Random

private fun Double.to255(): Int = (coerceIn(0.0, 1.0) * 255.0).roundToInt()

fun hlsToRgb(h: Double, l: Double, s: Double): Triple<Int, Int, Int> {
    fun hue2rgb(p: Double, q: Double, t0: Double): Double {
        var t = t0
        if (t < 0) t += 1.0
        if (t > 1) t -= 1.0
        return when {
            t < 1.0 / 6 -> p + (q - p) * 6 * t
            t < 1.0 / 2 -> q
            t < 2.0 / 3 -> p + (q - p) * (2.0 / 3 - t) * 6
            else -> p
        }
    }
    val q = if (l < 0.5) l * (1 + s) else l + s - l * s
    val p = 2 * l - q
    val r = hue2rgb(p, q, h + 1.0 / 3)
    val g = hue2rgb(p, q, h)
    val b = hue2rgb(p, q, h - 1.0 / 3)
    return Triple(r.to255(), g.to255(), b.to255())
}


fun distinctPalette(n: Int, s: Double = 0.72, l: Double = 0.53): List<String> =
    List(n) { i ->
        val (r, g, b) = hlsToRgb(i.toDouble() / n, l, s)
        "#%02X%02X%02X".format(r, g, b)
    }
    
val nCats = 36
val ptsPerCat = 13
val cats = (1..nCats).map { i -> "C%02d".format(i) }
val cols = 6

val rnd = Random(42)
data class Pt(val x: Double, val y: Double, val cat: String)

val points: List<Pt> = cats.flatMapIndexed { i, c ->
    List(ptsPerCat) {
        val x = (i % cols) + rnd.nextDouble(-0.4, 0.4)
        val y = (i / cols) + rnd.nextDouble(-0.4, 0.4)
        Pt(x, y, c)
    }
}

val xs = points.map { it.x }
val ys = points.map { it.y }
val catCol = points.map { it.cat }

val data = mapOf("x" to xs, "y" to ys, "cat" to catCol)

val palette = distinctPalette(nCats)
In [4]:
val base =
    letsPlot(data) { x = "x"; y = "y"; color = "cat" } +
        geomPoint(size = 3.0, alpha = 0.9) +
        scaleColorManual(values = palette, name = "Categories ($nCats)") +
        ggsize(800, 460)
In [5]:
// The default legend layout (vertical)

base
Out[5]:
0 1 2 3 4 5 0 1 2 3 4 5 y x Categories (36) C01 C02 C03 C04 C05 C06 C07 C08 C09 C10 C11 C12 C13 C14 C15 C16 C17 C18 C19 C20 C21 C22 C23 C24 C25 C26 C27 C28 C29 C30 C31 C32 C33 C34 C35 C36
In [6]:
// The horizontal case

base + theme().legendPositionBottom()
Out[6]:
-0.5 0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 0 1 2 3 4 5 y x Categories (36) C01 C02 C03 C04 C05 C06 C07 C08 C09 C10 C11 C12 C13 C14 C15 C16 C17 C18 C19 C20 C21 C22 C23 C24 C25 C26 C27 C28 C29 C30 C31 C32 C33 C34 C35 C36
In [7]:
// You can still adjust the number of legend rows or columns manually.

base +
    theme().legendPositionBottom() +
    guides(
        color = guideLegend(
            ncol = 10, 
            byRow = true)
    )
Out[7]:
-0.5 0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 0 1 2 3 4 5 y x Categories (36) C01 C02 C03 C04 C05 C06 C07 C08 C09 C10 C11 C12 C13 C14 C15 C16 C17 C18 C19 C20 C21 C22 C23 C24 C25 C26 C27 C28 C29 C30 C31 C32 C33 C34 C35 C36