Powered by AppSignal & Oban Pro

Subplots

notebooks/10_subplots.livemd

Subplots

Mix.install([
  {:plotly_ex, "~> 0.1"},
  {:kino, "~> 0.18"}
])

Subplots > Simple Subplot

layout.grid creates a subplot grid. Set rows: and columns: with pattern: "independent" to give each cell its own axes. Traces reference their cell via xaxis: "x2" etc. The first cell always uses xaxis/yaxis (no number suffix).

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [4, 5, 6], name: "Trace 1"))
|> Figure.add_trace(Scatter.new(x: [2, 3, 4], y: [5, 6, 7], xaxis: "x2", yaxis: "y2", name: "Trace 2"))
|> Figure.update_layout(
  title: "Simple Subplot",
  grid: %{rows: 1, columns: 2, pattern: "independent"}
)
|> Plotly.show()

Subplots > Custom Sized Subplot

Set xaxis.domain / xaxis2.domain as [start, end] fractions of the total plot width (0.0–1.0) to control column proportions. A small gap between domains prevents the axes from touching.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [4, 5, 6], name: "Wide"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [2, 3, 4], xaxis: "x2", yaxis: "y2", name: "Narrow"))
|> Figure.update_layout(
  title: "Custom Sized Subplots",
  xaxis: %{domain: [0, 0.65]},
  xaxis2: %{domain: [0.70, 1.0]},
  yaxis2: %{anchor: "x2"}
)
|> Plotly.show()

Subplots > Multiple Subplots

A 2×2 grid has 4 axis pairs. The mapping is row-major: cell (1,1)=x/y, (1,2)=x2/y2, (2,1)=x3/y3, (2,2)=x4/y4.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2], y: [1, 2], name: "R1C1"))
|> Figure.add_trace(Scatter.new(x: [1, 2], y: [2, 1], xaxis: "x2", yaxis: "y2", name: "R1C2"))
|> Figure.add_trace(Scatter.new(x: [1, 2], y: [3, 1], xaxis: "x3", yaxis: "y3", name: "R2C1"))
|> Figure.add_trace(Scatter.new(x: [1, 2], y: [1, 3], xaxis: "x4", yaxis: "y4", name: "R2C2"))
|> Figure.update_layout(
  title: "Multiple Subplots",
  grid: %{rows: 2, columns: 2, pattern: "independent"}
)
|> Plotly.show()

Subplots > Subplots with Shared Axes

pattern: "coupled" in layout.grid shares axes between cells in the same row/column. Alternatively, set xaxis2.matches: "x" explicitly to link specific axis pairs.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [4, 5, 6], name: "Top"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [2, 3, 4], xaxis: "x2", yaxis: "y2", name: "Bottom"))
|> Figure.update_layout(
  title: "Shared Axes",
  grid: %{rows: 2, columns: 1, pattern: "independent"},
  xaxis2: %{matches: "x"}
)
|> Plotly.show()

Subplots > Stacked Subplots

Three rows, one column. Each trace gets its own y-axis but they share the x-axis column visually.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [4, 5, 6], name: "Top"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [2, 3, 4], xaxis: "x2", yaxis: "y2", name: "Middle"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [1, 4, 2], xaxis: "x3", yaxis: "y3", name: "Bottom"))
|> Figure.update_layout(
  title: "Stacked Subplots",
  grid: %{rows: 3, columns: 1, pattern: "independent"}
)
|> Plotly.show()

Subplots > Stacked Subplots with a Shared X-Axis

xaxis2.matches: "x" and xaxis3.matches: "x" link all x-axes so zooming one zooms all. showticklabels: false hides redundant x tick labels on upper plots.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [0, 1, 2, 3], y: [10, 11, 12, 13], name: "Top"))
|> Figure.add_trace(Scatter.new(x: [0, 1, 2, 3], y: [20, 18, 17, 16], xaxis: "x2", yaxis: "y2", name: "Middle"))
|> Figure.add_trace(Scatter.new(x: [0, 1, 2, 3], y: [30, 29, 31, 28], xaxis: "x3", yaxis: "y3", name: "Bottom"))
|> Figure.update_layout(
  title: "Stacked Subplots — Shared X-Axis",
  grid: %{rows: 3, columns: 1, pattern: "independent"},
  xaxis: %{showticklabels: false},
  xaxis2: %{matches: "x", showticklabels: false},
  xaxis3: %{matches: "x"}
)
|> Plotly.show()

