Powered by AppSignal & Oban Pro

Statistical Charts

notebooks/05_statistical.livemd

Statistical Charts

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

Statistical Charts > Basic Symmetric Error Bars

error_y (or error_x) is a plain map on any Scatter or Bar trace. Key fields: type:, array:, visible: true. type: "data" means the error values are explicit per-point values in array:.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [0, 1, 2],
    y: [6, 10, 2],
    error_y: %{type: "data", array: [1, 2, 3], visible: true}
  )
)
|> Figure.update_layout(title: "Basic Symmetric Error Bars")
|> Plotly.show()

Statistical Charts > Bar Chart with Error Bars

error_y works on Bar traces too. Multiple bar groups can each carry independent error bars.

alias Plotly.{Figure, Bar}

Figure.new()
|> Figure.add_trace(
  Bar.new(
    x: ["Trial 1", "Trial 2", "Trial 3"],
    y: [3, 6, 4],
    name: "Control",
    error_y: %{type: "data", array: [1, 0.5, 1.5], visible: true}
  )
)
|> Figure.add_trace(
  Bar.new(
    x: ["Trial 1", "Trial 2", "Trial 3"],
    y: [4, 7, 3],
    name: "Experiment",
    error_y: %{type: "data", array: [0.5, 1.0, 0.8], visible: true}
  )
)
|> Figure.update_layout(title: "Bar Chart with Error Bars", barmode: "group")
|> Plotly.show()

Statistical Charts > Horizontal Error Bars

error_x adds horizontal error bars. Both error_x and error_y can appear on the same trace.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [1, 2, 3, 4],
    y: [2, 1, 3, 4],
    mode: "markers",
    error_x: %{type: "data", array: [0.2, 0.3, 0.1, 0.4], visible: true}
  )
)
|> Figure.update_layout(title: "Horizontal Error Bars")
|> Plotly.show()

Statistical Charts > Asymmetric Error Bars

arrayminus: sets the downward (or leftward) error values independently from array: (upward/rightward). Without arrayminus, both directions use array: symmetrically.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [1, 2, 3, 4],
    y: [2, 1, 3, 4],
    mode: "markers",
    error_y: %{
      type: "data",
      array: [0.1, 0.2, 0.3, 0.4],
      arrayminus: [0.4, 0.3, 0.2, 0.1],
      visible: true
    }
  )
)
|> Figure.update_layout(title: "Asymmetric Error Bars")
|> Plotly.show()

Statistical Charts > Colored and Styled Error Bars

color: sets the error bar colour. thickness: sets the line width of the bar. width: sets the length of the horizontal cap at the end of each bar.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [0, 1, 2],
    y: [6, 10, 2],
    mode: "markers",
    marker: %{color: "rgb(164, 194, 244)", size: 12},
    error_y: %{
      type: "data",
      array: [1, 2, 3],
      visible: true,
      color: "purple",
      thickness: 2.5,
      width: 8
    },
    error_x: %{
      type: "data",
      array: [0.1, 0.2, 0.1],
      visible: true,
      color: "purple",
      thickness: 2.5,
      width: 8
    }
  )
)
|> Figure.update_layout(title: "Colored and Styled Error Bars")
|> Plotly.show()

Statistical Charts > Error Bars as a Percentage of the y-Value

type: "percent" + value: N sets error bars to ±N% of each y value. type: "sqrt" sets error bars to ±√y (no value needed — useful for count data).

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [0, 1, 2, 3, 4, 5, 6, 7, 8],
    y: [8, 7, 6, 5, 4, 3, 2, 1, 0],
    error_y: %{type: "percent", value: 10, visible: true}
  )
)
|> Figure.update_layout(title: "Error Bars as 10% of y-Value")
|> Plotly.show()

Statistical Charts > Asymmetric Error Bars with a Constant Offset

type: "constant" + value: sets all upward bars to the same absolute value. Add valueminus: for a different constant in the downward direction.

alias Plotly.{Figure, Scatter}

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: [1, 2, 3, 4],
    y: [2, 4, 3, 5],
    mode: "markers+lines",
    error_y: %{type: "constant", value: 0.5, valueminus: 1.0, visible: true}
  )
)
|> Figure.update_layout(title: "Asymmetric Error Bars — Constant Offset")
|> Plotly.show()

Statistical Charts > Basic Box Plot

Box.new(y: [...]) creates a vertical box plot from raw data. Plotly automatically computes the quartiles, median, whiskers, and outliers. Use name: for the legend label.

alias Plotly.{Figure, Box}

Figure.new()
|> Figure.add_trace(Box.new(y: [0, 1, 1, 2, 3, 5, 8, 13, 21], name: "Fibonacci"))
|> Figure.update_layout(title: "Basic Box Plot")
|> Plotly.show()

Statistical Charts > Box Plot That Displays the Underlying Data

boxpoints: "all" overlays every raw data point on the box. jitter (0–1) adds horizontal scatter to the points so they don’t overlap. pointpos shifts points relative to the box centre (negative = left).

alias Plotly.{Figure, Box}

Figure.new()
|> Figure.add_trace(
  Box.new(
    y: [0, 1, 1, 2, 3, 5, 8, 13, 21],
    name: "With Points",
    boxpoints: "all",
    jitter: 0.3,
    pointpos: -1.8
  )
)
|> Figure.update_layout(title: "Box Plot Displaying the Underlying Data")
|> Plotly.show()

Statistical Charts > Horizontal Box Plot

Pass data as x: instead of y: to create a horizontal box plot. Multiple traces stack vertically.

alias Plotly.{Figure, Box}

