Statwise Visualization Gallery

Copy Markdown View Source

Section

This Livebook is a runnable tour of Statwise.Visualization. It covers the current chart types, result plots, styling, faceting, and export helpers.

The visualization API has two layers:

  • Build chart content with functions such as histogram/2, box_plot/2, and qq_plot/2.
  • Apply presentation separately with with_theme/2, with_palette/2, with_style/2, or with the style: export option.

Setup

Mix.install([
  {:statwise, path: Path.expand("..", __DIR__)},
  {:jason, "~> 1.4"},
  {:vega_lite, "~> 0.1"},
  {:kino_vega_lite, "~> 0.1"}
])
alias Statwise.Visualization

Tutorial Data

sample = [1, 2, 2, 3, 4, 5, 5, 5, 6, 7]

groups = %{
  control: [1.2, 1.8, 2.1, 2.4, 2.5],
  treatment: [2.4, 2.8, 3.0, 3.4, 3.9]
}

rows = [
  %{site: :north, treatment: :control, time: 1, score: 1.2},
  %{site: :north, treatment: :control, time: 2, score: 1.8},
  %{site: :north, treatment: :control, time: 3, score: 2.1},
  %{site: :north, treatment: :treated, time: 1, score: 2.4},
  %{site: :north, treatment: :treated, time: 2, score: 2.8},
  %{site: :north, treatment: :treated, time: 3, score: 3.0},
  %{site: :south, treatment: :control, time: 1, score: 1.0},
  %{site: :south, treatment: :control, time: 2, score: 1.4},
  %{site: :south, treatment: :control, time: 3, score: 1.9},
  %{site: :south, treatment: :treated, time: 1, score: 3.0},
  %{site: :south, treatment: :treated, time: 2, score: 3.4},
  %{site: :south, treatment: :treated, time: 3, score: 4.1}
]

normalish = [10.0, 11.5, 12.2, 13.1, 13.8, 15.0]

heatmap_cells = [
  %{metric: :mean, treatment: :control, value: 1.63},
  %{metric: :mean, treatment: :treated, value: 3.08},
  %{metric: :median, treatment: :control, value: 1.8},
  %{metric: :median, treatment: :treated, value: 3.0}
]

Semantic Mappings

Direct constructors use semantic channels such as x:, y:, color:, and facet::

Visualization.box_plot(rows,
  x: :treatment,
  y: :score,
  color: :treatment,
  facet: :site
)
|> Visualization.show()

Relational And Categorical Plots

Visualization.scatter(rows, x: :time, y: :score, color: :treatment)
|> Visualization.with_theme(:minimal)
|> Visualization.with_palette(:statwise)
|> Visualization.show()
Visualization.line(rows, x: :time, y: :score, color: :treatment)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()
Visualization.bar_plot(rows, x: :treatment, y: :score, stat: :mean)
|> Visualization.show()
Visualization.point_plot(rows,
  x: :treatment,
  y: :score,
  stat: :mean,
  interval: :confidence,
  confidence_level: 0.95
)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()
Visualization.bar_plot(rows,
  x: :treatment,
  y: :score,
  stat: :median,
  interval: :percentile,
  confidence_level: 0.5
)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()
Visualization.count_plot(rows, x: :treatment)
|> Visualization.show()
Visualization.strip_plot(rows, x: :treatment, y: :score)
|> Visualization.show()
Visualization.heatmap(heatmap_cells, x: :treatment, y: :metric, color: :value)
|> Visualization.with_palette(["#e0f2fe", "#0284c7"])
|> Visualization.show()

Faceting

Wrapped facets use facet: :field and columns::

Visualization.box_plot(rows,
  x: :treatment,
  y: :score,
  facet: :site,
  columns: 2
)
|> Visualization.with_style(width: 260, height: 220, color: "red")
|> Visualization.show()

Row and column facets use a facet specification:

Visualization.scatter(rows,
  x: :time,
  y: :score,
  facet: [row: :site, column: :treatment],
  share_y: false
)
|> Visualization.with_style(width: 180, height: 160)
|> Visualization.show()

Statistical Test Annotations

The usual workflow is to compute the test from the same rows used by the plot. That keeps filtering, grouping, and faceting aligned with the visual summary.

rows
|> Visualization.box_plot(x: :treatment, y: :score)
|> Visualization.with_test(:t_test,
  groups: {:control, :treated},
  show: [:p_value, :effect_size]
)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()

Switch to a rank-based comparison without changing the plot:

rows
|> Visualization.box_plot(x: :treatment, y: :score)
|> Visualization.with_test(:mann_whitney,
  groups: {:control, :treated},
  show: [:p_value, :effect_size]
)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()

You can still attach a precomputed result when the test was run separately:

