QuadrantChart is a 2×2 scatterplot that lets you pick two metrics, split each axis at a threshold, and label the four resulting cells. The chart's job is to focus on regions of "where does this item land?" that have meaning beyond just the correlation that scatterplots traditionally provide.
Why this exists
Most strategic frameworks are 2×2 grids: the BCG growth-share matrix (high/low market growth × high/low market share), the Eisenhower urgent/important matrix, RICE scoring, MoSCoW prioritization, risk/value, effort/impact. The 2×2 is so common in product, strategy, and ops that plotting items on one is its own visual language.
A QuadrantChart is what you reach for when:
- You have two metrics that span a meaningful threshold (above/below average effort; high/low strategic value; adopted/not-adopted).
- The four resulting categories have NAMES the audience recognizes ("quick wins," "money pits," "long bets," "fill-in").
- The decision the chart supports is which quadrant is this in, not "what's the continuous trend."
Live demo
Effort (1–10, low to high) × impact (1–10, low to high) for a synthetic feature backlog. Quadrants:
- Top-left (Quick wins) shows low effort, high impact items. Do these first.
- Top-right (Strategic bets) shows high effort, high impact items. Worth doing but plan carefully.
- Bottom-left (Fill-in) shows low effort, low impact items. Pull into a sprint when you have slack.
- Bottom-right (Money pits) shows high effort, low impact items. Don't do these.
How to read it
- Axes are both quantitative, just like any scatterplot. Pick metrics whose threshold values are meaningful (median, target, "minimum viable," etc.). It's okay to use hand-wavy values there are entire industries that run on quadrants plotting hand-wavy values.
- Quadrant tints should color the cells and double-encode by coloring the points and use semantically meaningful colors if you're using them though many quadrants avoid color.
- Threshold lines are usually drawn at
xCenter / yCenter. Omit and the chart uses the domain midpoint.
When to reach for it
Reach for QuadrantChart when:
- You have a 2×2 framework that reflects how your audience already thinks about the problem.
- You can justify the thresholds without making arbitrary cuts that make the labels misleading.
- The dataset has a manageable number of items (fewer than 20); beyond that the labels overlap and the chart loses its point.
Reach for something else when:
- The metrics don't naturally split at a threshold. Just use a plain Scatterplot instead and let the eye find the cluster.
- You actually have a third dimension you want to encode (e.g. revenue per item). Use a BubbleChart (you can still pass
quadrants via annotations on a regular Scatterplot if you want quadrant labels too).
Wiring it up
import { QuadrantChart } from "semiotic" <QuadrantChart data={backlog} xAccessor="effort" yAccessor="impact" xCenter={5} yCenter={5} quadrants={{ topLeft: { label: "Quick wins", color: "#22c55e" }, topRight: { label: "Strategic bets", color: "#3b82f6" }, bottomLeft: { label: "Fill-in", color: "#94a3b8" }, bottomRight: { label: "Money pits", color: "#ef4444" }, }} />Streaming / push mode
Quadrant charts are usually authored. Someone sat down, scored each item, dropped it on the grid. But streaming becomes interesting when the scores themselves come from live signals: ticket effort tracked from an issue tracker's labels, impact tracked from an analytics rollup the moment a feature ships. The grid becomes a live dashboard, not a static slide.
The demo below pushes items into the chart one at a time, like backlog tickets arriving from a planning session.
Wiring:
const ref = useRef() ref.current.push({ id: "Search typeahead", effort: 2, impact: 9 }) // later, scores update ref.current.update("Search typeahead", (d) => ({ ...d, impact: 8 })) <QuadrantChart ref={ref} xAccessor="effort" yAccessor="impact" pointIdAccessor="id" // required for update() / remove() xCenter={5.5} yCenter={5.5} quadrants={...} />Why push mode helps: update(id, fn) mutates a single point in place without re-keying the rest. With data={...} on each tick, the whole array re-renders and the chart's hover state, animations, and any in-flight tooltip get reset. Push mode keeps the interaction context.