Figure.new()
|> Figure.add_trace(Box.new(x: [0, 1, 1, 2, 3, 5, 8, 13, 21], name: "Set 1"))
|> Figure.add_trace(Box.new(x: [2, 3, 4, 5, 6, 7, 8, 9, 10], name: "Set 2"))
|> Figure.update_layout(title: "Horizontal Box Plot")
|> Plotly.show()

Statistical Charts > Grouped Box Plot

Assign the same x: category values across multiple Box traces to group them side-by-side. layout.boxmode: "group" positions boxes of the same category next to each other.

alias Plotly.{Figure, Box}

x = ["day 1", "day 1", "day 1", "day 2", "day 2", "day 2"]

Figure.new()
|> Figure.add_trace(Box.new(y: [0.2, 0.2, 0.6, 1.0, 0.5, 0.4], x: x, name: "kale",     marker: %{color: "#3D9970"}))
|> Figure.add_trace(Box.new(y: [0.6, 0.7, 0.3, 0.6, 0.0, 0.5], x: x, name: "radishes", marker: %{color: "#FF4136"}))
|> Figure.add_trace(Box.new(y: [0.1, 0.3, 0.1, 0.9, 0.6, 0.6], x: x, name: "carrots",  marker: %{color: "#FF851B"}))
|> Figure.update_layout(
  title: "Grouped Box Plot",
  yaxis: %{title: "normalized moisture", zeroline: false},
  boxmode: "group"
)
|> Plotly.show()

Statistical Charts > Box Plot Styling Outliers

boxpoints: "suspectedoutliers" shows only the statistical outlier points (beyond 1.5× IQR). marker.outliercolor sets the outlier point fill colour. marker.line.outliercolor and marker.line.outlierwidth style the outlier point border.

alias Plotly.{Figure, Box}

data = [0.75, 5.25, 5.5, 6.0, 6.2, 6.6, 6.80, 7.0, 7.2, 7.5, 7.5, 7.75,
        8.15, 8.15, 8.65, 8.93, 9.2, 9.5, 10.0, 10.25, 11.5, 12.0, 16.0, 20.90, 22.3, 23.25]

Figure.new()
|> Figure.add_trace(
  Box.new(
    y: data,
    name: "Styled Outliers",
    boxpoints: "suspectedoutliers",
    marker: %{
      color: "rgb(8,81,156)",
      outliercolor: "rgba(219, 64, 82, 0.6)",
      line: %{outliercolor: "rgba(219, 64, 82, 1.0)", outlierwidth: 2}
    },
    line: %{color: "rgb(8,81,156)"}
  )
)
|> Figure.update_layout(title: "Box Plot Styling Outliers")
|> Plotly.show()

Statistical Charts > Box Plot Styling Mean and Standard Deviation

boxmean: true draws a dashed line at the mean. boxmean: "sd" draws the mean line and shades ±1 standard deviation.

alias Plotly.{Figure, Box}

data = [2.37, 2.16, 4.82, 1.73, 1.04, 0.23, 1.32, 2.91, 0.11, 4.51,
        0.51, 3.75, 1.35, 2.98, 4.50, 0.18, 4.66, 1.30, 2.06, 1.19]

Figure.new()
|> Figure.add_trace(Box.new(y: data, name: "Mean line only", boxmean: true))
|> Figure.add_trace(Box.new(y: data, name: "Mean + SD",      boxmean: "sd"))
|> Figure.update_layout(title: "Box Plot — Mean and Standard Deviation")
|> Plotly.show()

Statistical Charts > Grouped Horizontal Box Plot

Combine orientation: "h" with categorical y: values and layout.boxmode: "group" for grouped horizontal boxes. With horizontal orientation, x: holds the data values and y: holds the category.

alias Plotly.{Figure, Box}

y = ["day 1", "day 1", "day 1", "day 2", "day 2", "day 2"]

Figure.new()
|> Figure.add_trace(Box.new(x: [0.2, 0.2, 0.6, 1.0, 0.5, 0.4], y: y, name: "kale",     orientation: "h", marker: %{color: "#3D9970"}))
|> Figure.add_trace(Box.new(x: [0.6, 0.7, 0.3, 0.6, 0.0, 0.5], y: y, name: "radishes", orientation: "h", marker: %{color: "#FF4136"}))
|> Figure.add_trace(Box.new(x: [0.1, 0.3, 0.1, 0.9, 0.6, 0.6], y: y, name: "carrots",  orientation: "h", marker: %{color: "#FF851B"}))
|> Figure.update_layout(title: "Grouped Horizontal Box Plot", boxmode: "group")
|> Plotly.show()

Statistical Charts > Colored Box Plot

fillcolor sets the box interior colour (accepts rgba for transparency). marker.color sets the outlier / jitter point colour. line.color sets the box border and whisker colour.

alias Plotly.{Figure, Box}

Figure.new()
|> Figure.add_trace(
  Box.new(
    y: [1, 2, 3, 4, 5, 6, 7, 8, 9],
    name: "Set 1",
    fillcolor: "rgba(93, 164, 214, 0.5)",
    marker: %{color: "rgb(93, 164, 214)"},
    line: %{color: "rgb(8, 48, 107)"}
  )
)
|> Figure.add_trace(
  Box.new(
    y: [2, 3, 4, 5, 6, 7, 8, 9, 10],
    name: "Set 2",
    fillcolor: "rgba(255, 144, 14, 0.5)",
    marker: %{color: "rgb(255, 144, 14)"},
    line: %{color: "rgb(107, 48, 8)"}
  )
)
|> Figure.update_layout(title: "Colored Box Plot")
|> Plotly.show()

Statistical Charts > Fully Styled Box Plot

Combining all box plot styling: fill colour, border, mean line, underlying data points, jitter, and layout customisation.