control_scores = for %{treatment: :control, score: score} <- rows, do: score
treated_scores = for %{treatment: :treated, score: score} <- rows, do: score

treatment_result =
  Statwise.TTest.independent(control_scores, treated_scores,
    variance: :welch,
    effect_size: true
  )

rows
|> Visualization.box_plot(x: :treatment, y: :score)
|> Visualization.with_test(treatment_result, groups: {:control, :treated})
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()

Faceted computed tests run independently inside each facet panel:

rows
|> Visualization.box_plot(x: :treatment, y: :score, facet: :site)
|> Visualization.annotate_test(:t_test,
  groups: {:control, :treated},
  show: [:p_value]
)
|> Visualization.with_style(width: 260, height: 220)
|> Visualization.show()

Composition API

rows
|> Visualization.plot(x: :time, y: :score, color: :treatment)
|> Visualization.add(:point)
|> Visualization.add(:line)
|> Visualization.label(title: "Layered Scores")
|> Visualization.show()

Styling Model

with_style/2 attaches presentation choices to a plot without changing the plot data.

styled_histogram =
  Visualization.histogram(sample,
    bins: 6,
    title: "Styled Histogram"
  )
  |> Visualization.with_style(
    width: 420,
    height: 260,
    color: "#2563eb",
    opacity: 0.85,
    background: "#ffffff"
  )

styled_histogram
|> Visualization.show()

The underlying data is still available and unchanged:

styled_histogram.data

Supported Friendly Style Keys

These shortcuts are mapped into Vega-Lite-compatible locations:

  • Top-level/chart layout: width, height, background, padding, autosize, config, view
  • Mark style: color, fill, stroke, opacity, fill_opacity, stroke_opacity, size, stroke_width
  • Layer-specific style: point, reference, rule
  • Raw mark options: mark

In practice:

  • width and height control the chart size. In faceted charts they control each panel.
  • config and view are emitted at the top level of the Vega-Lite spec.
  • color, opacity, size, and stroke/fill options are merged into the mark.
  • mark: [...] is merged directly into the mark definition.
  • point: [...], reference: [...], and rule: [...] target specific layers on layered charts.
Visualization.ecdf(sample,
  title: "Styled ECDF"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  color: "#16a34a",
  stroke_width: 3,
  config: [
    axis: [
      labelColor: "#374151",
      titleColor: "#111827"
    ],
    view: [
      stroke: nil
    ]
  ]
)
|> Visualization.show()

Raw Mark Options

Use mark: for Vega-Lite mark options that belong inside the mark definition.

Visualization.histogram(sample,
  bins: 6,
  title: "Histogram with Mark Options"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  mark: [
    color: "#2563eb",
    opacity: 0.85,
    tooltip: true
  ]
)
|> Visualization.show()

Inspect the generated mark:

Visualization.histogram(sample, bins: 6)
|> Visualization.with_style(
  mark: [
    color: "#2563eb",
    opacity: 0.85,
    tooltip: true
  ]
)
|> Visualization.to_vega_lite()
|> Map.get("mark")

Export-Time Style

Style can also be passed only for one export. This is useful when you want the same plot content rendered in multiple ways.

base_ecdf = Visualization.ecdf(sample, title: "Export-Time Style")

base_ecdf
|> Visualization.to_vega_lite(
  style: [
    width: 420,
    height: 260,
    color: "#16a34a",
    stroke_width: 3
  ]
)
base_ecdf
|> Visualization.show(
  style: [
    width: 420,
    height: 260,
    color: "#dc2626",
    stroke_width: 1
  ]
)

Facet Width and Height

For faceted charts, width and height control each panel, not the whole faceted chart. Statwise places them inside the nested Vega-Lite spec.

Visualization.box_plot(rows,
  x: :treatment,
  y: :score,
  facet: :site,
  columns: 2
)
|> Visualization.with_style(width: 180, height: 220)
|> Visualization.to_vega_lite()
|> Map.take(["facet", "spec"])

Layer-Specific Style

Layered plots can route style to a specific layer. QQ plots use point: for sample points and reference: for the reference line.

Visualization.qq_plot(normalish,
  title: "Layer-Specific QQ Style"
)
|> Visualization.with_style(
  point: [
    color: "#dc2626",
    size: 80
  ],
  reference: [
    color: "#111827",
    stroke_width: 1
  ],
  width: 420,
  height: 260
)
|> Visualization.show()

Vega-Lite Escape Hatches

with_style/2 accepts arbitrary nested maps and keyword lists for supported Vega-Lite routing points including top-level keys, mark:, encoding:, and layer-specific keys such as point:, reference:, and rule:.

When in doubt, inspect the generated spec:

