Plot Overlay with ggdeck()

The ggdeck() function enables multivariate comparison by synchronized overlaying of independent plots. This allows you to combine separate plot objects—each with its own coordinate system and geometries—into a unified view.

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)

1. Delhi Mean Temperature Chart

In [3]:
var climateData = DataFrame.readCSV("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/delhi_climate.csv")
climateData.head(3)
Out[3]:

DataFrame: rowsCount = 3, columnsCount = 5

datemeantemphumiditywind_speedmeanpressure
2013-01-0110.00000084.5000000.0000001015.666667
2013-01-027.40000092.0000002.9800001017.800000
2013-01-037.16666787.0000004.6333331018.666667
In [4]:
climateData.schema().print()
date: kotlinx.datetime.LocalDate
meantemp: Double
humidity: Double
wind_speed: Double
meanpressure: Double
In [5]:
val temperaturePlot = letsPlot(climateData.toMap()) +
    geomLine() {
        x = "date"
        y = "meantemp"
        color = "meantemp"
    } +
    scaleColorGradient2(
        low = "#0571b0",
        mid = "#f7f7f7",
        high = "#ca0020",
        midpoint = 30
    ) +
    labs(y = "Mean Temperature [°C]") +
    theme(
        axisTextY = elementText(color = "#0571b0"),
        axisTitleY = elementText(color = "#0571b0")
    )

temperaturePlot
Out[5]:
2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 Mean Temperature [°C] date meantemp 10 15 20 25 30 35

2. Fictional 'Daily Ice Cream Cost' Data

In [6]:
fun calculateDailyCost(t: Double): Double {
    val baseCost = 1.5
    var surge = 0.0
    
    if (t > 30.0) {
        // Sharp surge above 30 degrees
        surge = 0.15 * (t - 30.0).pow(2)
    }
    
    return baseCost + surge
}
In [7]:
// Synthesize daily cost data and aggregate by month

// 1. Map the daily cost to a new column
val costDataTemp = climateData.add("daily_cost") {
    calculateDailyCost(it["meantemp"] as Double)
}

// 2. Group by Month-Start date and then aggregate
val costData = costDataTemp
    .add("month_start") {
        val date = it["date"] as kotlinx.datetime.LocalDate
        kotlinx.datetime.LocalDate(date.year, date.month, 1)
    }
    .groupBy("month_start")
    .aggregate {
        val dailyCosts = "daily_cost"<Double>()
        
        dailyCosts.mean() into "avg_daily_cost"
        dailyCosts.min() into "min_daily_cost"
        dailyCosts.max() into "max_daily_cost"
    }

costData.head(3)
Out[7]:

DataFrame: rowsCount = 3, columnsCount = 4

month_startavg_daily_costmin_daily_costmax_daily_cost
2013-01-011.5000001.5000001.500000
2013-02-011.5000001.5000001.500000
2013-03-011.5000001.5000001.500000

3. 'Daily Cost' Plot with Y-Axis on the Right

In [8]:
val costPlot = letsPlot(costData.toMap()) +
    geomBar(stat = Stat.identity, fill = "#F39C12", alpha = 0.4) {
        x = "month_start"
        y = "avg_daily_cost"
    } +
    geomPointRange(color = "#A04000", fatten = 3.0, size = 0.8) {
        x = "month_start"
        y = "avg_daily_cost"
        ymin = "min_daily_cost"
        ymax = "max_daily_cost"
    } +
    labs(y = "Daily Ice Cream Cost per Kid [\$]") +
    theme(
        axisTextY = elementText(color = "#A04000"),
        axisTitleY = elementText(color = "#A04000")
    ) +
    scaleYContinuous(position = "right")

costPlot
Out[8]:
2013 2014 2015 2016 2017 0 2 4 6 8 10 12 Daily Ice Cream Cost per Kid [$] month_start

4. Composing the 'Temperature' and 'Daily Cost' Plots into a Deck

In [9]:
ggdeck(listOf(
        temperaturePlot + theme().legendPositionNone(),
        costPlot
    )) +
    theme(
        plotTitle = elementText(hjust = 0.5, size = 21)
    ) +
    ggtitle("Delhi Mean Temperature & Ice Cream Cost") +
    ggsize(800, 400)
Out[9]:
Delhi Mean Temperature & Ice Cream Cost 2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 Mean Temperature [°C] date 2013 2014 2015 2016 2017 0 2 4 6 8 10 12 Daily Ice Cream Cost per Kid [$] month_start

5. Managing Tooltips in Composites

Since ggdeck() overlays independent plots, multiple tooltips may trigger simultaneously. This can lead to visual clutter and overlapping labels, especially on the axes.

To maintain clarity:

  • Silence background layers: Disable tooltips on secondary geometries to reduce hover noise.
  • Remove redundant axis labels: Suppress axis tooltips on overlaid plots to avoid duplication.
  • Use anchored tooltips: Consolidate information into a fixed position.
In [10]:
// Configure tooltips on the 'Daily Cost' plot

val costPlot2 = letsPlot(costData.toMap()) +
    geomBar(stat = Stat.identity, fill = "#F39C12", alpha = 0.4, 
            tooltips = tooltipsNone  // <-- Disable all tooltips on bars
           ) {
        x = "month_start"
        y = "avg_daily_cost"
    } + 
    geomPointRange(color = "#A04000", fatten = 3.0, size = 0.8,
        tooltips = layerTooltips() // <-- Consolidate tooltips in a single panel
            .title("@month_start")
            .line("Avg | ^y")
            .line("Min | ^ymin")
            .line("Max | ^ymax")
            .format("^Y", "\${.2f}")
            .format("@month_start", "%b")
            .anchor("top_right") // <-- Anchor to the top-right corner
    ) {
        x = "month_start"
        y = "avg_daily_cost"
        ymin = "min_daily_cost"
        ymax = "max_daily_cost"
    } +
    theme(axisTooltipX = "blank") + // <-- Suppress redundant X-axis tooltips
    labs(y = "Daily Ice Cream Cost per Kid [\$]") +
    theme(
        axisTextY = elementText(color = "#A04000"),
        axisTitleY = elementText(color = "#A04000")
    ) +
    scaleYContinuous(position = "right")

costPlot2 + ggsize(800, 400)
Out[10]:
2013 2014 2015 2016 2017 0 2 4 6 8 10 12 Daily Ice Cream Cost per Kid [$] month_start

6. Assembling the Updated 'Daily Cost' Plot into a Final Deck

In [11]:
ggdeck(listOf(
        temperaturePlot + theme().legendPositionNone(),
        costPlot2
    )) +
    theme(
        plotTitle = elementText(hjust = 0.5, size = 21)
    ) +
    ggtitle("Delhi Mean Temperature & Ice Cream Cost") +
    ggsize(800, 400)
Out[11]:
Delhi Mean Temperature & Ice Cream Cost 2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 Mean Temperature [°C] date 2013 2014 2015 2016 2017 0 2 4 6 8 10 12 Daily Ice Cream Cost per Kid [$] month_start