alias Plotly.{Figure, Box}

# Generate some sample data groups
:rand.seed(:exsss, {1, 2, 3})
make_data = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

Figure.new()
|> Figure.add_trace(
  Box.new(
    y: make_data.(50),
    name: "Group A",
    fillcolor: "rgba(93, 164, 214, 0.5)",
    marker: %{color: "rgb(93, 164, 214)", size: 4, outliercolor: "rgba(219,64,82,0.6)"},
    line: %{color: "rgb(8, 48, 107)"},
    boxmean: "sd",
    boxpoints: "suspectedoutliers"
  )
)
|> Figure.add_trace(
  Box.new(
    y: make_data.(50),
    name: "Group B",
    fillcolor: "rgba(255, 144, 14, 0.5)",
    marker: %{color: "rgb(255, 144, 14)", size: 4, outliercolor: "rgba(219,64,82,0.6)"},
    line: %{color: "rgb(107, 48, 8)"},
    boxmean: "sd",
    boxpoints: "suspectedoutliers"
  )
)
|> Figure.update_layout(
  title: "Fully Styled Box Plot",
  yaxis: %{zeroline: false},
  showlegend: true
)
|> Plotly.show()

Statistical Charts > Rainbow Box Plot

Seven Box traces with rainbow colours demonstrate the full colour spectrum. Each trace’s y data is offset slightly to space the boxes apart visually.

alias Plotly.{Figure, Box}

:rand.seed(:exsss, {42, 42, 42})

colors = ["red", "orangered", "orange", "gold", "green", "steelblue", "mediumpurple"]
names  = ["Red", "Orange-Red", "Orange", "Yellow", "Green", "Blue", "Violet"]

fig =
  Enum.zip(colors, names)
  |> Enum.with_index()
  |> Enum.reduce(Figure.new(), fn {{color, name}, i}, fig ->
    y = for _ <- 1..30 do
      (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0) |> Kernel.+(i * 0.5)
    end

    Figure.add_trace(fig, Box.new(
      y: y,
      name: name,
      fillcolor: color,
      line: %{color: color},
      marker: %{color: color}
    ))
  end)

fig
|> Figure.update_layout(title: "Rainbow Box Plot", showlegend: true)
|> Plotly.show()

Statistical Charts > Basic Histogram

Histogram.new(x: [...]) automatically bins the data and counts occurrences per bin. Plotly chooses the number of bins automatically; use nbinsx: to override.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

# Central Limit Theorem approximation: sum of 12 uniform(0,1) - 6 ≈ N(0,1)
x = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

Figure.new()
|> Figure.add_trace(Histogram.new(x: x))
|> Figure.update_layout(title: "Basic Histogram")
|> Plotly.show()

Statistical Charts > Horizontal Histogram

Pass data as y: instead of x: to produce a horizontal histogram. The x-axis becomes the count axis; y-axis shows the bin ranges.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

y = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

Figure.new()
|> Figure.add_trace(Histogram.new(y: y))
|> Figure.update_layout(title: "Horizontal Histogram")
|> Plotly.show()

Statistical Charts > Overlaid Histogram

layout.barmode: "overlay" renders multiple histograms on top of each other. Use opacity (0–1) on each trace so the overlapping bins remain visible.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

x1 = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

x2 = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(5.5)
end

Figure.new()
|> Figure.add_trace(Histogram.new(x: x1, name: "x1", opacity: 0.5))
|> Figure.add_trace(Histogram.new(x: x2, name: "x2", opacity: 0.5))
|> Figure.update_layout(
  title: "Overlaid Histogram",
  barmode: "overlay"
)
|> Plotly.show()

Statistical Charts > Stacked Histograms

layout.barmode: "stack" stacks histogram bars of the same bin on top of each other. The total bar height shows the combined count across all traces for that bin.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

x1 = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

x2 = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(5.5)
end

Figure.new()
|> Figure.add_trace(Histogram.new(x: x1, name: "x1"))
|> Figure.add_trace(Histogram.new(x: x2, name: "x2"))
|> Figure.update_layout(title: "Stacked Histograms", barmode: "stack")
|> Plotly.show()

Statistical Charts > Colored and Styled Histograms

marker.color sets the bar fill colour (CSS string or rgba). marker.line.color and marker.line.width style the bar border.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

x = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

Figure.new()
|> Figure.add_trace(
  Histogram.new(
    x: x,
    marker: %{
      color: "rgba(255, 100, 102, 0.7)",
      line: %{color: "rgba(255, 100, 102, 1.0)", width: 1}
    },
    opacity: 0.75
  )
)
|> Figure.update_layout(
  title: "Colored and Styled Histogram",
  xaxis: %{title: "Value"},
  yaxis: %{title: "Count"},
  bargap: 0.05
)
|> Plotly.show()

Statistical Charts > Cumulative Histogram

cumulative: %{enabled: true} turns a histogram into a cumulative distribution. Each bar shows how many data points fall at or below that bin’s upper bound.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

x = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

Figure.new()
|> Figure.add_trace(Histogram.new(x: x, name: "Regular",    opacity: 0.5))
|> Figure.add_trace(Histogram.new(x: x, name: "Cumulative", opacity: 0.3, cumulative: %{enabled: true}))
|> Figure.update_layout(
  title: "Cumulative Histogram",
  barmode: "overlay"
)
|> Plotly.show()

Statistical Charts > Normalized Histogram

histnorm normalises the y-axis:

  • "" (default) — raw counts
  • "probability" — each bar is count / total (sums to 1)
  • "percent" — each bar is (count / total) × 100 (sums to 100)
  • "density" — count / (bin width × total) — useful for comparing distributions of different sizes
  • "probability density" — density that integrates to 1
alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

x = for _ <- 1..500 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

for norm <- ["", "probability", "percent", "density"] do
  label = if norm == "", do: "count (default)", else: norm

  Figure.new()
  |> Figure.add_trace(Histogram.new(x: x, histnorm: (if norm == "", do: nil, else: norm)))
  |> Figure.update_layout(title: "histnorm: \"#{label}\"")
  |> Plotly.show()
end
|> Kino.Layout.grid(columns: 2)

Statistical Charts > Specify Binning Function

histfunc changes what is computed per bin (default "count"):

  • "count" — number of data points in the bin
  • "sum" — sum of all y: values for points in the bin (requires x: for bins + y: for values)
  • "avg" — average of y: values per bin
  • "min" / "max" — min or max of y: values per bin

nbinsx overrides the automatic bin count.

alias Plotly.{Figure, Histogram}

:rand.seed(:exsss, {1, 2, 3})

# x = categories, y = values to aggregate
x = for _ <- 1..200, do: :rand.uniform() * 10
y = for _ <- 1..200, do: :rand.uniform() * 100

for func <- ["count", "sum", "avg", "min", "max"] do
  Figure.new()
  |> Figure.add_trace(
    Histogram.new(
      x: x,
      y: (if func == "count", do: nil, else: y),
      histfunc: func,
      nbinsx: 10
    )
  )
  |> Figure.update_layout(title: "histfunc: \"#{func}\"", xaxis: %{title: "x"}, yaxis: %{title: func})
  |> Plotly.show()
end
|> Kino.Layout.grid(columns: 3)

Statistical Charts > 2D Histogram Contour Plot with Histogram Subplots (Slider Control)

A Histogram2dcontour in the centre with marginal Histogram traces on the top and right. The layout.sliders widget (same technique as custom_controls/09_basic_slider.livemd) uses method: "restyle" to update the ncontours property on the contour trace in real time — no JavaScript callbacks required.

Subplot domains:

region xaxis yaxis x domain y domain
contour (main) x y [0, 0.85] [0, 0.85]
top histogram x y2 [0, 0.85] [0.85, 1]
right histogram x2 y [0.85, 1] [0, 0.85]
alias Plotly.{Figure, Histogram, Histogram2dcontour}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 500
x = gen.(n)
y = gen.(n)

ncontours_values = [3, 5, 10, 15, 20, 30]

slider_steps =
  for {nc, _i} <- Enum.with_index(ncontours_values) do
    %{
      method: "restyle",
      label: "#{nc}",
      args: [%{ncontours: nc}, [0]]
    }
  end

# Default active index — ncontours = 20 is index 4
default_index = Enum.find_index(ncontours_values, &amp;(&amp;1 == 20))

Figure.new()
|> Figure.add_trace(
  Histogram2dcontour.new(
    x: x, y: y,
    ncontours: 20,
    colorscale: "Blues",
    reversescale: true,
    showscale: false,
    xaxis: "x",
    yaxis: "y"
  )
)
|> Figure.add_trace(
  Histogram.new(
    x: x,
    xaxis: "x",
    yaxis: "y2",
    showlegend: false,
    marker: %{color: "rgba(31, 119, 180, 0.7)"}
  )
)
|> Figure.add_trace(
  Histogram.new(
    y: y,
    xaxis: "x2",
    yaxis: "y",
    showlegend: false,
    marker: %{color: "rgba(31, 119, 180, 0.7)"}
  )
)
|> Figure.update_layout(
  title: "2D Contour Histogram with Marginals — Slider Controls Contour Count",
  showlegend: false,
  autosize: false,
  width: 600,
  height: 580,
  margin: %{t: 60, l: 50, r: 20, b: 110},
  hovermode: "closest",
  bargap: 0,
  xaxis: %{domain: [0, 0.85], showgrid: false, zeroline: false, title: "x"},
  yaxis: %{domain: [0, 0.85], showgrid: false, zeroline: false, title: "y"},
  xaxis2: %{domain: [0.85, 1], showgrid: false, zeroline: false},
  yaxis2: %{domain: [0.85, 1], showgrid: false, zeroline: false},
  sliders: [%{
    steps: slider_steps,
    active: default_index,
    currentvalue: %{prefix: "Contours: ", visible: true, xanchor: "center"},
    pad: %{t: 60, b: 10}
  }]
)
|> Plotly.show()

Statistical Charts > Filled Lines (Continuous Error Band)

A continuous error band (shaded confidence interval) uses three Scatter traces:

  1. Upper boundfill: nil, mode: "lines", thin line or invisible
  2. Lower boundfill: "tonexty", which fills the area between this trace and the one drawn above it
  3. Mean line — drawn on top

The key is that Plotly fills between consecutive traces in the order they are added. Use fillcolor on the lower-bound trace to set the band colour. Use line: %{color: "transparent"} to hide a bound line while keeping the fill anchor.

alias Plotly.{Figure, Scatter}