Visualization.histogram(sample, bins: 6)
|> Visualization.with_style(
  width: 420,
  height: 260,
  mark: [tooltip: true],
  config: [axis: [labelColor: "#374151"]]
)
|> Visualization.to_vega_lite()

For example, this custom key is stored on the plot:

plot_with_unknown_style =
  Visualization.histogram(sample, bins: 6)
  |> Visualization.with_style(custom_vega_lite_key: [enabled: true])

plot_with_unknown_style.style

But it is not emitted into the Vega-Lite spec, because the exporter does not yet know where that key belongs:

plot_with_unknown_style
|> Visualization.to_vega_lite()
|> Map.has_key?("custom_vega_lite_key")

The remaining sections show each plot type with a small, runnable example.

Histogram

Visualization.histogram(sample,
  bins: 6,
  title: "Histogram"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  color: "#2563eb",
  opacity: 0.85
)
|> Visualization.show()
Visualization.histogram(%{height: [62, 64, 66, nil, 70, 71]},
  x: :height,
  bins: 4,
  x_title: "Height",
  y_title: "Observations",
  title: "Histogram from a Column"
)
|> Visualization.with_style(width: 420, height: 260, color: "#0f766e")
|> Visualization.show()

Box Plot

Visualization.box_plot(groups,
  title: "Grouped Box Plot"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  color: "#7c3aed"
)
|> Visualization.show()

Faceted Box Plot

Visualization.box_plot(rows,
  x: :treatment,
  y: :score,
  facet: :site,
  columns: 2,
  title: "Scores by Treatment and Site"
)
|> Visualization.with_style(
  width: 280,
  height: 220,
  color: "#2563eb",
  config: [
    axis: [
      labelColor: "#374151",
      titleColor: "#111827"
    ],
    view: [
      stroke: nil
    ]
  ]
)
|> Visualization.show()

ECDF

Visualization.ecdf(sample,
  title: "Empirical CDF"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  color: "#16a34a",
  stroke_width: 3
)
|> Visualization.show()

Normal QQ Plot

The default reference line is scaled to the sample mean and sample standard deviation.

Visualization.qq_plot(normalish,
  title: "Normal QQ Plot"
)
|> Visualization.with_style(
  color: "#dc2626",
  size: 80,
  width: 420,
  height: 260
)
|> Visualization.show()

Use reference_scale: :standard to show the unscaled standard-normal line.

Visualization.qq_plot(normalish,
  reference_scale: :standard,
  title: "QQ Plot with Standard Reference Line"
)
|> Visualization.with_style(
  point: [color: "#dc2626", size: 80],
  reference: [color: "#6b7280", stroke_width: 1],
  width: 420,
  height: 260
)
|> Visualization.show()

Rank Plot

Visualization.rank_plot(
  [10, 30, 40],
  [20, 25, 50],
  x_label: :control,
  y_label: :treatment,
  title: "Rank Plot"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  size: 90,
  color: "#0f766e"
)
|> Visualization.show()

Confidence Interval Plot

one_sample_result =
  Statwise.TTest.one_sample([2.5, 3.1, 3.6, 4.0],
    mean: 3.0
  )

Visualization.confidence_interval(one_sample_result,
  title: "One-Sample Mean Confidence Interval"
)
|> Visualization.with_style(
  width: 420,
  height: 160,
  color: "#0f766e",
  size: 80
)
|> Visualization.show()

T-Test Result Plot

t_test_result =
  Statwise.TTest.independent(
    [1.2, 1.9, 2.4, 2.9],
    [2.2, 3.0, 3.4, 4.1],
    variance: :welch
  )

Visualization.t_test(t_test_result,
  title: "Welch T-Test Mean Difference"
)
|> Visualization.with_style(
  width: 420,
  height: 160,
  color: "#9333ea",
  size: 80
)
|> Visualization.show()

Mann-Whitney U Plot

mann_whitney_result =
  Statwise.MannWhitney.test(
    [1.0, 3.0, 5.0],
    [2.0, 4.0, 6.0],
    method: :auto
  )

Visualization.mann_whitney(mann_whitney_result,
  title: "Mann-Whitney U"
)
|> Visualization.with_style(
  width: 420,
  height: 260,
  color: "#ea580c"
)
|> Visualization.show()
mann_whitney_result

Export Helpers

Use to_vega_lite/1 to inspect the generated Vega-Lite map.

Visualization.box_plot(rows,
  x: :treatment,
  y: :score,
  facet: :site,
  columns: 2
)
|> Visualization.with_style(width: 180, height: 220)
|> Visualization.to_vega_lite()

Use to_json/1 when you need encoded Vega-Lite JSON.

Visualization.histogram(sample, bins: 6)
|> Visualization.to_json()