Subplots > Multiple Custom Sized Subplots

Manual xaxis.domain/yaxis.domain gives full control over each subplot’s position. Each domain is [x_start, x_end] / [y_start, y_end] as fractions of the figure (0.0–1.0). Leave a gap between domains for spacing (e.g. 0.0–0.45, 0.55–1.0).

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [1, 2, 3], name: "Large"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [3, 1, 2], xaxis: "x2", yaxis: "y2", name: "Small Top"))
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [2, 3, 1], xaxis: "x3", yaxis: "y3", name: "Small Bottom"))
|> Figure.update_layout(
  title: "Multiple Custom Sized Subplots",
  xaxis:  %{domain: [0.0, 0.55]},
  yaxis:  %{domain: [0.0, 1.0]},
  xaxis2: %{domain: [0.65, 1.0]},
  yaxis2: %{domain: [0.55, 1.0]},
  xaxis3: %{domain: [0.65, 1.0]},
  yaxis3: %{domain: [0.0, 0.45]}
)
|> Plotly.show()

Subplots > Simple Inset Graph

An inset is a small subplot overlaid on top of the main plot. Achieved by giving xaxis2 and yaxis2 fractional domains in the upper-right corner of the figure space (e.g. 0.6–1.0). Both axes occupy the same figure area — the inset “floats” over the main chart.

alias Plotly.{Figure, Scatter}

x = Enum.map(0..49, & &1 / 10)
y = Enum.map(x, &:math.sin/1)

# Zoomed region for inset
x_inset = Enum.map(0..9, & &1 / 10)
y_inset = Enum.map(x_inset, &:math.sin/1)

Figure.new()
|> Figure.add_trace(Scatter.new(x: x, y: y, name: "sin(x)"))
|> Figure.add_trace(
  Scatter.new(x: x_inset, y: y_inset, xaxis: "x2", yaxis: "y2", name: "Zoom")
)
|> Figure.update_layout(
  title: "Simple Inset Graph",
  xaxis2: %{domain: [0.6, 1.0], anchor: "y2"},
  yaxis2: %{domain: [0.6, 1.0], anchor: "x2"}
)
|> Plotly.show()

Subplots > Multiple 3D Subplots

3D traces (Surface, Scatter3d, Mesh3d) use scene: instead of xaxis:/yaxis:. The first scene is "scene", the second is "scene2", etc. layout.scene and layout.scene2 each accept a domain: %{column: 0|1} to assign to grid columns.

alias Plotly.{Figure, Surface}

# Simple z-surfaces
n = 20
xs = Enum.map(0..(n - 1), &(&1 / (n - 1) * 4 - 2))
z1 = for x <- xs, do: (for y <- xs, do: :math.sin(:math.sqrt(x * x + y * y)))
z2 = for x <- xs, do: (for y <- xs, do: :math.cos(:math.sqrt(x * x + y * y)))

Figure.new()
|> Figure.add_trace(Surface.new(z: z1, colorscale: "Viridis", showscale: false))
|> Figure.add_trace(Surface.new(z: z2, colorscale: "RdBu", showscale: false, scene: "scene2"))
|> Figure.update_layout(
  title: "Multiple 3D Subplots",
  grid: %{rows: 1, columns: 2},
  scene:  %{domain: %{column: 0}},
  scene2: %{domain: %{column: 1}}
)
|> Plotly.show()

Subplots > Mixed Subplots

Different trace types can share a grid. Scatter and Bar use the xaxis/yaxis system. Pie uses domain: %{row:, column:} to position itself in the grid (0-indexed).

alias Plotly.{Figure, Scatter, Bar, Pie}