x = Enum.to_list(1..10)
y_mean  = [2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
y_upper = [3, 4, 5, 6, 7, 6, 5, 4, 3, 2]
y_lower = [1, 2, 3, 4, 5, 4, 3, 2, 1, 0]

Figure.new()
|> Figure.add_trace(
  # Upper bound — acts as the fill anchor for the next trace
  Scatter.new(
    x: x, y: y_upper,
    mode: "lines",
    line: %{color: "transparent"},
    name: "Upper bound",
    showlegend: false
  )
)
|> Figure.add_trace(
  # Lower bound — fill: "tonexty" fills between this and the upper bound above
  Scatter.new(
    x: x, y: y_lower,
    mode: "lines",
    fill: "tonexty",
    fillcolor: "rgba(0, 100, 255, 0.2)",
    line: %{color: "transparent"},
    name: "Confidence band",
    showlegend: true
  )
)
|> Figure.add_trace(
  # Mean line drawn on top
  Scatter.new(
    x: x, y: y_mean,
    mode: "lines",
    line: %{color: "rgb(0, 100, 255)"},
    name: "Mean"
  )
)
|> Figure.update_layout(title: "Continuous Error Band (Filled Lines)")
|> Plotly.show()

Statistical Charts > Asymmetric Filled Error Bands

Asymmetric filled error bands work the same as symmetric bands but with different upper and lower offsets from the mean. Two fill: "tonexty" traces sandwich the upper and lower bounds respectively.

alias Plotly.{Figure, Scatter}

x = Enum.to_list(1..10)
y_mean  = [2, 3, 4, 5, 4, 3, 2, 3, 4, 5]
# Upper offset is larger than lower — asymmetric band
y_upper = Enum.zip_with(y_mean, [1.5,1.5,2.0,2.0,1.5,1.0,1.0,1.5,2.0,2.0], &amp;(&amp;1 + &amp;2))
y_lower = Enum.zip_with(y_mean, [0.5,0.5,0.5,1.0,0.5,0.5,0.5,0.5,0.5,0.5], &amp;(&amp;1 - &amp;2))

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: x, y: y_upper,
    mode: "lines",
    line: %{color: "transparent"},
    name: "Upper bound",
    showlegend: false
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: x, y: y_lower,
    mode: "lines",
    fill: "tonexty",
    fillcolor: "rgba(200, 50, 50, 0.2)",
    line: %{color: "transparent"},
    name: "Asymmetric band",
    showlegend: true
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: x, y: y_mean,
    mode: "lines",
    line: %{color: "rgb(200, 50, 50)"},
    name: "Mean"
  )
)
|> Figure.update_layout(title: "Asymmetric Filled Error Bands")
|> Plotly.show()

Statistical Charts > 2D Histogram of a Bivariate Normal Distribution

Histogram2d.new(x: [...], y: [...]) creates a 2D histogram (heatmap of counts). Both x and y must have the same length — each element is one observation. The colour intensity encodes the count in each 2D bin.

alias Plotly.{Figure, Histogram2d}

:rand.seed(:exsss, {1, 2, 3})

# ~N(0,1) via Central Limit Theorem
gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 500
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(Histogram2d.new(x: x, y: y))
|> Figure.update_layout(title: "2D Histogram of a Bivariate Normal Distribution")
|> Plotly.show()

Statistical Charts > 2D Histogram Binning and Styling Options

nbinsx: / nbinsy: control how many bins appear on each axis. colorscale: picks a named Plotly colour scale (e.g. "Hot", "Viridis", "Jet"). reversescale: true flips the colour direction.

alias Plotly.{Figure, Histogram2d}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 500
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(
  Histogram2d.new(
    x: x,
    y: y,
    nbinsx: 20,
    nbinsy: 20,
    colorscale: "Hot",
    reversescale: true
  )
)
|> Figure.update_layout(title: "2D Histogram — Custom Bins and Colourscale")
|> Plotly.show()

Statistical Charts > 2D Histogram Overlaid with a Scatter Chart

Combining a Histogram2d with a Scatter trace on the same axes lets you see both the density distribution and the individual data points at once. Set opacity on the histogram to let the scatter points show through.

alias Plotly.{Figure, Histogram2d, Scatter}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 200
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(
  Histogram2d.new(
    x: x, y: y,
    colorscale: "YlOrRd",
    opacity: 0.7
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: x, y: y,
    mode: "markers",
    marker: %{color: "white", size: 4, opacity: 0.5}
  )
)
|> Figure.update_layout(title: "2D Histogram Overlaid with Scatter")
|> Plotly.show()

Statistical Charts > Basic SPC Control Chart

A Statistical Process Control (SPC) chart tracks a process over time with three horizontal reference lines: the mean (centre line), and upper/lower control limits (UCL/LCL), typically set at ±3σ.

layout.shapes draws horizontal lines across the full x range. Each shape entry is a map with type: "line", x0/x1 (or use "paper" coordinates), and y0/y1 set to the same value for a horizontal line.

alias Plotly.{Figure, Scatter}

:rand.seed(:exsss, {10, 20, 30})

n = 30
# Simulated process measurements with occasional shift
process = for i <- 1..n do
  base = (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  if i > 20, do: base + 1.5, else: base
end

mean  = Enum.sum(process) / n
sigma = :math.sqrt(Enum.sum(Enum.map(process, fn v -> (v - mean) * (v - mean) end)) / n)
ucl   = mean + 3 * sigma
lcl   = mean - 3 * sigma

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: Enum.to_list(1..n),
    y: process,
    mode: "lines+markers",
    name: "Measurements",
    marker: %{size: 6}
  )
)
|> Figure.update_layout(
  title: "Basic SPC Control Chart",
  shapes: [
    %{type: "line", x0: 1, x1: n, y0: mean, y1: mean,
      line: %{color: "green", dash: "solid", width: 2}},
    %{type: "line", x0: 1, x1: n, y0: ucl, y1: ucl,
      line: %{color: "red", dash: "dash", width: 1.5}},
    %{type: "line", x0: 1, x1: n, y0: lcl, y1: lcl,
      line: %{color: "red", dash: "dash", width: 1.5}}
  ],
  annotations: [
    %{x: n, y: mean, xanchor: "left", text: "Mean", showarrow: false},
    %{x: n, y: ucl,  xanchor: "left", text: "UCL (+3σ)", showarrow: false},
    %{x: n, y: lcl,  xanchor: "left", text: "LCL (−3σ)", showarrow: false}
  ]
)
|> Plotly.show()

