Skip to content

Venn Plot

Venn and Euler diagrams from pre-aggregated subset sizes.

Only 2- or 3-set diagrams are supported; input is subset sizes (no aggregation is performed). vary_size=True switches from equal circles to area-accurate regions.

plot(df, labels, title=None, eyebrow=None, subtitle=None, source_text=None, vary_size=False, figsize=None, ax=None, subset_label_formatter=None, **kwargs)

Plot a Venn or Euler diagram from the subset sizes in a DataFrame.

df needs groups, a tuple (not a list) of 0/1 flags where flag i means membership of labels[i], and percent, the subset size, shown as given. The all-zero subset is dropped.

Parameters:

Name Type Description Default
df DataFrame

Frame with groups and percent columns.

required
labels list[str]

Labels for the sets (2 or 3).

required
title str

Plot title.

None
eyebrow str

Uppercase label rendered above the title.

None
subtitle str

Supporting copy rendered below the title.

None
source_text str

Source attribution rendered at the bottom.

None
vary_size bool

False draws equal circles; True sizes each region by its subset size.

False
figsize tuple[int, int]

Figure size, used only when ax is None.

None
ax Axes

Axes to plot on.

None
subset_label_formatter callable

Applied to each subset size; if None, the raw value is stringified.

None
**kwargs Any

Forwarded to matplotlib_set_diagrams' VennDiagram, or EulerDiagram when vary_size is True.

{}

Returns:

Name Type Description
SubplotBase SubplotBase

The matplotlib axes object.

Raises:

Type Description
ValueError

If the number of sets (len(labels)) is not 2 or 3.

Source code in openretailscience/plots/venn.py
def plot(
    df: pd.DataFrame,
    labels: list[str],
    title: str | None = None,
    eyebrow: str | None = None,
    subtitle: str | None = None,
    source_text: str | None = None,
    vary_size: bool = False,
    figsize: tuple[int, int] | None = None,
    ax: Axes | None = None,
    subset_label_formatter: Callable | None = None,
    **kwargs: Any,  # noqa: ANN401
) -> SubplotBase:
    """Plot a Venn or Euler diagram from the subset sizes in a DataFrame.

    df needs `groups`, a tuple (not a list) of 0/1 flags where flag i means membership of
    `labels[i]`, and `percent`, the subset size, shown as given. The all-zero subset is
    dropped.

    Args:
        df (pd.DataFrame): Frame with `groups` and `percent` columns.
        labels (list[str]): Labels for the sets (2 or 3).
        title (str, optional): Plot title.
        eyebrow (str, optional): Uppercase label rendered above the title.
        subtitle (str, optional): Supporting copy rendered below the title.
        source_text (str, optional): Source attribution rendered at the bottom.
        vary_size (bool, optional): False draws equal circles; True sizes each region by its
            subset size.
        figsize (tuple[int, int], optional): Figure size, used only when ax is None.
        ax (Axes, optional): Axes to plot on.
        subset_label_formatter (callable, optional): Applied to each subset size; if None,
            the raw value is stringified.
        **kwargs: Forwarded to matplotlib_set_diagrams' ``VennDiagram``, or ``EulerDiagram`` when
            ``vary_size`` is True.

    Returns:
        SubplotBase: The matplotlib axes object.

    Raises:
        ValueError: If the number of sets (len(labels)) is not 2 or 3.

    """
    num_sets = len(labels)
    if num_sets not in {MIN_SUPPORTED_SETS, MAX_SUPPORTED_SETS}:
        raise ValueError("Only 2-set or 3-set Venn diagrams are supported")

    default_colors = get_plot_colors(num_sets)
    colors = kwargs.pop("color", default_colors)

    zero_group = (0, 0) if num_sets == MIN_SUPPORTED_SETS else (0, 0, 0)
    percent_s = df.loc[df["groups"] != zero_group, ["groups", "percent"]].set_index("groups")["percent"]
    subset_sizes = percent_s.to_dict()

    subset_labels = {
        k: subset_label_formatter(v) if subset_label_formatter else str(v) for k, v in subset_sizes.items()
    }

    if ax is None:
        _, ax = plt.subplots(figsize=figsize)

    diagram_class = EulerDiagram if vary_size else VennDiagram
    diagram = diagram_class(
        set_labels=labels,
        subset_sizes=subset_sizes,
        subset_labels=subset_labels,
        set_colors=colors,
        ax=ax,
        **kwargs,
    )

    for patch in ax.patches:
        patch.set_alpha(_VENN_FILL_ALPHA)

    center_x, center_y, displacement = 0.5, 0.5, 0.1
    style = PlotStyleHelper()
    for text in diagram.set_label_artists:
        text.set_fontproperties(get_font_properties(style.legend_font))
        text.set_fontsize(style.legend_size)
        if num_sets == MAX_SUPPORTED_SETS and not vary_size:
            x, y = text.get_position()
            direction_x, direction_y = x - center_x, y - center_y
            scale = displacement / (direction_x**2 + direction_y**2) ** 0.5
            text.set_position((x + scale * direction_x, y + scale * direction_y))

    for subset_id in subset_sizes:
        if subset_id not in diagram.subset_label_artists:
            continue
        text = diagram.subset_label_artists[subset_id]
        text.set_fontproperties(get_font_properties(style.data_label_font))
        text.set_fontsize(style.data_label_size)

    ax.set_xticklabels([], visible=False)
    ax.set_yticklabels([], visible=False)

    # The library leaves generous padding in the data limits; shrink the window
    # to the real artist extents so the diagram fills the chart area.
    _tighten_to_artists(ax)

    apply_chart_chrome(
        ax,
        eyebrow=eyebrow,
        title=title,
        subtitle=subtitle,
        source_text=source_text,
        warn_stacklevel=3,
    )

    return ax