VennChart

Overlapping sets as area-proportional circles, for how much two or three audiences, tags or cohorts share. Exact at three sets and no further — four circles can't draw all fifteen of their regions, so a `BarChart` of the combinations is the honest chart past that.

vue
<UiVennChart :data="..." :setsKey="..." :valueKey="..." :ariaLabel="..." />

Needs the charts module

This component ships in @jgastager/bundy-ui/charts, a separate module so projects that never draw a chart don't carry the paint classes these need. Nothing extra to install — just add the module entry. See Charts.


Options

One block per prop — what it does, and what it looks like set.

Data

required

data is one row per combination: which sets it's about, and how many things are in it. A single-set row is that set's whole total, overlaps included — so Email is all 4,200 people, 1,100 of whom also get push.

Areas are the encoding, not decoration: each circle is sized from its total and each pair is moved until the lens between them measures the overlap exactly. That holds for two and three sets. Four circles cannot draw all fifteen of their regions at any sizes, so past three the layout is the closest least-squares compromise it can find and several overlaps it shows are wrong by construction — rank the combinations as a horizontal BarChart instead.

A single-set row is that set's whole total, overlaps included — the venn.js convention. exclusive flips it to one disjoint count per row. Either way, the number printed in a region is the value you gave for that combination, so under the default reading an A ∩ B of 1,100 includes whatever also falls in C, even though it is printed in the crescent that excludes C.

Rows naming a set that has no total of its own still work: the largest row mentioning it becomes its total, which is the smallest size consistent with the data. A set another one completely swallows loses its name, though — it has no crescent to write it in, so turn the legend on for that case.

Sets key

required

setsKey sets which field lists a row's sets. An array for a combination, or a bare name for a single set.

Value key

required

valueKey sets which field holds the number. Every circle's area is that number, which is what makes the picture readable as a measurement rather than a diagram.

ARIA label

required

ariaLabel is required, and the place to put the finding. A Venn is read for one thing — how much of one set the other takes — and the geometry never says it out loud.

ARIA description

ariaDescription is the detail the label shouldn't carry.

2,760 accounts are on trial only, 1,460 are paid without a trial before it, and 640 are both.

Exclusive

exclusive switches how a row's number is read. Both charts below are the same data: on the left each row is a set's total, on the right each row is the count in only that region — the shape a GROUP BY over set membership returns.

Colors

colors pins a paint per set, matched to the order the sets first appear in data.

Fill opacity

fillOpacity is how solid each circle is. The overlaps are those fills stacking up, so it's also how strongly a shared region reads — past about 0.5 three circles blend into one dark mass.

The overlaps are the translucent fills stacking up, not paths of their own. That is why there is no per-region color: a region is bounded by however many arcs the circles happen to give it, and the blend already paints it correctly.

Stroke width

strokeWidth is the outline, which is what keeps each set's own edge findable through the fills. Zero drops it and leaves the blend to do all the work.

Labels

labels writes each set's name inside the part of it no other set reaches. Turn it off when the names are longer than the regions have room for and let the legend carry them instead.

  • Weekly digest readers
  • In-app announcement viewers
  • Release notes subscribers

Every label is dropped rather than drawn when it is wider than the region it sits in — a number that crosses an arc reads as the neighbouring region's number. So a long valueFormat, a small height, or a nearly-covered region will quietly thin the labels out. The tooltip always has the full figures.

Values

values writes each row's number in its own region. Off leaves the areas to speak for themselves, with the exact figures in the tooltip.

Value format

valueFormat formats every number the chart shows. A label wider than its own region is dropped rather than drawn across an arc, so a long format quietly thins out the small regions.

Tooltip

tooltip shows the panel on hover: the focused sets with their totals, and the size of the overlap itself set off below them.

Animate

animate is on by default: the circles grow out of their centres on the first draw and ease to their new sizes and positions. Roll new values to compare it with the chart on the right, which has it off. Only the marks move — axes, tooltips and labels always read the real numbers — and a reader who has asked their system for reduced motion gets no motion either way.

Legend

legend is off by default, because labels already names each circle in place — which needs no matching of swatch to shape. It's the fallback when the names don't fit.

  • Email
  • Push
  • SMS

Legend position

legendPosition sets where the legend sits relative to the plot.

  • Email
  • Push
  • SMS

Legend align

legendAlign places the key horizontally. Centre reads well under a diagram that's itself centred in its box.

  • Trial
  • Paid
  • Trial
  • Paid
  • Trial
  • Paid

Legend orientation

legendOrientation flows the entries in a row or a column.

  • Email · 4,200 people
  • Push · 2,600 people
  • SMS · 1,800 people

Legend group

legendGroup names a group that a standalone UiChartLegend with the same group belongs to, so the legend can sit anywhere — here above the plot — and still dim the other circles while you hover an entry. The chart hands the legend its entries, colors included, so it needs no items. Switch the chart's own legend off if you don't want two.

