# Statwise Visualization Gallery
## 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
```elixir
Mix.install([
{:statwise, path: Path.expand("..", __DIR__)},
{:jason, "~> 1.4"},
{:vega_lite, "~> 0.1"},
{:kino_vega_lite, "~> 0.1"}
])
```
```elixir
alias Statwise.Visualization
```
## Tutorial Data
```elixir
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:`:
```elixir
Visualization.box_plot(rows,
x: :treatment,
y: :score,
color: :treatment,
facet: :site
)
|> Visualization.show()
```
## Relational And Categorical Plots
```elixir
Visualization.scatter(rows, x: :time, y: :score, color: :treatment)
|> Visualization.with_theme(:minimal)
|> Visualization.with_palette(:statwise)
|> Visualization.show()
```
```elixir
Visualization.line(rows, x: :time, y: :score, color: :treatment)
|> Visualization.with_style(width: 420, height: 260)
|> Visualization.show()
```
```elixir
Visualization.bar_plot(rows, x: :treatment, y: :score, stat: :mean)
|> Visualization.show()
```
```elixir
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()
```
```elixir
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()
```
```elixir
Visualization.count_plot(rows, x: :treatment)
|> Visualization.show()
```
```elixir
Visualization.strip_plot(rows, x: :treatment, y: :score)
|> Visualization.show()
```
```elixir
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:`:
```elixir
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:
```elixir
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.
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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
```elixir
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.
```elixir
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:
```elixir
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.
```elixir
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.
```elixir
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:
```elixir
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.
```elixir
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
]
)
```
```elixir
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`.
```elixir
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.
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
plot_with_unknown_style
|> Visualization.to_vega_lite()
|> Map.has_key?("custom_vega_lite_key")
```
## Chart Gallery
The remaining sections show each plot type with a small, runnable example.
## Histogram
```elixir
Visualization.histogram(sample,
bins: 6,
title: "Histogram"
)
|> Visualization.with_style(
width: 420,
height: 260,
color: "#2563eb",
opacity: 0.85
)
|> Visualization.show()
```
```elixir
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
```elixir
Visualization.box_plot(groups,
title: "Grouped Box Plot"
)
|> Visualization.with_style(
width: 420,
height: 260,
color: "#7c3aed"
)
|> Visualization.show()
```
## Faceted Box Plot
```elixir
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
```elixir
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.
```elixir
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.
```elixir
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
```elixir
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
```elixir
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
```elixir
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
```elixir
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()
```
```elixir
mann_whitney_result
```
## Export Helpers
Use `to_vega_lite/1` to inspect the generated Vega-Lite map.
```elixir
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.
```elixir
Visualization.histogram(sample, bins: 6)
|> Visualization.to_json()
```