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, andqq_plot/2. - Apply presentation separately with
with_theme/2,with_palette/2,with_style/2, or with thestyle:export option.
Setup
Mix.install([
{:statwise, path: Path.expand("..", __DIR__)},
{:jason, "~> 1.4"},
{:vega_lite, "~> 0.1"},
{:kino_vega_lite, "~> 0.1"}
])alias Statwise.VisualizationTutorial 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.dataSupported 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:
widthandheightcontrol the chart size. In faceted charts they control each panel.configandvieware 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: [...], andrule: [...]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.styleBut 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")Chart Gallery
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_resultExport 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()