diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9db9772..134c1f9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,10 @@ What's new in version 0.2
 -------------------------
 
 - Added `from_memberships` to allow a more convenient data input format.
+- `plot` and `UpSet` now accept a `pandas.DataFrame` as input, if the
+  `sum_over` parameter is also given.
+- Added an `add_catplot` method to `UpSet` which adds Seaborn plots of set
+  intersection data to show more than just set size or total.
 - Shading of subset matrix is continued through to totals.
 - Added a `show_counts` option to show counts at the ends of bar plots.
   (:issue:`5`)
diff --git a/README.rst b/README.rst
index 742569f..843f2ef 100644
--- a/README.rst
+++ b/README.rst
@@ -49,11 +49,28 @@ items only in ``set1``, ``set2`` and ``set3``` respectively, with following
 columns showing cardinalities for items in each combination of exactly two
 named sets. The rightmost column counts items in all three sets.
 
+Rotation
+........
+
 We call the above plot style "horizontal" because the set intersections are
-presented from left to right.  Vertical plots are also supported!
+presented from left to right.  `Vertical plots
+<http://upsetplot.readthedocs.io/en/latest/auto_examples/plot_vertical.html>`__
+are also supported!
 
 .. image:: http://upsetplot.readthedocs.io/en/latest/_images/sphx_glr_plot_vertical_001.png
-   :target: ../auto_examples/plot_vertical.html
+   :target: http://upsetplot.readthedocs.io/en/latest/auto_examples/plot_vertical.html
+
+Distributions
+.............
+
+Providing a DataFrame rather than a Series as input allows us to expressively
+`plot the distribution of variables
+<http://upsetplot.readthedocs.io/en/latest/auto_examples/plot_boston.html>`__
+in each subset.
+
+.. image:: http://upsetplot.readthedocs.io/en/latest/_images/sphx_glr_plot_boston_001.png
+   :target: http://upsetplot.readthedocs.io/en/latest/auto_examples/plot_boston.html
+
 
 Loading datasets
 ................
@@ -99,6 +116,7 @@ Installation requires:
 
 * pandas
 * matplotlib >= 2.0
+* seaborn to use `UpSet.add_catplot`
 
 It should then be possible to::
 
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 769acc6..7660b8f 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -5,3 +5,4 @@ matplotlib
 numpydoc
 sphinx-gallery
 sphinx-issues
+seaborn
diff --git a/examples/plot_boston.py b/examples/plot_boston.py
new file mode 100644
index 0000000..8bd4c8b
--- /dev/null
+++ b/examples/plot_boston.py
@@ -0,0 +1,61 @@
+"""
+================================
+Above-average features in Boston
+================================
+
+Explore above-average neighborhood characteristics in the Boston dataset.
+
+Here we take some features correlated with house price, and look at the
+distribution of median house price when each of these features is above
+average.
+
+The most correlated features are:
+
+ZN
+    proportion of residential land zoned for lots over 25,000 sq.ft.
+CHAS
+    Charles River dummy variable (= 1 if tract bounds river; 0 otherwise)
+RM
+    average number of rooms per dwelling
+DIS
+    weighted distances to five Boston employment centres
+B
+    1000(Bk - 0.63)^2 where Bk is the proportion of blacks by town
+
+This kind of dataset analysis may not be a practical use of UpSet, but helps
+to illustrate the :meth:`UpSet.add_catplot` feature.
+"""
+
+import pandas as pd
+from sklearn.datasets import load_boston
+from matplotlib import pyplot as plt
+from upsetplot import UpSet
+
+# Load the dataset into a DataFrame
+boston = load_boston()
+boston_df = pd.DataFrame(boston.data, columns=boston.feature_names)
+
+# Get five features most correlated with median house value
+correls = boston_df.corrwith(pd.Series(boston.target),
+                             method='spearman').sort_values()
+top_features = correls.index[-5:]
+
+# Get a binary indicator of whether each top feature is above average
+boston_above_avg = boston_df > boston_df.median(axis=0)
+boston_above_avg = boston_above_avg[top_features]
+boston_above_avg = boston_above_avg.rename(columns=lambda x: x + '>')
+
+# Make this indicator mask an index of boston_df
+boston_df = pd.concat([boston_df, boston_above_avg],
+                      axis=1)
+boston_df = boston_df.set_index(list(boston_above_avg.columns))
+
+# Also give us access to the target (median house value)
+boston_df = boston_df.assign(median_value=boston.target)
+
+# UpSet plot it!
+upset = UpSet(boston_df, sum_over=False, intersection_plot_elements=3)
+upset.add_catplot(value='median_value', kind='strip', color='blue')
+upset.add_catplot(value='AGE', kind='strip', color='black')
+upset.plot()
+plt.show()
diff --git a/upsetplot/plotting.py b/upsetplot/plotting.py
index 14e06ef..c94f4ea 100644
--- a/upsetplot/plotting.py
+++ b/upsetplot/plotting.py
@@ -9,17 +9,34 @@ from matplotlib import pyplot as plt
 from matplotlib.tight_layout import get_renderer
 
 
