Dual-Axis Effect with ggdeck()

The ggdeck() function allows for the overlaying of multiple plots into a single view.

By combining a primary plot with one or more blank plots—each configured with its own scale—you can achieve a dual-axis (or multi-axis) effect.

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]:
var data = DataFrame.readCSV("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/delhi_climate.csv")
data.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]:
val basePlot = letsPlot(data.toMap()) +
    geomLine {
        x = "date"
        y = "meantemp"
        color = "meantemp"
    } +
    scaleColorGradient2(
        low = "#0571b0",
        mid = "#f7f7f7",
        high = "#ca0020",
        midpoint = 30
    ) +
    labs(y = "°C", color = "°C")

basePlot
Out[4]:
2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 °C date °C 10 15 20 25 30 35

1. Secondary Axis - Fahrenheits

In [5]:
// Add the Fahrenheit column via a linear transformation
data = data.add("meantemp_f") {
    (meantemp * 9 / 5) + 32
}
data.head(3)
Out[5]:

DataFrame: rowsCount = 3, columnsCount = 6

datemeantemphumiditywind_speedmeanpressuremeantemp_f
2013-01-0110.00000084.5000000.0000001015.66666750.000000
2013-01-027.40000092.0000002.9800001017.80000045.320000
2013-01-037.16666787.0000004.6333331018.66666744.900000
In [6]:
// Helper to synchronize the Y-axis aesthetic (line, ticks, and text)
fun axisYColor(color: String): theme {
    return theme(
        axisLineY = elementLine(color = color),
        axisTicksY = elementLine(color = color),
        axisTextY = elementText(color = color),
        axisTitleY = elementText(color = color)
    )
}
In [7]:
// Create an empty plot with the y-axis on the right

val fahrenheitPlot = letsPlot(data.toMap()) + 
    geomBlank {
        // Scale mapping without rendering data
        x = "date"
        y = "meantemp_f"
    } + 
    labs(y = "°F") + 
    axisYColor("blue") + 
    scaleYContinuous(position = "right")

fahrenheitPlot + ggsize(400, 200)
Out[7]:
2013 2014 2015 2016 2017 40 60 80 100 °F date
In [8]:
// Combine the primary and secondary plots into a single layout

ggdeck(listOf(basePlot, fahrenheitPlot)) +
    ggsize(800, 400) +
    ggtitle("Delhi Mean Temperature") +
    themeClassic() +
    theme(
        plotTitle = elementText(hjust = 0.5, size = 21)
    )
Out[8]:
Delhi Mean Temperature °C 10 15 20 25 30 35 2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 °C date 2013 2014 2015 2016 2017 40 50 60 70 80 90 100 °F date

2. Show More Temperature Scales - Kelvin and Rankine

This section demonstrates an alternative approach by anchoring secondary scales using boundary coordinates to ensure precise axis synchronization with minimal data overhead.

In [9]:
val xMin = data.min("date")
val xMax = data.max("date")

val cMin = data.min("meantemp") as Double
val cMax = data.max("meantemp") as Double

// Perform linear transformations for Kelvin and Rankine boundaries
val fMin = (cMin * 9 / 5) + 32
val fMax = (cMax * 9 / 5) + 32

val kMin = cMin + 273.15
val kMax = cMax + 273.15

val rMin = fMin + 459.67
val rMax = fMax + 459.67
In [10]:
// Option 1: create a lightweight dictionary for scale mapping.

val boundsData = mapOf(
    "x" to listOf(xMin, xMax),
    "y_kelvin" to listOf(kMin, kMax)
)

val kelvinPlot = letsPlot(boundsData) +
    geomBlank {
        x = "x"
        y = "y_kelvin"
    } +
    labs(y = "K") +
    axisYColor("green") +
    scaleYContinuous(position = "right")

kelvinPlot + ggsize(400, 200)
Out[10]:
2013 2014 2015 2016 2017 280 290 300 310 K x
In [11]:
// Option 2: set the scale limits.

val rankinePlot = letsPlot() +
    geomBlank() +
    labs(y = "°R") +
    axisYColor("red") +
    scaleYContinuous(position = "right", limits = Pair(rMin, rMax)) +
    scaleXDateTime(limits = Pair(xMin, xMax))

rankinePlot + ggsize(400, 200)
Out[11]:
2013 2014 2015 2016 2017 500 520 540 560 °R x
In [12]:
// Assemble the multi-axis deck.
// Use 'panelInset' to create horizontal spacing between adjacent Y-axes.

ggdeck(
    listOf(
        basePlot,
        fahrenheitPlot,
        kelvinPlot + 
            theme(panelInset = listOf(0.0, 3.0, 0.0, 0.0)),  // <-- 3px offset for K axis
        rankinePlot + 
            theme(panelInset = listOf(0.0, 3.0, 0.0, 0.0))   // <-- 3px offset for °R axis
    )
) +
    ggsize(800, 400) +
    ggtitle("Delhi Mean Temperature") +
    themeClassic() +
    theme(
        plotTitle = elementText(hjust = 0.5, size = 21),
        panelGrid = elementLine()     // <-- Add gridlines to the Classic theme
    ) + 
    ggtb()
Out[12]:
Delhi Mean Temperature °C 10 15 20 25 30 35 2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 °C date 2013 2014 2015 2016 2017 40 50 60 70 80 90 100 °F date 2013 2014 2015 2016 2017 280 285 290 295 300 305 310 K x 2013 2014 2015 2016 2017 500 510 520 530 540 550 560 °R x

3. Layout Optimization — Plot Subtitles Instead of Y-Axis Titles

In [13]:
ggdeck(
    listOf(
        basePlot,
        
        fahrenheitPlot + 
            labs(subtitle = "°F") +   // <-- Subtitle labeling the Y-axis
            theme(
                axisTitleY = "blank",
                plotSubtitle = elementText(hjust = 1.0, color = "blue")  // <-- Subtitle right-justification and color
            ),
            
        kelvinPlot + 
            labs(subtitle = "K") + 
            theme(
                axisTitleY = "blank",
                plotSubtitle = elementText(hjust = 1.0, color = "green"),
                panelInset = listOf(0.0, 3.0, 0.0, 0.0)
            ),
            
        rankinePlot + 
            labs(subtitle = "°R") + 
            theme(
                axisTitleY = "blank",
                plotSubtitle = elementText(hjust = 1.0, color = "red"),
                panelInset = listOf(0.0, 3.0, 0.0, 0.0)
            )
    )
) +
    ggsize(800, 400) +
    ggtitle("Delhi Mean Temperature") +
    themeClassic() +
    theme(
        plotTitle = elementText(hjust = 0.5, size = 21),
        panelGrid = elementLine()
    ) + 
    ggtb()
Out[13]:
Delhi Mean Temperature °C 10 15 20 25 30 35 2013 2014 2015 2016 2017 5 10 15 20 25 30 35 40 °C date 2013 2014 2015 2016 2017 40 50 60 70 80 90 100 °F date 2013 2014 2015 2016 2017 280 285 290 295 300 305 310 K x 2013 2014 2015 2016 2017 500 510 520 530 540 550 560 °R x