Statistical Charts > SPC Control Chart and Distribution

The control chart (left, 70% width) shows measurements with UCL/LCL and a centre line drawn as scatter traces so they appear in the legend. Points outside the control limits are highlighted as violation markers.

The distribution histogram (right, 20% width) uses orientation: "h" so bins run along the shared y-scale. It has its own independent yaxis2 anchored to xaxis2, with tick labels hidden to avoid duplication.

alias Plotly.{Figure, Scatter, Histogram}

:rand.seed(:exsss, {10, 20, 30})

n = 30
process =
  for i <- 1..n do
    base = (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
    if i > 20, do: base + 1.5, else: base
  end

mean  = Enum.sum(process) / n
sigma = :math.sqrt(Enum.sum(Enum.map(process, fn v -> (v - mean) * (v - mean) end)) / n)
ucl   = mean + 3 * sigma
lcl   = mean - 3 * sigma

violations =
  process
  |> Enum.with_index(1)
  |> Enum.filter(fn {v, _i} -> v > ucl or v < lcl end)

viol_x = Enum.map(violations, fn {_v, i} -> i end)
viol_y = Enum.map(violations, fn {v, _i} -> v end)

Figure.new()
|> Figure.add_trace(
  Scatter.new(
    x: Enum.to_list(1..n),
    y: process,
    mode: "lines+markers",
    name: "Data",
    line: %{color: "blue", width: 2},
    marker: %{color: "blue", size: 8, symbol: "circle"}
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: viol_x,
    y: viol_y,
    mode: "markers",
    name: "Violation",
    marker: %{color: "rgb(255,65,54)", size: 12, symbol: "circle-open", line: %{width: 3}, opacity: 0.5}
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: [0.5, n + 0.5, nil, 0.5, n + 0.5],
    y: [lcl, lcl, nil, ucl, ucl],
    mode: "lines",
    name: "LCL/UCL",
    line: %{color: "red", dash: "dash", width: 2}
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: [0.5, n + 0.5],
    y: [mean, mean],
    mode: "lines",
    name: "Centre",
    line: %{color: "grey", width: 2}
  )
)
|> Figure.add_trace(
  Histogram.new(
    y: process,
    orientation: "h",
    name: "Distribution",
    xaxis: "x2",
    yaxis: "y2",
    marker: %{color: "blue", line: %{color: "white", width: 1}}
  )
)
|> Figure.update_layout(
  title: "SPC Control Chart and Distribution",
  xaxis: %{domain: [0, 0.7], zeroline: false},
  yaxis: %{zeroline: false},
  xaxis2: %{domain: [0.8, 1]},
  yaxis2: %{anchor: "x2", matches: "y", showticklabels: false}
)
|> Plotly.show()

Statistical Charts > Basic Violin Plot

Violin.new(y: [...]) creates a vertical violin plot. The shape mirrors a kernel density estimate (KDE) of the data. Like a box plot, it shows median, quartiles, and whiskers, but also the full distribution shape.

alias Plotly.{Figure, Violin}

:rand.seed(:exsss, {1, 2, 3})

y = for _ <- 1..100 do
  (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
end

Figure.new()
|> Figure.add_trace(Violin.new(y: y, name: "Sample"))
|> Figure.update_layout(title: "Basic Violin Plot")
|> Plotly.show()

Statistical Charts > Grouped Violin Plot

Multiple Violin traces with the same categorical x: values group side-by-side. layout.violinmode: "group" places them next to each other (default is "overlay").

alias Plotly.{Figure, Violin}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n, shift ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0 - shift)
  end
end

Figure.new()
|> Figure.add_trace(
  Violin.new(
    y: gen.(50, 0.0) ++ gen.(50, 0.0),
    x: List.duplicate("Mon", 50) ++ List.duplicate("Tue", 50),
    name: "Group A",
    box: %{visible: true},
    meanline: %{visible: true}
  )
)
|> Figure.add_trace(
  Violin.new(
    y: gen.(50, 1.0) ++ gen.(50, 1.0),
    x: List.duplicate("Mon", 50) ++ List.duplicate("Tue", 50),
    name: "Group B",
    box: %{visible: true},
    meanline: %{visible: true}
  )
)
|> Figure.update_layout(
  title: "Grouped Violin Plot",
  violinmode: "group"
)
|> Plotly.show()

Statistical Charts > Horizontal Violin Plot

orientation: "h" makes the violin horizontal. Pass data as x: instead of y:. Multiple horizontal violins stack vertically using categorical y: values.

alias Plotly.{Figure, Violin}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

Figure.new()
|> Figure.add_trace(
  Violin.new(
    x: gen.(80),
    y: List.duplicate("Group A", 80),
    name: "Group A",
    orientation: "h",
    box: %{visible: true}
  )
)
|> Figure.add_trace(
  Violin.new(
    x: gen.(80),
    y: List.duplicate("Group B", 80),
    name: "Group B",
    orientation: "h",
    box: %{visible: true}
  )
)
|> Figure.update_layout(title: "Horizontal Violin Plot")
|> Plotly.show()

Statistical Charts > Split Violin Plot

side: "positive" draws the KDE on the right side of the centre line only. side: "negative" draws only the left side. violinmode: "overlay" layers both traces on the same axis so the two halves form a single split violin for each category.

alias Plotly.{Figure, Violin}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n, shift ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0 - shift)
  end
end