-def _process_data(data, sort_by, sort_sets_by):
-    # check all indices are vertical
+def _process_data(df, sort_by, sort_sets_by, sum_over):
+    if df.ndim == 1:
+        data = df
+        df = pd.DataFrame({'_value': df})
+
+        if not data.index.is_unique:
+            data = (data
+                    .groupby(level=list(range(data.index.nlevels)))
+                    .sum())
+        if sum_over is not None:
+            raise ValueError('sum_over is not applicable when the input is a '
+                             'Series')
+    elif sum_over is None:
+        raise ValueError('sum_over must be False or a column name when a '
+                         'DataFrame is input')
+    else:
+        gb = df.groupby(level=list(range(df.index.nlevels)))
+        if sum_over is False:
+            data = gb.size()
+            data.name = 'size'
+        elif hasattr(sum_over, 'lower'):
+            data = gb[sum_over].sum()
+        else:
+            raise ValueError('Unsupported value for sum_over: %r' % sum_over)
+
+    # check all indices are boolean
     assert all(set([True, False]) >= set(level) for level in data.index.levels)
-    if not data.index.is_unique:
-        data = (data
-                .groupby(level=list(range(data.index.nlevels)))
-                .sum())
-
-    if data.ndim != 1:
-        raise ValueError('data must be a pandas.Series')
-    # XXX: .values.astype(bool) not required for recent Pandas
+
     totals = [data[data.index.get_level_values(name).values.astype(bool)].sum()
               for name in data.index.names]
     totals = pd.Series(totals, index=data.index.names)
@@ -27,6 +44,7 @@ def _process_data(data, sort_by, sort_sets_by):
         totals.sort_values(ascending=False, inplace=True)
     elif sort_sets_by is not None:
         raise ValueError('Unknown sort_sets_by: %r' % sort_sets_by)
+    df = df.reorder_levels(totals.index.values)
     data = data.reorder_levels(totals.index.values)
 
     if sort_by == 'cardinality':
@@ -48,7 +66,23 @@ def _process_data(data, sort_by, sort_sets_by):
     max_value = np.inf
     data = data[np.logical_and(data >= min_value, data <= max_value)]
 
-    return data, totals
+    # add '_bin' to df indicating index in data
+    # XXX: ugly!
+    def _pack_binary(X):
+        X = pd.DataFrame(X)
+        out = 0
+        for i, (_, col) in enumerate(X.items()):
+            out *= 2
+            out += col
+        return out
+
+    df_packed = _pack_binary(df.index.to_frame())
+    data_packed = _pack_binary(data.index.to_frame())
+    df['_bin'] = pd.Series(df_packed).map(
+        pd.Series(np.arange(len(data_packed)),
+                  index=data_packed))
+
+    return df, data, totals
 
 
 class _Transposed:
@@ -92,6 +126,8 @@ class _Transposed:
         'get_figheight': 'get_figwidth',
         'set_figwidth': 'set_figheight',
         'set_figheight': 'set_figwidth',
