Half-Eye Plot
A one-sided density curve paired with a point-and-interval summary — the cleaner sibling of the raincloud plot. Shows distributional shape and key statistics without the noise of raw data points.
// 01 — The chart
What it looks like
Half-eye plots showing Bayesian posterior distributions for three regression coefficients. The dot is the median; thick and thin lines show 66% and 95% credible intervals. The dashed vertical at zero highlights that all three intervals exclude the null effect.
// 02 — Definition
What is a half-eye plot?
A half-eye plot combines a one-sided density curve (the “half eye”) with a point-interval summary underneath. The density shows the full distributional shape, while the point-interval provides quick-read statistics — typically a median point with credible / confidence intervals as horizontal lines.
It’s essentially a raincloud plot without the raw data dots. This makes it cleaner and more compact, trading individual-level detail for a more polished, publication-ready appearance. It’s the chart of choice for Bayesian statisticians presenting posterior distributions, and it has become a standard in journals that publish Bayesian model summaries.
The name “half-eye” comes from the shape: a half-violin (density curve on one side only) with a point below it, which visually resembles a sideways eye. The asymmetric design is intentional — it avoids the visual redundancy of a full (mirrored) violin plot while reserving the empty side of the axis for explicit interval line widths that encode credibility levels.
Two interval levels are typically drawn: a thicker line (often 50% or 66%) for the central mass and a thinner line (often 95%) for the broader uncertainty band. This dual-width encoding makes credibility immediately legible without any axis ticks or numbers.
Origin: The half-eye plot was popularized through Matthew Kay’s R package ggdist (released 2019, building on earlier tidybayes work), which provides a systematic grammar for visualizing distributions with layered density + interval representations.
// 03 — When to use
When a half-eye plot is the right call
Reach for a half-eye plot whenever you need to communicate a full distribution along with explicit uncertainty intervals — most often for Bayesian posteriors, but increasingly for any summary of a sampling distribution.
- Communicating Bayesian posterior distributions for coefficients or parameters
- Comparing distributions across groups in a publication-ready way
- You need shape + summary statistics in a single chart
- Raincloud-style raw data dots would crowd the figure
- Showing simulation outputs or bootstrap distributions
- You need to emphasize credible / confidence intervals at multiple levels
- Producing figures for an academic paper, slide, or technical report
// 04 — When not to use
When a half-eye plot is the wrong call
Half-eye plots assume a numerate audience and a continuous distribution to summarize. Outside that sweet spot, simpler charts almost always win.
- Your audience is unfamiliar with density curves and credible intervals
- You only have a few data points per group — a strip plot or dot plot is more honest
- You need to show individual observations — use a raincloud or strip plot instead
- A simple bar chart with error bars would communicate the same conclusion
- The distribution is binary or strongly bimodal in a way density estimation distorts
- Space is too tight to draw two interval widths legibly
- Your data is not really a distribution — just a set of point estimates
// 05 — Data requirements
What your data needs to look like
Before building the chart, your dataset needs to fit a specific shape. Use this checklist to confirm yours does.
Shape
Long format — one row per draw / observation, with a categorical group column and a numeric value column.
Minimum rows
A few hundred draws per group. Below ~100 draws, the density estimate becomes unreliable.
Maximum rows
No effective upper bound. ggdist and ArviZ both handle hundreds of thousands of draws routinely.
A label for each row in the eventual half-eye axis. Typically a parameter name (β₁, β₂, …), a treatment arm, or a model term.
The numeric draw being summarized. Each row is a single sample from the distribution — a posterior draw, a bootstrap replicate, or an experimental measurement.
Optional draw / iteration index. Useful when downstream code groups by chain or iteration to compute interval widths.
| parameter | draw | value |
|---|---|---|
| β₁ | 1 | 0.31 |
| β₁ | 2 | 0.28 |
| β₂ | 1 | -0.18 |
| β₂ | 2 | -0.22 |
| β₃ | 1 | 0.21 |
| β₃ | 2 | 0.19 |
Tip: if your input is a tidy MCMC fit (PyMC, Stan, brms, NumPyro), you almost certainly already have it in long format. tidybayes::gather_draws() in R and ArviZ’s to_dataframe() in Python both produce the right shape directly.
// 06 — Anatomy
Parts of a half-eye plot
A half-eye is composed of four pieces: the one-sided density slab, the median point, the thicker inner interval, and the thinner outer interval. Knowing each piece by name makes it easy to talk about which to keep and which to drop.
// 07 — Step-by-step
Step-by-step: how to build a good half-eye plot
An eight-step recipe that works regardless of tool. The bandwidth and interval-level choices are the only steps that need taste; the rest is mechanical once the data is in long format.
- 1
Decide what the distribution represents
Half-eye plots are most often used for Bayesian posteriors, but they also work for bootstrap distributions, simulation outputs, or repeated measurements. Be explicit in the title — “posterior”, “bootstrap”, or “observed” — so the reader knows what to interpret. - 2
Reshape the data into long format
One row per draw, two columns: the group label and the numeric value. ggdist, ArviZ, and Observable Plot all expect this shape; wide formats need a pivot first. - 3
Pick the interval widths
The convention is to draw two intervals: a thicker one at 50–66% and a thinner one at 95%. Some teams use 89% in place of 95% to break the false-alarm pattern of ‘0.95’ being read as a p-value cutoff. - 4
Choose a sensible bandwidth
Densities are estimated, not measured. The default kernel-density bandwidth usually works, but you should sanity-check by overlaying the empirical histogram and confirming the curve isn’t dramatically over- or under-smoothed. - 5
Sort the categorical axis meaningfully
Order groups by posterior median or by an experimental design (treatment arms, model terms in canonical order). Alphabetical order is almost never the right choice. - 6
Add a reference line at zero or null effect
When the posterior is for a coefficient, draw a faint vertical line at zero so readers can visually check whether the credible interval excludes the null. - 7
Title the takeaway
Replace “Posterior distributions” with “All three coefficients exclude zero with 95% credibility — β₁ has the strongest effect.” The chart should announce the conclusion. - 8
Check that intervals stay legible at thumbnail size
Half-eye intervals are thin lines; if your panel will be embedded in a paper margin or dashboard tile, verify the thicker / thinner contrast is still readable.
// 08 — Real-world examples
Where you’ll see half-eye plots used
Half-eye plots have become the default chart for Bayesian model summaries in academic publications and well-documented industry analyses.
Bayesian regression: posterior coefficients
Almost every published Bayesian regression now ships posterior summaries as half-eye plots, one row per coefficient, with the canonical 66/95 interval pair and a reference line at zero.
StatisticsA/B testing: lift per variant
Modern experimentation platforms summarize each variant’s lift as a half-eye over the posterior of the treatment effect. Decision-makers read the chart by checking which intervals exclude zero.
ExperimentationEpidemiology: parameter estimates from disease models
Compartmental epidemic models (SEIR, age-structured) report key parameters — R₀, generation interval, IFR — as half-eye summaries that capture both central estimate and posterior spread.
EpidemiologySports analytics: player skill estimates
Hierarchical models of player skill produce posteriors with very different widths between veterans and rookies. Half-eye plots make the uncertainty visible alongside the point estimate.
Sports analytics// 09 — Variations
Variants of the half-eye plot
ggdist exposes a family of related geoms; each replaces the density slab or the interval encoding with something different to answer a different question.
Eye plot (full violin + interval)
Mirrors the density on both sides. Use when distributional shape is the headline and the asymmetry of half-eye feels too sparse.
Gradient interval
Replaces discrete interval lines with a smooth color gradient encoding interval width. More compact but harder to read precise levels.
Dotsinterval
Replaces the density slab with a quantile-dotplot — a small constant number of dots arranged to mimic the density. Better at communicating frequency framing.
CCDF interval
Plots the complementary cumulative distribution function with intervals overlaid. Used when readers care about the probability of exceeding a threshold.
// 10 — Comparisons
How it compares
Half-eye sits between violin (more shape) and raincloud (more raw data) in the family of distribution charts. Box plots are the older, lossier alternative.
Half-eye vs violin plot
Both show distributional shape. A violin mirrors the density on both sides; a half-eye uses only one side and adds an explicit point-interval summary, which makes the central tendency and credibility easier to read at a glance.
Half-eye plot
One-sided density paired with a point-and-interval summary. Designed for posteriors and other distributional summaries with explicit uncertainty.
- Asymmetric — no mirrored ink
- Point + intervals are explicit
- Best for posteriors and credible-interval style summaries
Violin plot
Symmetric density on both sides of the categorical axis. Better when shape is the headline and explicit intervals aren’t needed.
- Mirrored density emphasizes shape
- No built-in interval summary
- Better for raw empirical distributions
Half-eye vs raincloud plot
A raincloud is a half-eye with raw data dots glued underneath. The raincloud is more honest about per-observation noise; the half-eye is cleaner and more publication-friendly.
Half-eye plot
Density + point-interval summary, no raw data. Compact and presentation-ready.
- Lower ink-to-data ratio
- Cleaner at small sizes
- Hides individual observations
Raincloud plot
Density + interval + a strip of raw data dots. Better when readers need to see individual observations or sample size.
- Shows per-observation detail
- Higher ink-to-data ratio
- Best with small / moderate n
Half-eye vs box plot
Box plots compress the distribution into five numbers; half-eye plots show the full distributional shape plus an interval summary. Half-eye reveals multimodality and skew that box plots silently hide.
Half-eye plot
Continuous density curve with explicit credible / confidence intervals. Reveals modes, skew, and tails.
- Shows distributional shape
- Explicit interval levels
- Reveals multimodality
Box plot
Five-number summary (min, Q1, median, Q3, max) drawn as a box with whiskers. Compact but lossy.
- Five-number summary, no shape
- Hides multimodality
- Familiar to all audiences
// 11 — Common mistakes
Common half-eye plot mistakes
The chart is forgiving of bandwidth choice but unforgiving of unlabeled intervals. Almost every bad half-eye in the wild fails on the same handful of issues.
Failing to label the interval levels
Without an explicit caption, no one can tell whether you drew 50/89, 66/95, or 50/95 intervals. State it.
Truncating the density at the interval bounds
Cutting the density off where the 95% interval ends hides skew and tails. Let the curve extend over the actual support.
Mirroring the density to both sides
Mirroring is exactly what a violin does; a half-eye should be one-sided to leave room for the interval visual.
Picking decorative colors instead of meaningful ones
Color should encode group identity. Random palettes from the default colormap dilute the signal.
Plotting on an axis without a reference line
For coefficients, omit the dashed line at zero and you make readers do mental arithmetic. Always include it.
Comparing groups without a consistent axis order
Sort the categorical axis by median or by experimental design. Alphabetical order is almost always wrong.
// 12 — Accessibility
Accessibility checklist
Run through this list before publishing. The chart should still communicate its message to readers using assistive technology, color-blind users, keyboard navigation, and reduced-motion settings.
- ✓
Color contrast for density fills and interval lines
WCAG 1.4.3Density fills should reach at least 3:1 against the chart background; interval lines and the median dot should reach 4.5:1 against any region they overlay so they remain visible at thumbnail size. - ✓
Don’t rely on color to distinguish groups
WCAG 1.4.1When multiple groups share a panel, encode each group’s identity with both color and direct labels (or marker shape). Color-vision-deficient readers should still be able to map each density to its group. - ✓
Provide a text alternative for the chart
WCAG 1.1.1An accessible name should describe the takeaway: “Posteriors for three regression coefficients; all three 95% credible intervals exclude zero with β₁ having the largest median effect.” - ✓
Expose the underlying summary table
WCAG 1.3.1Place a small table next to the chart with each group’s median and interval bounds. Screen readers and analysts both rely on it. The chart’s shape information can’t be experienced without sight. - ✓
Label the interval level explicitly
WCAG 3.3.2Captions or legend entries must say what each line represents (“thick = 66% CI, thin = 95% CI”). Without the label, the visual asymmetry is meaningless. - ✓
Respect prefers-reduced-motion
WCAG 2.3.3If your interactive version animates the density on hover, gate the animation behind prefers-reduced-motion: no-preference. Static rendering is fine for everyone. - ✓
Keyboard-accessible interactivity
WCAG 2.1.1If the plot is interactive (tooltips on hover, focus-driven highlighting), every density / interval pair must be reachable with the Tab key with a visible focus ring.
// 13 — Best practices
Design and craft tips
A short list of dos and don’ts that separate the publication-quality half-eye plots from the demo-grade ones.
Always draw two interval levels
Mirror the density to make a violin
Anchor the categorical axis
Truncate the density at the interval bounds
Label the interval levels in the caption
Use a default colormap with five colors
Add a faint reference line at zero
Overlay raincloud dots when n is huge
// 15 — Tool instructions
How to build it in your tool of choice
Recipes for the libraries that have a real half-eye implementation, plus pragmatic workarounds for tools that don’t.
R (ggplot2 + ggdist)
Code — ~5 min- 01Install ggplot2 and ggdist with install.packages(c('ggplot2', 'ggdist')).
- 02Reshape your samples into a long-format tibble: one row per draw, columns for group label and numeric value.
- 03Pipe into ggplot(df, aes(x = value, y = group)).
- 04Add stat_halfeye(.width = c(0.66, 0.95)) for the canonical thick + thin interval pair.
- 05Layer geom_vline(xintercept = 0, linetype = 'dashed', alpha = 0.4) when comparing to a null effect.
- 06Polish with labs(), theme_minimal(), and a takeaway-style title.
ggdist is the reference implementation — every other tool’s half-eye is essentially a port of Matthew Kay’s grammar.
Python (ArviZ)
Code — ~5 min- 01Install ArviZ and matplotlib with pip install arviz matplotlib.
- 02Load your MCMC samples into an ArviZ InferenceData (most PPLs export this directly).
- 03Call az.plot_forest(idata, kind='ridgeplot', combined=True) for the closest built-in equivalent.
- 04Or use az.plot_density() for true half-density curves and overlay az.plot_hdi() for the intervals.
- 05Set figsize tightly so the asymmetric layout doesn’t leave a lot of empty space.
- 06Add a title naming the model and the credible interval level.
For more flexible half-eye-style plots in Python, the seaborn.violinplot(split=True, inner='quart') trick gets you most of the way without ArviZ.
JavaScript (Observable Plot)
Code — ~10 min- 01Install Observable Plot with npm i @observablehq/plot or include via CDN.
- 02Compute the density per group with a kernel-density helper (Plot.binY or a custom kde).
- 03Render the density as Plot.areaX(... , { fillOpacity: 0.3, curve: 'basis' }) restricted to one side of the categorical axis.
- 04Compute median and interval bounds with d3.quantile.
- 05Overlay Plot.ruleY for the thin (95%) interval and Plot.ruleY({ strokeWidth: 4 }) for the thick (66%) interval.
- 06Add a Plot.dot for the median point and a Plot.ruleX at zero for the reference line.
There’s no first-party half-eye mark in Observable Plot today; the recipe above composes density + ruleY + dot to imitate the ggdist output.
Tableau
BI — ~7 min- 01Tableau doesn’t ship a half-eye mark. The two routes are: (1) calculate density bins in a calculated field and draw them as filled-area marks, or (2) embed an R or Python script.
- 02For the density route, bin your value column into ~50 buckets and aggregate count per bucket per group.
- 03Use a polygon mark to draw the half-density on one side of the categorical axis.
- 04Compute median, 66%, and 95% percentiles in three calculated fields.
- 05Layer a reference line per group for each percentile, with line thickness encoding the interval level.
- 06Combine via dual axis with the polygon density on the secondary axis.
If you have R/Python integration set up, calling ggdist via TabPy / Rserve is dramatically easier than reproducing the geom by hand.
Power BI
BI — ~7 min- 01Power BI has no native half-eye visual. The pragmatic option is the R script visual.
- 02Drag your group and value fields into the Values well of an R script visual.
- 03Paste a ggplot2 + ggdist script that uses stat_halfeye() on the supplied dataset.
- 04Set ggsave dimensions in the script so the visual exports at the report’s panel size.
- 05Add a title via the Power BI title bar that names the model and interval levels.
- 06Cache the visual’s underlying dataset so the R script doesn’t re-run on every interaction.
The R script visual requires R to be installed locally and configured under File → Options → R scripting in Power BI Desktop.
// 16 — Code examples
Working code in the most common stacks
Three runnable snippets that produce equivalent half-eye plots in R (the canonical ggdist version), Python (ArviZ), and JavaScript (Observable Plot composition). Each uses the same simulated three-coefficient posterior.
library(ggplot2)
library(ggdist)
library(dplyr)
set.seed(42)
# Simulated posterior samples for three regression coefficients.
posterior <- bind_rows(
tibble(parameter = "β₁", draw = 1:4000, value = rnorm(4000, 0.32, 0.12)),
tibble(parameter = "β₂", draw = 1:4000, value = rnorm(4000, -0.18, 0.16)),
tibble(parameter = "β₃", draw = 1:4000, value = rnorm(4000, 0.21, 0.10))
)
ggplot(posterior, aes(x = value, y = parameter, fill = parameter)) +
stat_halfeye(
.width = c(0.66, 0.95),
point_interval = "median_qi",
slab_alpha = 0.5,
slab_color = "#1a1a18",
slab_size = 0.4
) +
geom_vline(xintercept = 0, linetype = "dashed", alpha = 0.4) +
scale_fill_manual(values = c("#c94a2e", "#e8c4b8", "#c94a2e"), guide = "none") +
labs(
title = "All three coefficients exclude zero — β₁ has the largest effect",
subtitle = "Posterior medians with 66% (thick) and 95% (thin) credible intervals",
x = "Coefficient value",
y = NULL
) +
theme_minimal(base_size = 12) +
theme(panel.grid.major.y = element_blank(),
plot.title.position = "plot")
// 17 — FAQs
Frequently asked questions
What is a half-eye plot?+
A half-eye plot combines a one-sided density curve (the ‘half eye’) with a point-and-interval summary underneath. The density shows the full distributional shape, while the point-interval provides quick-read statistics — typically a median point with credible or confidence intervals as horizontal lines.
When should you use a half-eye plot?+
Use a half-eye plot when communicating Bayesian posteriors, comparing distributions across groups in a publication-ready way, or when you need to show both shape and key statistics in a single chart. It is the preferred chart when raincloud-style raw data dots would crowd the figure.
When should you avoid a half-eye plot?+
Avoid a half-eye plot when your audience needs to see individual observations — use a raincloud or strip plot instead. Skip it when you have only a handful of data points, when categorical bar charts would communicate the same conclusion, or when readers are not familiar with density curves and credible intervals.
What is the difference between a half-eye plot and a violin plot?+
A violin plot mirrors the density on both sides; a half-eye plot draws it on one side only and pairs it with an explicit point-interval summary below. The asymmetric design avoids the visual redundancy of a full violin while making the median and intervals easier to compare across groups.
What’s the difference between a half-eye plot and a raincloud plot?+
A raincloud plot adds a strip of raw data dots beneath the density and interval. A half-eye plot omits the dots, trading per-observation detail for a cleaner, more compact, publication-ready figure.
Is a half-eye plot suitable for dashboards?+
Yes — half-eye plots work well in dashboards as long as the panel is large enough to read the interval line widths, the title makes clear which interval level is shown, and the comparison axis runs along a consistent direction across panels.
What category of chart is a half-eye plot?+
Half-Eye Plot belongs to the Distribution family of charts. Charts in that family are designed to answer the same kind of question — what is the shape and spread of a continuous variable — so they often work as alternatives when one doesn’t quite fit your data.
How do you read a half-eye plot?+
Start with the density curve to get the overall shape and any skew. Then look at the dot below it for the central tendency, the thicker line for the tighter interval (typically 50% or 66%), and the thinner line for the wider interval (typically 95%). Compare these summaries across groups along the categorical axis.
What’s the best library for building half-eye plots?+
For R, Matthew Kay’s ggdist package (paired with ggplot2) is the canonical choice and the source of the modern half-eye grammar. For Python, ArviZ provides built-in half-eye-style posterior plots, and seaborn or matplotlib can approximate the chart with violinplot half-options. For the web, Observable Plot or D3 with a kernel-density helper produce custom versions.
// 18 — References
References and further reading
Primary sources, official library documentation, and the foundational papers on Bayesian visualization cited throughout this guide.
- The reference implementation of the half-eye / slab-interval grammar in R. Includes the original vignettes that defined the modern conventions for thick/thin intervals.https://mjskay.github.io/ggdist/
- Detailed walkthrough of stat_halfeye and its siblings (stat_eye, stat_dotsinterval, stat_gradientinterval), explaining the design choices behind each variant.https://mjskay.github.io/ggdist/articles/slabinterval.html
- ArviZ’s closest built-in equivalent to a half-eye plot, with the kind=‘ridgeplot’ option used in the Python sample.https://python.arviz.org/en/stable/api/generated/arviz.plot_forest.html
- Reference for the Observable Plot marks (areaY, ruleY, dot) used to compose a half-eye in JavaScript.https://observablehq.com/plot/
- The raincloud-plot paper, which formalized the close cousin of the half-eye and motivates the design trade-offs between density, interval, and raw-data layers.https://wellcomeopenresearch.org/articles/4-63/v2
- Background on why box plots fail for large data and where distributional plots like the half-eye are necessary.https://www.tandfonline.com/doi/abs/10.1080/10618600.2017.1305277
- Open-access book by the author of cowplot/ggridges. Chapters 9 and 10 cover distribution charts, including density-interval composites.https://clauswilke.com/dataviz/
- WAI — Complex Images: Charts and GraphsAccessibilityWeb Accessibility Initiative guidance on text alternatives, long descriptions, and data tables for complex distributional plots.https://www.w3.org/WAI/tutorials/images/complex/
- Official reference for the ggplot2 layers and theming used to build half-eye plots in R.https://ggplot2.tidyverse.org/reference/