Figure.new()
|> Figure.add_trace(
  Violin.new(
    y: gen.(80, 0.0),
    name: "Male",
    side: "negative",
    line: %{color: "blue"},
    meanline: %{visible: true}
  )
)
|> Figure.add_trace(
  Violin.new(
    y: gen.(80, 0.5),
    name: "Female",
    side: "positive",
    line: %{color: "orchid"},
    meanline: %{visible: true}
  )
)
|> Figure.update_layout(
  title: "Split Violin Plot",
  violinmode: "overlay"
)
|> Plotly.show()

Statistical Charts > Advanced Violin Plot

Combining all violin options in one chart:

  • box: %{visible: true} — overlays a box plot inside the violin
  • points: "all" — shows every data point as a jittered dot
  • meanline: %{visible: true} — draws a line at the mean
  • jitter — spreads the overlay points horizontally
  • pointpos — shifts the point cloud left or right of the violin centre

Each trace is given an explicit x array so each violin sits at its own categorical x position. pointpos: -1.0 shifts dots to the left of the violin centre without overflowing into the neighbouring violin — ±1.8 is only safe for a single isolated violin; for adjacent violins the shift lands the dots inside the neighbouring body. xaxis: range is extended slightly so the leftmost violin’s (Kale) dots don’t clip at the axis boundary.

alias Plotly.{Figure, Violin}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n, shift ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0 - shift)
  end
end

n = 60

Figure.new()
|> Figure.add_trace(
  Violin.new(
    x: List.duplicate("Kale", n),
    y: gen.(n, 0.0),
    name: "Kale",
    box: %{visible: true},
    meanline: %{visible: true},
    points: "all",
    jitter: 0.05,
    pointpos: -1.0,
    line: %{color: "green"},
    marker: %{color: "green", size: 4}
  )
)
|> Figure.add_trace(
  Violin.new(
    x: List.duplicate("Radishes", n),
    y: gen.(n, 0.5),
    name: "Radishes",
    box: %{visible: true},
    meanline: %{visible: true},
    points: "all",
    jitter: 0.05,
    pointpos: -1.0,
    line: %{color: "red"},
    marker: %{color: "red", size: 4}
  )
)
|> Figure.add_trace(
  Violin.new(
    x: List.duplicate("Carrots", n),
    y: gen.(n, 1.0),
    name: "Carrots",
    box: %{visible: true},
    meanline: %{visible: true},
    points: "all",
    jitter: 0.05,
    pointpos: -1.0,
    line: %{color: "orange"},
    marker: %{color: "orange", size: 4}
  )
)
|> Figure.update_layout(
  title: "Advanced Violin Plot",
  xaxis: %{range: [-0.8, 2.5]}
)
|> Plotly.show()

Statistical Charts > Basic Parallel Categories Diagram

Parcats.new(dimensions: [...]) renders a parallel categories (alluvial / Sankey-for-categories) diagram. Each dimension is a map with values: (one entry per observation) and label: (column header). The flows represent how observations move between categories across dimensions.

alias Plotly.{Figure, Parcats}

Figure.new()
|> Figure.add_trace(
  Parcats.new(
    dimensions: [
      %{values: ["Cat A", "Cat A", "Cat B", "Cat B", "Cat A", "Cat B", "Cat A"],
        label: "Dimension 1"},
      %{values: ["High",  "High",  "Low",   "High",  "Low",   "Low",   "High"],
        label: "Dimension 2"},
      %{values: ["X",     "Y",     "X",     "Y",     "Y",     "X",     "X"],
        label: "Dimension 3"}
    ]
  )
)
|> Figure.update_layout(title: "Basic Parallel Categories Diagram")
|> Plotly.show()

Statistical Charts > Parallel Categories Diagram with Counts

counts: is a list of integers, one per unique combination of category values. It lets you pass pre-aggregated data instead of one row per observation. The length of counts must equal the number of unique value combinations. When using counts, each element of values in each dimension must be the same length as counts.

alias Plotly.{Figure, Parcats}

# Pre-aggregated: 4 unique combinations, with their counts
Figure.new()
|> Figure.add_trace(
  Parcats.new(
    dimensions: [
      %{values: ["Female", "Female", "Male", "Male"], label: "Gender"},
      %{values: ["1",      "2",      "1",    "2"],    label: "Class"}
    ],
    counts: [9, 40, 10, 41]
  )
)
|> Figure.update_layout(title: "Parallel Categories with Counts")
|> Plotly.show()

Statistical Charts > Multi-Color Parallel Categories Diagram

line: %{color: [...], colorscale: "..."} colours each flow by a numeric variable. Provide one colour value per observation (same length as values in each dimension). The colorscale maps the numeric range to colours (e.g. "Jet", "Viridis", "Hot").

alias Plotly.{Figure, Parcats}

# colour index: 0 = Survived, 1 = Died
survived = [0, 0, 0, 1, 1, 1, 1, 0, 0, 1]

Figure.new()
|> Figure.add_trace(
  Parcats.new(
    dimensions: [
      %{values: ["Female", "Female", "Male", "Male", "Male", "Female", "Female", "Male", "Male", "Female"],
        label: "Gender"},
      %{values: ["1", "2", "1", "3", "2", "3", "1", "2", "3", "2"],
        label: "Class"},
      %{values: ["Adult","Child","Adult","Adult","Child","Adult","Child","Adult","Adult","Adult"],
        label: "Age"}
    ],
    line: %{
      color: survived,
      colorscale: [[0, "lightsteelblue"], [1, "mediumseagreen"]],
      shape: "hspline"
    }
  )
)
|> Figure.update_layout(title: "Multi-Color Parallel Categories")
|> Plotly.show()

Statistical Charts > Scatter Plot Matrix (SPLOM) of the Iris Dataset

Splom.new(dimensions: [...]) creates a Scatter Plot Matrix (SPLOM). Each dimension is a map with label: (axis title) and values: (numeric list). Every pair of dimensions gets its own scatter panel.