Height

height is the plot's height in pixels; the diagram is scaled to fit inside it. It's ignored once aspectRatio is set.

Aspect ratio

aspectRatio sizes the chart from its container's width instead of a fixed height.

Loading

loading is a skeleton at the chart's exact height; an empty data array gets the empty state instead.

No data to display

Examples

The component in real use: compositions that answer "how do I build X with this", rather than one prop at a time.

Sets that share nothing

Two sets with no row between them are drawn just apart rather than made to touch. That's the honest picture, and it's worth showing: “these two audiences don't overlap at all” is a finding, not an empty chart.

Past three sets, use a bar chart

Three circles is the limit of the shape, not of the component. Four can't draw all fifteen of their regions at any sizes, so the layout below is the closest compromise it can find — several of the overlaps it shows are wrong by construction. Rank the combinations as bars instead.


API Reference

Generated from the component's source — props, slots and emits as the component actually declares them.

Props

Prop
Type
Default
Description
datarequired
Record<string, unknown>[]
—
One row per combination of sets: which sets it's about, and how many things are in it. ```ts [ { sets: ["Email"], size: 4200 }, { sets: ["Push"], size: 2600 }, { sets: ["Email", "Push"], size: 1100 }, ] ``` A single-set row is that set's whole total, overlaps included — `Email` is 4,200 people, 1,100 of whom also get push. `exclusive` switches that reading if your rows are counts of one region each instead. Areas are the encoding: every circle is sized from its total and every pair is moved until the lens between them measures the overlap. That's exact for two and three sets, which is as far as circles go — four circles can't draw all fifteen of their regions at any sizes, so past three the layout is the closest compromise it can find and an `UpSet`-style bar of the combinations is the honest chart.
setsKeyrequired
string
—
Field holding each row's set names — an array, or a bare name for a single-set row.
valueKeyrequired
string
—
Field holding each row's numeric size.
ariaLabelrequired
string
—
What the chart shows, for screen readers. Required, and the place to put the finding: a proportional Venn is read for one thing, how much of one set the other one takes, and no amount of geometry says that out loud.
string
—
A longer accessible description, for detail the label shouldn't carry.
boolean
false
Reads each row as the count in *only* that combination, rather than as a whole set's total. Off, `{ sets: ["Email"], size: 4200 }` means all 4,200 with email, and the `Email ∩ Push` row is part of that — how a sentence like "4,200 on email, 1,100 of them on push too" is phrased. On, the rows are a partition: 4,200 would be the people on email *and nothing else*, and each set's total is summed up from the rows that mention it. That's the shape a `GROUP BY` over set membership hands you, and the reading where every number printed on the chart is the count of exactly the region it sits in.
ChartColorName[]
—
Per-set colors, matched to the order the sets first appear in `data`.
number
0.35
How solid each circle's fill is. The overlaps are those fills stacking, so this is also how strongly a shared region reads — past about 0.5 three circles blend into a single dark mass.
number
1.5
Outline width in pixels. It's what keeps each set's own edge findable through the fills.
boolean
true
Draws each set's name inside the part of it no other set reaches.
boolean
true
Draws each row's number in the region it belongs to.
(value: number) => string
—
Formats the numbers on the chart, in the tooltip and in the legend.
boolean
true
Shows the tooltip on hover.
boolean
true
Grows the circles out of their centres on the first draw, and eases them to their new sizes and positions whenever the data changes. The labels and tooltip always read the real numbers. Readers who ask their system for reduced motion get no animation either way.
boolean
false
Shows the legend. Off by default, because `labels` already names each circle in place — which is the better read, since it needs no matching of swatch to shape. Turn it on (with `labels` off) when the names are longer than the regions have room for.
"top" | "bottom"
'bottom'
Where the legend sits relative to the plot.
"start" | "center" | "end"
'center'
Horizontal placement of the legend.
"horizontal" | "vertical"
'horizontal'
How the legend entries flow.
string
—
Names the group a `<UiChartLegend group="…">` elsewhere on the page belongs to, so hovering its entries dims this chart's other marks. Set it — usually with `:legend="false"` — to place the legend anywhere you like instead of under the plot.
number
320
Height of the plot in pixels. Ignored when `aspectRatio` is set.
number
—
Width-to-height ratio, sizing the chart from its container's width.
boolean
false
Replaces the plot with a skeleton of the same height.

Emits

Event
Payload
Description
focusChange
[members: string[] | null]
The region under the pointer as its set names, or `null` once the pointer leaves every circle.

Types

ts
interface ChartPlot {
    x: number;
    y: number;
    width: number;
    height: number;
    /** Right edge, since marks need it more often than the width. */
    right: number;
    /** Bottom edge, which is also the x axis' baseline. */
    bottom: number;
}
ts
type ChartColorName = ChartColorFamily | `${ChartColorFamily}-${ChartColorShade}` | `[${string}]`;