Scatter plots reveal relationships between two variables, but they hide the univariate distribution along each axis. This recipe adds marginal distribution graphics -- a ridgeline plot along the top and a histogram along the right -- to provide density context alongside each dimension.
The Visualization
Marginal graphics are small distribution visualizations rendered in the axis margins of scatter and bubble charts. They show the univariate distribution along each axis, providing density context alongside the bivariate relationship.
You can configure marginals on any side using the marginalGraphics prop with a type string ("histogram", "violin", "ridgeline", "boxplot") or a config object with type, bins, fill, fillOpacity, stroke, and strokeWidth. This chart shows Giancarlo Stanton's hit distance vs exit velocity.
import { StreamXYFrame } from "semiotic" const frameProps = { /* --- Data --- */ data: [{ game_date: "2017-05-08", bx: -16.29, by: 419.09, distance: 419, exit_velocity: 111.2 }, { game_date: "2017-08-25", bx: 118.41, by: 377.24, distance: 395, exit_velocity: 104.8 }, ... ], chartType: "scatter", /* --- Size --- */ size: [700,400], margin: { left: 70, right: 60, top: 60, bottom: 60 }, /* --- Process --- */ xAccessor: "distance", yAccessor: "exit_velocity", /* --- Customize --- */ pointStyle: d => ({ fill: "#E0488B", fillOpacity: 0.7, r: 4 }), showAxes: true, xLabel: "Distance", yLabel: "Exit Velocity", marginalGraphics: { top: { type: "ridgeline", fill: "#e0d33a", fillOpacity: 0.5, stroke: "#e0d33a" }, right: { type: "histogram", fill: "#e0d33a", fillOpacity: 0.5 } }, /* --- Interact --- */ enableHover: true } export default () => { return <StreamXYFrame {...frameProps} /> }
How It Works
Marginal graphics are configured using the marginalGraphicsprop on StreamXYFrame, Scatterplot, orBubbleChart. Each side (top,bottom, left, right) can host a different distribution type:
JSX
<StreamXYFrame chartType="scatter" data={data} xAccessor="distance" yAccessor="exit_velocity" marginalGraphics={{ top: { type: "ridgeline", fill: "#9fd0cb", fillOpacity: 0.5 }, right: { type: "histogram", fill: "#9fd0cb", fillOpacity: 0.5 } }} margin={{ left: 70, right: 60, top: 60, bottom: 60 }} />
You can also use string shorthand for defaults:marginalGraphics={{ top: "histogram", left: "boxplot" }}
Margins are auto-expanded to at least 60px on any side with a marginal, so there is enough room for the distribution visualization.
Available Marginal Types
- histogram — Binned bars showing frequency distribution
- ridgeline — One-sided density area fill
- violin — Symmetric density path centered on the margin midline
- boxplot — Box-and-whisker showing quartiles and whiskers
Config Options
JS
// Full config object { type: "histogram" | "violin" | "ridgeline" | "boxplot", bins: 20, // number of bins (histogram/violin/ridgeline) fill: "#4e79a7", // fill color fillOpacity: 0.5, // fill opacity stroke: "none", // stroke color strokeWidth: 1 // stroke width }
Using with HOC Charts
The marginalGraphics prop is available directly onScatterplot and BubbleChart:
JSX
<Scatterplot data={iris} xAccessor="sepalLength" yAccessor="petalLength" colorBy="species" marginalGraphics={{ top: "histogram", right: "violin" }} />
Streaming Marginal Graphics
Marginal graphics work with streaming data too. When usingruntimeMode="streaming" on a StreamXYFrame, the marginals update live as new data arrives via theref.push() API. The distributions reflect the current sliding window of data, recomputing on every render frame.
Marginal graphics update live as streaming data arrives. This example pushes bivariate data points via the ref.push() API. The ridgeline (top) and histogram (right) reflect the current distribution within the sliding window.
The key is combining runtimeMode="streaming" with marginalGraphics on a StreamXYFrame. The marginals are recomputed on every render frame from the current window of data.
JSX
const chartRef = useRef() // Push bivariate data points useEffect(() => { const id = setInterval(() => { const x = Math.random() * 100 const y = x * 0.8 + (Math.random() - 0.5) * 25 chartRef.current.push({ time: Date.now(), x, y }) }, 80) return () => clearInterval(id) }, []) <StreamXYFrame ref={chartRef} chartType="scatter" runtimeMode="streaming" xAccessor="x" yAccessor="y" windowSize={200} marginalGraphics={{ top: { type: "ridgeline", fill: "#9fd0cb", fillOpacity: 0.5 }, right: { type: "histogram", fill: "#9fd0cb", fillOpacity: 0.5 } }} margin={{ left: 70, right: 70, top: 70, bottom: 60 }} size={[700, 400]} />
You can use any of the four marginal types (histogram, ridgeline, violin, boxplot) in streaming mode. Each side updates independently as the window slides forward.
Key Takeaways
- The
marginalGraphics prop on StreamXYFrame, Scatterplot, and BubbleChart creates distribution visualizations in the chart margins. - Four types are supported: histogram, ridgeline, violin, and boxplot.
- Each side (top, bottom, left, right) can use a different type independently.
- Margins auto-expand to 60px minimum to ensure enough room for the visualization.
- Streaming mode (
runtimeMode="streaming") updates marginals live as data arrives viaref.push().