The Iris dataset is inlined here as simple representative data.

alias Plotly.{Figure, Splom}

# Simplified Iris dataset (20 samples, 3 classes encoded as 0/1/2)
sepal_len  = [5.1,4.9,4.7,4.6,5.0,5.4,4.6,5.0,4.4,4.9,7.0,6.4,6.9,5.5,6.5,5.7,6.3,4.9,6.6,5.2]
sepal_wid  = [3.5,3.0,3.2,3.1,3.6,3.9,3.4,3.4,2.9,3.1,3.2,3.2,3.1,2.3,2.8,2.8,3.3,2.4,2.9,2.7]
petal_len  = [1.4,1.4,1.3,1.5,1.4,1.7,1.4,1.5,1.4,1.5,4.7,4.5,4.9,4.0,4.6,4.5,4.7,3.3,4.6,3.9]
petal_wid  = [0.2,0.2,0.2,0.2,0.2,0.4,0.3,0.2,0.2,0.1,1.4,1.5,1.5,1.3,1.5,1.3,1.6,1.0,1.3,1.4]
species    = [0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1]

Figure.new()
|> Figure.add_trace(
  Splom.new(
    dimensions: [
      %{label: "Sepal Length", values: sepal_len},
      %{label: "Sepal Width",  values: sepal_wid},
      %{label: "Petal Length", values: petal_len},
      %{label: "Petal Width",  values: petal_wid}
    ],
    marker: %{
      color: species,
      colorscale: "Viridis",
      size: 5,
      showscale: false,
      line: %{width: 0.5, color: "white"}
    }
  )
)
|> Figure.update_layout(
  title: "SPLOM — Iris Dataset",
  dragmode: "select",
  height: 700,
  margin: %{l: 130}
)
|> Plotly.show()

Statistical Charts > Scatter Plot Matrix (SPLOM) — Styled

showupperhalf: false hides the upper triangle, leaving only the lower half and diagonal. diagonal: %{visible: false} removes the histograms on the diagonal. marker.size can be reduced for denser datasets.

This example uses synthetic 3-variable data to demonstrate multi-class colouring.

alias Plotly.{Figure, Splom}

:rand.seed(:exsss, {7, 14, 21})

# 3 clusters of 20 points each, encoded as class 0/1/2
gen_cluster = fn n, cx, cy, cz ->
  for _ <- 1..n do
    x = cx + ((for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0))
    y = cy + ((for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0))
    z = cz + ((for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0))
    {x, y, z}
  end
end

all = gen_cluster.(20, 0, 0, 0) ++ gen_cluster.(20, 5, 3, 2) ++ gen_cluster.(20, -4, 4, -2)
x_vals = Enum.map(all, fn {x, _, _} -> x end)
y_vals = Enum.map(all, fn {_, y, _} -> y end)
z_vals = Enum.map(all, fn {_, _, z} -> z end)
classes = List.duplicate(0, 20) ++ List.duplicate(1, 20) ++ List.duplicate(2, 20)

Figure.new()
|> Figure.add_trace(
  Splom.new(
    dimensions: [
      %{label: "Feature A", values: x_vals},
      %{label: "Feature B", values: y_vals},
      %{label: "Feature C", values: z_vals}
    ],
    marker: %{
      color: classes,
      colorscale: "Portland",
      size: 4,
      showscale: false
    },
    showupperhalf: false,
    diagonal: %{visible: false}
  )
)
|> Figure.update_layout(title: "Styled SPLOM — Lower Half Only")
|> Plotly.show()

Statistical Charts > Basic 2D Histogram Contour

Histogram2dcontour.new(x: [...], y: [...]) creates a 2D kernel density contour plot. Like Histogram2d but draws contour lines instead of filled squares. Both x and y must have the same length (one point per observation).

alias Plotly.{Figure, Histogram2dcontour}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 300
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(Histogram2dcontour.new(x: x, y: y))
|> Figure.update_layout(title: "Basic 2D Histogram Contour")
|> Plotly.show()

Statistical Charts > 2D Histogram Contour Colorscale

colorscale: picks the named colour scale for the contour fill. reversescale: true flips the colour direction (lowest count = darkest). ncontours: sets how many contour levels are drawn.

alias Plotly.{Figure, Histogram2dcontour}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 300
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(
  Histogram2dcontour.new(
    x: x,
    y: y,
    colorscale: "Hot",
    reversescale: true,
    ncontours: 20
  )
)
|> Figure.update_layout(title: "2D Histogram Contour — Custom Colorscale")
|> Plotly.show()

Statistical Charts > Styled 2D Histogram Contour

contours: %{showlines: false} removes the contour outlines, leaving only the fill. line: %{width: 0} achieves the same result when using the line key. Overlaying a Scatter on top shows individual data points within the density map.

alias Plotly.{Figure, Histogram2dcontour, Scatter}

:rand.seed(:exsss, {1, 2, 3})

gen = fn n ->
  for _ <- 1..n do
    (for _ <- 1..12, do: :rand.uniform()) |> Enum.sum() |> Kernel.-(6.0)
  end
end

n = 300
x = gen.(n)
y = gen.(n)

Figure.new()
|> Figure.add_trace(
  Histogram2dcontour.new(
    x: x,
    y: y,
    colorscale: "Blues",
    ncontours: 15,
    contours: %{showlines: false}
  )
)
|> Figure.add_trace(
  Scatter.new(
    x: x,
    y: y,
    mode: "markers",
    marker: %{color: "rgba(255,255,255,0.5)", size: 3},
    showlegend: false
  )
)
|> Figure.update_layout(title: "Styled 2D Histogram Contour")
|> Plotly.show()