Figure.new()
|> Figure.add_trace(Scatter.new(x: [1, 2, 3], y: [4, 3, 2], name: "Scatter"))
|> Figure.add_trace(Bar.new(x: ["A", "B", "C"], y: [3, 1, 4], xaxis: "x2", yaxis: "y2", name: "Bar"))
|> Figure.add_trace(
  Pie.new(
    labels: ["X", "Y", "Z"],
    values: [40, 35, 25],
    domain: %{row: 1, column: 0},
    name: "Pie"
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: [1, 2, 3],
    y: [10, 11, 12],
    mode: "markers",
    xaxis: "x4",
    yaxis: "y4",
    name: "Scatter 2"
  )
)
|> Figure.update_layout(
  title: "Mixed Subplots",
  grid: %{rows: 2, columns: 2, pattern: "independent"}
)
|> Plotly.show()

Subplots > Table and Chart Subplot

Table is positioned using domain: %{row:, column:} (0-indexed row/column in the grid). Chart traces use xaxis2/yaxis2 referencing the second grid cell.

alias Plotly.{Figure, Table, Scatter}

headers = ["Month", "Sales", "Returns"]
months = ["Jan", "Feb", "Mar", "Apr", "May"]
sales = [120, 150, 130, 170, 160]
returns = [10, 12, 8, 15, 11]

Figure.new()
|> Figure.add_trace(
  Table.new(
    header: %{values: headers},
    cells: %{values: [months, sales, returns]},
    domain: %{row: 0, column: 0}
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: months,
    y: sales,
    mode: "lines+markers",
    name: "Sales",
    xaxis: "x2",
    yaxis: "y2"
  )
)
|> Figure.update_layout(
  title: "Table and Chart Subplot",
  grid: %{rows: 2, columns: 1},
  yaxis2: %{domain: [0.0, 0.45]},
  xaxis2: %{domain: [0.0, 1.0], anchor: "y2"}
)
|> Plotly.show()

Subplots > Two Y-Axes

Two y-axes on one chart: the second y-axis uses overlaying: "y" to share the same x-axis and plot area. side: "right" puts it on the right. The trace referencing it sets yaxis: "y2".

alias Plotly.{Figure, Scatter}

x = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
y1 = [1000, 1200, 900, 1400, 1100, 1300]
y2 = [22, 19, 25, 18, 23, 20]

Figure.new()
|> Figure.add_trace(
  Scatter.new(x: x, y: y1, name: "Revenue ($)", mode: "lines+markers")
)
|> Figure.add_trace(
  Scatter.new(x: x, y: y2, name: "Temperature (°C)", mode: "lines+markers", yaxis: "y2")
)
|> Figure.update_layout(
  title: "Two Y-Axes",
  yaxis: %{title: %{text: "Revenue ($)"}},
  yaxis2: %{
    title: %{text: "Temperature (°C)"},
    overlaying: "y",
    side: "right"
  }
)
|> Plotly.show()

Subplots > Multiple Y-Axes

Three y-axes: narrow the x-axis domain to leave room for the extra axis on the right. yaxis3.position is a 0.0–1.0 fraction of the plot width where the axis line appears.

alias Plotly.{Figure, Scatter}

x = [1, 2, 3, 4, 5]

Figure.new()
|> Figure.add_trace(Scatter.new(x: x, y: [10, 15, 13, 17, 12], name: "Price", mode: "lines"))
|> Figure.add_trace(
  Scatter.new(x: x, y: [1000, 1200, 900, 1400, 1100], name: "Volume", mode: "lines", yaxis: "y2")
)
|> Figure.add_trace(
  Scatter.new(x: x, y: [22, 19, 25, 18, 23], name: "Temp (°C)", mode: "lines", yaxis: "y3")
)
|> Figure.update_layout(
  title: "Multiple Y-Axes",
  xaxis: %{domain: [0.0, 0.8]},
  yaxis: %{title: %{text: "Price"}},
  yaxis2: %{
    title: %{text: "Volume"},
    overlaying: "y",
    side: "right"
  },
  yaxis3: %{
    title: %{text: "Temperature (°C)"},
    overlaying: "y",
    side: "right",
    position: 0.9
  }
)
|> Plotly.show()