+        'set_xlabel': 'set_ylabel',
+        'set_ylabel': 'set_xlabel',
     }
 
 
@@ -112,10 +148,11 @@ class UpSet:
 
     Parameters
     ----------
-    data : pandas.Series
+    data : pandas.Series or pandas.DataFrame
         Values for each set to plot.
         Should have multi-index where each level is binary,
         corresponding to set membership.
+        If a DataFrame, `sum_over` must be a string or False.
     orientation : {'horizontal' (default), 'vertical'}
         If horizontal, intersections are listed from left to right.
     sort_by : {'cardinality', 'degree'}
@@ -126,6 +163,10 @@ class UpSet:
     sort_sets_by : {'cardinality', None}
         Whether to sort the overall sets by total cardinality, or leave them
         in the provided order.
+    sum_over : str, False or None (default)
+        Must be specified when `data` is a DataFrame. If False, the
+        intersection plot will show the count of each subset. Otherwise, it
+        shows the sum of the specified field.
     facecolor : str
         Color for bar charts and dots.
     with_lines : bool
@@ -147,7 +188,7 @@ class UpSet:
     _default_figsize = (10, 6)
 
     def __init__(self, data, orientation='horizontal', sort_by='degree',
-                 sort_sets_by='cardinality', facecolor='black',
+                 sort_sets_by='cardinality', sum_over=None, facecolor='black',
                  with_lines=True, element_size=32,
                  intersection_plot_elements=6, totals_plot_elements=2,
                  show_counts=''):
@@ -158,13 +199,16 @@ class UpSet:
         self._with_lines = with_lines
         self._element_size = element_size
         self._totals_plot_elements = totals_plot_elements
-        self._intersection_plot_elements = intersection_plot_elements
+        self._subset_plots = [{'type': 'default',
+                               'id': 'intersections',
+                               'elements': intersection_plot_elements}]
         self._show_counts = show_counts
 
-        (self.intersections,
+        (self._df, self.intersections,
          self.totals) = _process_data(data,
                                       sort_by=sort_by,
-                                      sort_sets_by=sort_sets_by)
+                                      sort_sets_by=sort_sets_by,
+                                      sum_over=sum_over)
         if not self._horizontal:
             self.intersections = self.intersections[::-1]
 
@@ -173,6 +217,73 @@ class UpSet:
             return x, y
         return y, x
 
+    def add_catplot(self, kind, value=None, elements=3, **kw):
+        """Add a seaborn catplot over subsets when :func:`plot` is called.
+
+        Parameters
+        ----------
+        kind : str
+            One of {"point", "bar", "strip", "swarm", "box", "violin", "boxen"}
+        value : str, optional
+            Column name for the value to plot (i.e. y if
+            orientation='horizontal'), required if `data` is a DataFrame.
+        elements : int, default=3
+            Size of the axes counted in number of matrix elements.
+        **kw : dict
+            Additional keywords to pass to :func:`seaborn.catplot`.
+
+            Our implementation automatically determines 'ax', 'data', 'x', 'y'
+            and 'orient', so these are prohibited keys in `kw`.
+
+        Returns
+        -------
+        None
+        """
+        assert not set(kw.keys()) & {'ax', 'data', 'x', 'y', 'orient'}
+        if value is None:
+            if '_value' not in self._df.columns:
+                raise ValueError('value cannot be set if data is a Series. '
+                                 'Got %r' % value)
+        else:
+            if value not in self._df.columns:
+                raise ValueError('value %r is not a column in data' % value)
+        self._subset_plots.append({'type': 'catplot',
+                                   'value': value,
+                                   'kind': kind,
+                                   'id': 'extra%d' % len(self._subset_plots),
+                                   'elements': elements,
+                                   'kw': kw})
+
+    def _plot_catplot(self, ax, value, kind, kw):
+        df = self._df
+        if value is None and '_value' in df.columns:
+            value = '_value'
+        elif value is None:
+            raise ValueError('value can only be None when data is a Series')
+        kw = kw.copy()
+        if self._horizontal:
+            kw['orient'] = 'v'
+            kw['x'] = '_bin'
+            kw['y'] = value
+        else:
+            kw['orient'] = 'h'
+            kw['x'] = value
+            kw['y'] = '_bin'
+        import seaborn
+        kw['ax'] = ax
+        getattr(seaborn, kind + 'plot')(data=df, **kw)
+
+        ax = self._reorient(ax)
+        if value == '_value':
+            ax.set_ylabel('')
+
+        ax.xaxis.set_visible(False)
+        for x in ['top', 'bottom', 'right']:
+            ax.spines[self._reorient(x)].set_visible(False)
+
+        tick_axis = ax.yaxis
+        tick_axis.grid(True)
+
     def make_grid(self, fig=None):
         """Get a SubplotSpec for each Axes, accounting for label text width
         """
@@ -190,6 +301,9 @@ class UpSet:
 
         MAGIC_MARGIN = 10  # FIXME
         figw = self._reorient(fig.get_window_extent(renderer=r)).width
+
+        sizes = np.asarray([p['elements'] for p in self._subset_plots])
+
         if self._element_size is None:
             colw = (figw - textw - MAGIC_MARGIN) / (len(self.intersections) +
                                                     self._totals_plot_elements)
@@ -201,31 +315,37 @@ class UpSet:
                             self._totals_plot_elements) +
                     MAGIC_MARGIN + textw)
             fig.set_figwidth(figw / render_ratio)
-            fig.set_figheight((colw * (n_cats +
-                                       self._intersection_plot_elements)) /
+            fig.set_figheight((colw * (n_cats + sizes.sum())) /
                               render_ratio)
 
         text_nelems = int(np.ceil(figw / colw - (len(self.intersections) +
                                                  self._totals_plot_elements)))
 
         GS = self._reorient(matplotlib.gridspec.GridSpec)
-        gridspec = GS(*self._swapaxes(n_cats +
-                                      self._intersection_plot_elements,
+        gridspec = GS(*self._swapaxes(n_cats + sizes.sum(),
                                       n_inters + text_nelems +
                                       self._totals_plot_elements),
                       hspace=1)
         if self._horizontal:
-            return {'intersections': gridspec[:-n_cats, -n_inters:],
-                    'matrix': gridspec[-n_cats:, -n_inters:],
-                    'shading': gridspec[-n_cats:, :],
-                    'totals': gridspec[-n_cats:, :self._totals_plot_elements],
-                    'gs': gridspec}
+            out = {'matrix': gridspec[-n_cats:, -n_inters:],
+                   'shading': gridspec[-n_cats:, :],
+                   'totals': gridspec[-n_cats:, :self._totals_plot_elements],
+                   'gs': gridspec}
+            cumsizes = np.cumsum(sizes[::-1])
+            for start, stop, plot in zip(np.hstack([[0], cumsizes]), cumsizes,
+                                         self._subset_plots[::-1]):
+                out[plot['id']] = gridspec[start:stop, -n_inters:]
         else:
-            return {'intersections': gridspec[-n_inters:, n_cats:],
-                    'matrix': gridspec[-n_inters:, :n_cats],
-                    'shading': gridspec[:, :n_cats],
-                    'totals': gridspec[:self._totals_plot_elements, :n_cats],
-                    'gs': gridspec}
+            out = {'matrix': gridspec[-n_inters:, :n_cats],
+                   'shading': gridspec[:, :n_cats],
+                   'totals': gridspec[:self._totals_plot_elements, :n_cats],
+                   'gs': gridspec}
+            cumsizes = np.cumsum(sizes)
+            for start, stop, plot in zip(np.hstack([[0], cumsizes]), cumsizes,
+                                         self._subset_plots):
+                out[plot['id']] = gridspec[-n_inters:,
+                                           start + n_cats:stop + n_cats]
+        return out
 
     def plot_matrix(self, ax):
         """Plot the matrix of intersection indicators onto ax
@@ -279,6 +399,7 @@ class UpSet:
 
         tick_axis = ax.yaxis
         tick_axis.grid(True)
+        # FIXME: doesn't seem to display
         tick_axis.set_label('Intersection size')
         # tick_axis.set_tick_params(direction='in')
 
@@ -373,16 +494,24 @@ class UpSet:
         matrix_ax = self._reorient(fig.add_subplot)(specs['matrix'],
                                                     sharey=shading_ax)
         self.plot_matrix(matrix_ax)
-        inters_ax = self._reorient(fig.add_subplot)(specs['intersections'],
-                                                    sharex=matrix_ax)
-        self.plot_intersections(inters_ax)
         totals_ax = self._reorient(fig.add_subplot)(specs['totals'],
                                                     sharey=matrix_ax)
         self.plot_totals(totals_ax)
-        return {'matrix': matrix_ax,
-                'intersections': inters_ax,
-                'shading': shading_ax,
-                'totals': totals_ax}
+        out = {'matrix': matrix_ax,
+               'shading': shading_ax,
+               'totals': totals_ax}
+
+        for plot in self._subset_plots:
+            ax = self._reorient(fig.add_subplot)(specs[plot['id']],
+                                                 sharex=matrix_ax)
+            if plot['type'] == 'default':
+                self.plot_intersections(ax)
+            elif plot['type'] == 'catplot':
+                self._plot_catplot(ax, plot['value'], plot['kind'], plot['kw'])
+            else:
+                raise ValueError('Unknown subset plot type: %r' % plot['type'])
+            out[plot['id']] = ax
+        return out
 
     def _repr_html_(self):
         fig = plt.figure(figsize=self._default_figsize)
@@ -395,10 +524,11 @@ def plot(data, fig=None, **kwargs):
 
     Parameters
     ----------
-    data : pandas.Series
+    data : pandas.Series or pandas.DataFrame
         Values for each set to plot.
         Should have multi-index where each level is binary,
         corresponding to set membership.
+        If a DataFrame, `sum_over` must be a string or False.
     fig : matplotlib.figure.Figure, optional
         Defaults to a new figure.
     kwargs

Distribution plots for intersections as well as bar chart
User would pass in a DataFrame or Series (indexed by boolean indicators, or with these available as data columns for groupby), and UpSet would store a `groupby` attribute. User could `add_plot(size, func)` or `add_plot(size, pandas_kind, field)` (field not required for Series) and these would be plotted above the intersections size plot. Also allow disabling the intersections plot with `intersections_plot_size=0`.
diff --git a/upsetplot/tests/test_upsetplot.py b/upsetplot/tests/test_upsetplot.py
index 57bb8b1..14fe6ca 100644
--- a/upsetplot/tests/test_upsetplot.py
+++ b/upsetplot/tests/test_upsetplot.py
@@ -2,7 +2,9 @@ import io
 import itertools
 
 import pytest
-from pandas.util.testing import assert_series_equal
+from pandas.util.testing import (
+    assert_series_equal, assert_frame_equal, assert_index_equal)
+from numpy.testing import assert_array_equal
 import pandas as pd
 import numpy as np
 import matplotlib.figure
@@ -25,10 +27,16 @@ def is_ascending(seq):
 ])
 @pytest.mark.parametrize('sort_by', ['cardinality', 'degree'])
 @pytest.mark.parametrize('sort_sets_by', [None, 'cardinality'])
-def test_process_data(X, sort_by, sort_sets_by):
-    intersections, totals = _process_data(X,
-                                          sort_by=sort_by,
-                                          sort_sets_by=sort_sets_by)
+def test_process_data_series(X, sort_by, sort_sets_by):
+    with pytest.raises(ValueError, match='sum_over is not applicable'):
+        _process_data(X, sort_by=sort_by, sort_sets_by=sort_sets_by,
+                      sum_over=False)
+
+    df, intersections, totals = _process_data(X,
+                                              sort_by=sort_by,
+                                              sort_sets_by=sort_sets_by,
+                                              sum_over=None)
+    assert intersections.name == 'value'
     X_reordered = (X
                    .reorder_levels(intersections.index.names)
                    .reindex(index=intersections.index))
@@ -49,20 +57,103 @@ def test_process_data(X, sort_by, sort_sets_by):
 
     assert np.all(totals.index.values == intersections.index.names)
 
+    assert np.all(df.index.names == intersections.index.names)
+    assert set(df.columns) == {'_value', '_bin'}
+    assert_index_equal(df['_value'].reorder_levels(X.index.names).index,
+                       X.index)
+    assert_array_equal(df['_value'], X)
+    assert_index_equal(intersections.iloc[df['_bin']].index,
+                       df.index)
+    assert len(df) == len(X)
+
+
+@pytest.mark.parametrize('x', [
+    generate_data(aggregated=False),
+])
+@pytest.mark.parametrize('sort_by', ['cardinality', 'degree'])
+@pytest.mark.parametrize('sort_sets_by', [None, 'cardinality'])
+def test_process_data_frame(x, sort_by, sort_sets_by):
+    X = pd.DataFrame({'a': x})
+
+    with pytest.raises(ValueError, match='sum_over must be False or '):
+        _process_data(X, sort_by=sort_by, sort_sets_by=sort_sets_by,
+                      sum_over=None)
+
+    df, intersections, totals = _process_data(X,
+                                              sort_by=sort_by,
+                                              sort_sets_by=sort_sets_by,
+                                              sum_over='a')
+    assert df is not X
+
+    # check equivalence to Series
+    df1, intersections1, totals1 = _process_data(x,
+                                                 sort_by=sort_by,
+                                                 sort_sets_by=sort_sets_by,
+                                                 sum_over=None)
+
+    assert intersections.name == 'a'
+    assert_frame_equal(df, df1.rename(columns={'_value': 'a'}))
+    assert_series_equal(intersections, intersections1, check_names=False)
+    assert_series_equal(totals, totals1)
+
+    # check effect of extra column
+    X = pd.DataFrame({'a': x, 'b': np.arange(len(x))})
+    df2, intersections2, totals2 = _process_data(X,
+                                                 sort_by=sort_by,
+                                                 sort_sets_by=sort_sets_by,
+                                                 sum_over='a')
+    assert_series_equal(intersections, intersections2)
+    assert_series_equal(totals, totals2)
+    assert_frame_equal(df, df2.drop('b', axis=1))
+    assert_array_equal(df2['b'], X['b'])  # disregard levels, tested above
+
+    # check effect not dependent on order/name
+    X = pd.DataFrame({'b': np.arange(len(x)), 'c': x})
+    df3, intersections3, totals3 = _process_data(X,
+                                                 sort_by=sort_by,
+                                                 sort_sets_by=sort_sets_by,
+                                                 sum_over='c')
+    assert_series_equal(intersections, intersections3, check_names=False)
+    assert intersections.name == 'a'
+    assert intersections3.name == 'c'
+    assert_series_equal(totals, totals3)
+    assert_frame_equal(df.rename(columns={'a': 'c'}), df3.drop('b', axis=1))
+    assert_array_equal(df3['b'], X['b'])
+
+    # check sum_over=False
+    X = pd.DataFrame({'b': np.ones(len(x), dtype=int), 'c': x})
+    df4, intersections4, totals4 = _process_data(X,
+                                                 sort_by=sort_by,
+                                                 sort_sets_by=sort_sets_by,
+                                                 sum_over='b')
+    df5, intersections5, totals5 = _process_data(X,
+                                                 sort_by=sort_by,
+                                                 sort_sets_by=sort_sets_by,
+                                                 sum_over=False)
+    assert_series_equal(intersections4, intersections5, check_names=False)
+    assert intersections4.name == 'b'
+    assert intersections5.name == 'size'
+    assert_series_equal(totals4, totals5)
+    assert_frame_equal(df4, df5)
+
 
 @pytest.mark.parametrize('sort_by', ['cardinality', 'degree'])
 @pytest.mark.parametrize('sort_sets_by', [None, 'cardinality'])
 def test_not_aggregated(sort_by, sort_sets_by):
     # FIXME: this is not testing if aggregation used is count or sum
-    kw = {'sort_by': sort_by, 'sort_sets_by': sort_sets_by}
+    kw = {'sort_by': sort_by, 'sort_sets_by': sort_sets_by, 'sum_over': None}
     Xagg = generate_data(aggregated=True)
-    intersections1, totals1 = _process_data(Xagg, **kw)
+    df1, intersections1, totals1 = _process_data(Xagg, **kw)
     Xunagg = generate_data()
     Xunagg.loc[:] = 1
-    intersections2, totals2 = _process_data(Xunagg, **kw)
+    df2, intersections2, totals2 = _process_data(Xunagg, **kw)
     assert_series_equal(intersections1, intersections2,
                         check_dtype=False)
     assert_series_equal(totals1, totals2, check_dtype=False)
+    assert set(df1.columns) == {'_value', '_bin'}
+    assert set(df2.columns) == {'_value', '_bin'}
+    assert len(df2) == len(Xunagg)
+    assert df2['_bin'].nunique() == len(intersections2)
 
 
 @pytest.mark.parametrize('kw', [{'sort_by': 'blah'},
@@ -110,7 +201,7 @@ def test_dataframe_raises():
     df = pd.DataFrame({'val': [5, 7],
                        'set1': [False, True],
                        'set2': [True, True]}).set_index(['set1', 'set2'])
-    with pytest.raises(ValueError, match='must be a pandas.Series'):
+    with pytest.raises(ValueError, match='sum_over must be'):
         plot(df, fig)
 
 
@@ -190,3 +281,38 @@ def test_show_counts(orientation):
     with pytest.raises(ValueError):
         fig = matplotlib.figure.Figure()
         plot(X, fig, show_counts='%0.2h')
+
+
+def test_add_catplot():
+    pytest.importorskip('seaborn')
+    X = generate_data(n_samples=100)
+    upset = UpSet(X)
+    # smoke test
+    upset.add_catplot('violin')
+    fig = matplotlib.figure.Figure()
+    upset.plot(fig)
+
+    # can't provide value with Series
+    with pytest.raises(ValueError):
+        upset.add_catplot('violin', value='foo')
+
+    # check the above add_catplot did not break the state
+    upset.plot(fig)
+
+    X = generate_data(n_samples=100)
+    X.name = 'foo'
+    X = X.to_frame()
+    upset = UpSet(X, sum_over=False)
+    # must provide value with DataFrame
+    with pytest.raises(ValueError):
+        upset.add_catplot('violin')
+    upset.add_catplot('violin', value='foo')
+    with pytest.raises(ValueError):
+        # not a known column
+        upset.add_catplot('violin', value='bar')
+    upset.plot(fig)
+
+    # invalid plot kind raises error when plotting
+    upset.add_catplot('foobar', value='foo')
+    with pytest.raises(AttributeError):
+        upset.plot(fig)

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.6
doc/requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs==22.2.0
Babel==2.11.0
certifi==2021.5.30
charset-normalizer==2.0.12
cycler==0.11.0
docutils==0.18.1
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
Jinja2==3.0.3
kiwisolver==1.3.1
MarkupSafe==2.0.1
matplotlib==3.3.4
numpy==1.19.5
numpydoc==1.1.0
packaging==21.3
pandas==1.1.5
Pillow==8.4.0
pluggy==1.0.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.27.1
scipy==1.5.4
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-gallery==0.10.0
sphinx-issues==3.0.1
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli==1.2.3
typing_extensions==4.1.1
-e git+https://github.com/jnothman/UpSetPlot.git@39e3b4cfbf47175134776a07c9630734f1759a2e#egg=UpSetPlot
urllib3==1.26.20
zipp==3.6.0

name: UpSetPlot
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - attrs==22.2.0
      - babel==2.11.0
      - charset-normalizer==2.0.12
      - cycler==0.11.0
      - docutils==0.18.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - kiwisolver==1.3.1
      - markupsafe==2.0.1
      - matplotlib==3.3.4
      - numpy==1.19.5
      - numpydoc==1.1.0
      - packaging==21.3
      - pandas==1.1.5
      - pillow==8.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.27.1
      - scipy==1.5.4
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-gallery==0.10.0
      - sphinx-issues==3.0.1
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/UpSetPlot

upsetplot/tests/test_upsetplot.py::test_process_data_series[None-cardinality-X0] upsetplot/tests/test_upsetplot.py::test_process_data_series[None-cardinality-X1] upsetplot/tests/test_upsetplot.py::test_process_data_series[None-degree-X0] upsetplot/tests/test_upsetplot.py::test_process_data_series[None-degree-X1] upsetplot/tests/test_upsetplot.py::test_process_data_series[cardinality-cardinality-X0] upsetplot/tests/test_upsetplot.py::test_process_data_series[cardinality-cardinality-X1] upsetplot/tests/test_upsetplot.py::test_process_data_series[cardinality-degree-X0] upsetplot/tests/test_upsetplot.py::test_process_data_series[cardinality-degree-X1] upsetplot/tests/test_upsetplot.py::test_process_data_frame[None-cardinality-x0] upsetplot/tests/test_upsetplot.py::test_process_data_frame[None-degree-x0] upsetplot/tests/test_upsetplot.py::test_process_data_frame[cardinality-cardinality-x0] upsetplot/tests/test_upsetplot.py::test_process_data_frame[cardinality-degree-x0] upsetplot/tests/test_upsetplot.py::test_not_aggregated[None-cardinality] upsetplot/tests/test_upsetplot.py::test_not_aggregated[None-degree] upsetplot/tests/test_upsetplot.py::test_not_aggregated[cardinality-cardinality] upsetplot/tests/test_upsetplot.py::test_not_aggregated[cardinality-degree] upsetplot/tests/test_upsetplot.py::test_dataframe_raises
upsetplot/tests/test_upsetplot.py::test_param_validation[kw0] upsetplot/tests/test_upsetplot.py::test_param_validation[kw1] upsetplot/tests/test_upsetplot.py::test_param_validation[kw2] upsetplot/tests/test_upsetplot.py::test_param_validation[kw3] upsetplot/tests/test_upsetplot.py::test_param_validation[kw4] upsetplot/tests/test_upsetplot.py::test_plot_smoke_test[kw0] upsetplot/tests/test_upsetplot.py::test_plot_smoke_test[kw1] upsetplot/tests/test_upsetplot.py::test_plot_smoke_test[kw2] upsetplot/tests/test_upsetplot.py::test_two_sets[set20-set10] upsetplot/tests/test_upsetplot.py::test_two_sets[set20-set11] upsetplot/tests/test_upsetplot.py::test_two_sets[set20-set12] upsetplot/tests/test_upsetplot.py::test_two_sets[set20-set13] upsetplot/tests/test_upsetplot.py::test_two_sets[set21-set10] upsetplot/tests/test_upsetplot.py::test_two_sets[set21-set11] upsetplot/tests/test_upsetplot.py::test_two_sets[set21-set12] upsetplot/tests/test_upsetplot.py::test_two_sets[set21-set13] upsetplot/tests/test_upsetplot.py::test_two_sets[set22-set10] upsetplot/tests/test_upsetplot.py::test_two_sets[set22-set11] upsetplot/tests/test_upsetplot.py::test_two_sets[set22-set12] upsetplot/tests/test_upsetplot.py::test_two_sets[set22-set13] upsetplot/tests/test_upsetplot.py::test_two_sets[set23-set10] upsetplot/tests/test_upsetplot.py::test_two_sets[set23-set11] upsetplot/tests/test_upsetplot.py::test_two_sets[set23-set12] upsetplot/tests/test_upsetplot.py::test_two_sets[set23-set13] upsetplot/tests/test_upsetplot.py::test_vertical upsetplot/tests/test_upsetplot.py::test_element_size upsetplot/tests/test_upsetplot.py::test_show_counts[horizontal] upsetplot/tests/test_upsetplot.py::test_show_counts[vertical]
README.rst doc/requirements.txt examples/plot_boston.py CHANGELOG.rst upsetplot/plotting.py
README.rst doc/requirements.txt examples/plot_boston.py CHANGELOG.rst upsetplot/plotting.py