diff --git a/doc/docstrings/barplot.ipynb b/doc/docstrings/barplot.ipynb
index 685d729129..b693cefa93 100644
--- a/doc/docstrings/barplot.ipynb
+++ b/doc/docstrings/barplot.ipynb
@@ -67,7 +67,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.barplot(data=df, x=\"island\", y=\"body_mass_g\", ci=\"sd\")"
+    "sns.barplot(data=df, x=\"island\", y=\"body_mass_g\", errorbar=\"sd\")"
    ]
   },
   {
@@ -87,7 +87,7 @@
    "source": [
     "sns.barplot(\n",
     "    data=df, x=\"body_mass_g\", y=\"island\",\n",
-    "    ci=\"sd\", capsize=.4, errcolor=\".5\",\n",
+    "    errorbar=(\"pi\", 50), capsize=.4, errcolor=\".5\",\n",
     "    linewidth=3, edgecolor=\".5\", facecolor=(0, 0, 0, 0),\n",
     ")"
    ]
diff --git a/doc/docstrings/pointplot.ipynb b/doc/docstrings/pointplot.ipynb
index 69cc0625fb..8334bf8998 100644
--- a/doc/docstrings/pointplot.ipynb
+++ b/doc/docstrings/pointplot.ipynb
@@ -85,7 +85,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.pointplot(data=df, x=\"island\", y=\"body_mass_g\", ci=\"sd\")"
+    "sns.pointplot(data=df, x=\"island\", y=\"body_mass_g\", errorbar=\"sd\")"
    ]
   },
   {
@@ -105,7 +105,7 @@
    "source": [
     "sns.pointplot(\n",
     "    data=df, x=\"body_mass_g\", y=\"island\",\n",
-    "    ci=\"sd\", capsize=.4, join=False, color=\".5\",\n",
+    "    errorbar=(\"pi\", 100), capsize=.4, join=False, color=\".5\",\n",
     ")"
    ]
   },
diff --git a/doc/releases/v0.12.0.txt b/doc/releases/v0.12.0.txt
index 6beda83985..463568db59 100644
--- a/doc/releases/v0.12.0.txt
+++ b/doc/releases/v0.12.0.txt
@@ -20,6 +20,8 @@ This release begins the process of modernizing the :ref:`categorical plots <cate

 - |Enhancement| It is now possible to assign `hue` when using only one coordinate variable (i.e. only `x` or `y`).

+- |Enhancement| It is now possible to disable the legend.
+
 The updates also harmonize behavior with functions that have been more recently introduced. This should be relatively non-disruptive, although a few defaults will change:

 - |Defaults| The functions now hook into matplotlib's unit system for plotting categorical data. (Seaborn's categorical functions actually predate support for categorical data in matplotlib.) This should mostly be transparent to the user, but it may resolve a few edge cases. For example, matplotlib interactivity should work better (e.g., for showing the data value under the cursor).
@@ -42,14 +44,23 @@ Similar enhancements / updates should be expected to roll out to other categoric

 - |Fix| In :func:`swarmplot`, the order of the points in each swarm now matches the order in the original dataset; previously they were sorted. This affects only the underlying data stored in the matplotlib artist, not the visual representation (:pr:`2443`).

+More flexible errorbars
+~~~~~~~~~~~~~~~~~~~~~~~
+
+|API| |Feature|
+
+Increased the flexibility of what can be shown by the internally-calculated errorbars for :func:`lineplot`, :func:`barplot`, and :func:`pointplot`.
+
+With the new `errorbar` parameter, it is now possible to select bootstrap confidence intervals, percentile / predictive intervals, or intervals formed by scaled standard deviations or standard errors. The parameter also accepts an arbitrary function that maps from a vector to an interval. There is a new :doc:`user guide chapter <errorbar_tutorial>` demonstrating these options and explaining when you might want to use each one.
+
+As a consequence of this change, the `ci` parameter has been deprecated. Note that :func:`regplot` does not yet have access to this feature, but it likely will be added there in a future release (:pr:`2407`, :pr:`2866`).
+
 Other updates
 ~~~~~~~~~~~~~

-- |Feature| Increased the flexibility of what can be shown by the internally-calculated errorbars for :func:`lineplot`. With the new `errorbar` parameter, it is now possible to select bootstrap confidence intervals, percentile / predictive intervals, or intervals formed by scaled standard deviations or standard errors. As a consequence of this change, the `ci` parameter has been deprecated. Similar changes will be made to other functions that aggregate data points in future releases. (:pr:`2407`).
-
 - |Feature| It is now possible to aggregate / sort a :func:`lineplot` along the y axis using `orient="y"` (:pr:`2854`).

-- |Enhancement| Example datasets are now stored in an OS-specific cache location (as determined by `appdirs`) rather than in the user's home directory. Users should feel free to remove `~/seaborn-data` if desired (:pr:`2773`).
+- |Feature| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).

 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

@@ -87,6 +98,8 @@ Other updates

 - |Dependencies| Following `NEP29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_, dropped support for Python 3.6 and bumped the minimally-supported versions of the library dependencies.

+- |Dependencies| Example datasets are now stored in an OS-specific cache location (as determined by `appdirs`) rather than in the user's home directory. Users should feel free to remove `~/seaborn-data` if desired (:pr:`2773`).
+
 - |Dependencies| The unit test suite is no longer part of the source or wheel distribution. Seaborn has never had a runtime API for exercising the tests, so this should not have workflow implications (:pr:`2833`).

 - |API| Removed the previously-deprecated `factorplot` along with several previously-deprecated utility functions (`iqr`, `percentiles`, `pmf_hist`, and `sort_df`).
diff --git a/doc/tutorial/categorical.ipynb b/doc/tutorial/categorical.ipynb
index fb0a9226f5..5d3a2df425 100644
--- a/doc/tutorial/categorical.ipynb
+++ b/doc/tutorial/categorical.ipynb
@@ -54,23 +54,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "import numpy as np\n",
     "import seaborn as sns\n",
     "import matplotlib.pyplot as plt\n",
-    "sns.set_theme(style=\"ticks\", color_codes=True)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "tags": [
-     "hide"
-    ]
-   },
-   "outputs": [],
-   "source": [
-    "%matplotlib inline\n",
-    "import numpy as np\n",
+    "sns.set_theme(style=\"ticks\", color_codes=True)\n",
     "np.random.seed(sum(map(ord, \"categorical\")))"
    ]
   },
@@ -91,7 +78,7 @@
    "outputs": [],
    "source": [
     "tips = sns.load_dataset(\"tips\")\n",
-    "sns.catplot(x=\"day\", y=\"total_bill\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\")"
    ]
   },
   {
@@ -107,7 +94,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", jitter=False, data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", jitter=False)"
    ]
   },
   {
@@ -123,7 +110,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"swarm\")"
    ]
   },
   {
@@ -139,7 +126,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\", kind=\"swarm\")"
    ]
   },
   {
@@ -155,7 +142,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"size\", y=\"total_bill\", data=tips)"
+    "sns.catplot(data=tips.query(\"size != 3\"), x=\"size\", y=\"total_bill\")"
    ]
   },
   {
@@ -171,7 +158,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"smoker\", y=\"tip\", order=[\"No\", \"Yes\"], data=tips)"
+    "sns.catplot(data=tips, x=\"smoker\", y=\"tip\", order=[\"No\", \"Yes\"])"
    ]
   },
   {
@@ -187,7 +174,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"time\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"total_bill\", y=\"day\", hue=\"time\", kind=\"swarm\")"
    ]
   },
   {
@@ -211,7 +198,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", kind=\"box\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"box\")"
    ]
   },
   {
@@ -227,7 +214,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"smoker\", kind=\"box\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", hue=\"smoker\", kind=\"box\")"
    ]
   },
   {
@@ -244,8 +231,10 @@
    "outputs": [],
    "source": [
     "tips[\"weekend\"] = tips[\"day\"].isin([\"Sat\", \"Sun\"])\n",
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"weekend\",\n",
-    "            kind=\"box\", dodge=False, data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"weekend\",\n",
+    "    kind=\"box\", dodge=False,\n",
+    ")"
    ]
   },
   {
@@ -262,8 +251,10 @@
    "outputs": [],
    "source": [
     "diamonds = sns.load_dataset(\"diamonds\")\n",
-    "sns.catplot(x=\"color\", y=\"price\", kind=\"boxen\",\n",
-    "            data=diamonds.sort_values(\"color\"))"
+    "sns.catplot(\n",
+    "    data=diamonds.sort_values(\"color\"),\n",
+    "    x=\"color\", y=\"price\", kind=\"boxen\",\n",
+    ")"
    ]
   },
   {
@@ -282,8 +273,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
-    "            kind=\"violin\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"total_bill\", y=\"day\", hue=\"sex\", kind=\"violin\",\n",
+    ")"
    ]
   },
   {
@@ -299,9 +291,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
-    "            kind=\"violin\", bw=.15, cut=0,\n",
-    "            data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
+    "    kind=\"violin\", bw=.15, cut=0,\n",
+    ")"
    ]
   },
   {
@@ -317,8 +310,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
-    "            kind=\"violin\", split=True, data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
+    "    kind=\"violin\", split=True,\n",
+    ")"
    ]
   },
   {
@@ -334,9 +329,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
-    "            kind=\"violin\", inner=\"stick\", split=True,\n",
-    "            palette=\"pastel\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
+    "    kind=\"violin\", inner=\"stick\", split=True, palette=\"pastel\",\n",
+    ")"
    ]
   },
   {
@@ -352,8 +348,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "g = sns.catplot(x=\"day\", y=\"total_bill\", kind=\"violin\", inner=None, data=tips)\n",
-    "sns.swarmplot(x=\"day\", y=\"total_bill\", color=\"k\", size=3, data=tips, ax=g.ax)"
+    "g = sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"violin\", inner=None)\n",
+    "sns.swarmplot(data=tips, x=\"day\", y=\"total_bill\", color=\"k\", size=3, ax=g.ax)"
    ]
   },
   {
@@ -378,7 +374,23 @@
    "outputs": [],
    "source": [
     "titanic = sns.load_dataset(\"titanic\")\n",
-    "sns.catplot(x=\"sex\", y=\"survived\", hue=\"class\", kind=\"bar\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"sex\", y=\"survived\", hue=\"class\", kind=\"bar\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The default error bars show 95% confidence intervals, but (starting in v0.12), it is possible to select from a number of other representations:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sns.catplot(data=titanic, x=\"age\", y=\"deck\", errorbar=(\"pi\", 95), kind=\"bar\")"
    ]
   },
   {
@@ -394,7 +406,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"deck\", kind=\"count\", palette=\"ch:.25\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"deck\", kind=\"count\", palette=\"ch:.25\")"
    ]
   },
   {
@@ -410,9 +422,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(y=\"deck\", hue=\"class\", kind=\"count\",\n",
-    "            palette=\"pastel\", edgecolor=\".6\",\n",
-    "            data=titanic)"
+    "sns.catplot(\n",
+    "    data=titanic, y=\"deck\", hue=\"class\", kind=\"count\",\n",
+    "    palette=\"pastel\", edgecolor=\".6\",\n",
+    ")"
    ]
   },
   {
@@ -431,7 +444,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"sex\", y=\"survived\", hue=\"class\", kind=\"point\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"sex\", y=\"survived\", hue=\"class\", kind=\"point\")"
    ]
   },
   {
@@ -447,10 +460,12 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"class\", y=\"survived\", hue=\"sex\",\n",
-    "            palette={\"male\": \"g\", \"female\": \"m\"},\n",
-    "            markers=[\"^\", \"o\"], linestyles=[\"-\", \"--\"],\n",
-    "            kind=\"point\", data=titanic)"
+    "sns.catplot(\n",
+    "    data=titanic, x=\"class\", y=\"survived\", hue=\"sex\",\n",
+    "    palette={\"male\": \"g\", \"female\": \"m\"},\n",
+    "    markers=[\"^\", \"o\"], linestyles=[\"-\", \"--\"],\n",
+    "    kind=\"point\"\n",
+    ")"
    ]
   },
   {
@@ -486,7 +501,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.violinplot(x=iris.species, y=iris.sepal_length)"
+    "sns.violinplot(x=iris[\"species\"], y=iris[\"sepal_length\"])"
    ]
   },
   {
@@ -503,7 +518,7 @@
    "outputs": [],
    "source": [
     "f, ax = plt.subplots(figsize=(7, 3))\n",
-    "sns.countplot(y=\"deck\", data=titanic, color=\"c\")"
+    "sns.countplot(data=titanic, y=\"deck\", color=\"c\")"
    ]
   },
   {
@@ -524,9 +539,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"smoker\",\n",
-    "            col=\"time\", aspect=.7,\n",
-    "            kind=\"swarm\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"smoker\",\n",
+    "    kind=\"swarm\", col=\"time\", aspect=.7,\n",
+    ")"
    ]
   },
   {
@@ -542,10 +558,17 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "g = sns.catplot(x=\"fare\", y=\"survived\", row=\"class\",\n",
-    "                kind=\"box\", orient=\"h\", height=1.5, aspect=4,\n",
-    "                data=titanic.query(\"fare > 0\"))\n",
-    "g.set(xscale=\"log\")"
+    "g = sns.catplot(\n",
+    "    data=titanic,\n",
+    "    x=\"fare\", y=\"embark_town\", row=\"class\",\n",
+    "    kind=\"box\", orient=\"h\",\n",
+    "    sharex=False, margin_titles=True,\n",
+    "    height=1.5, aspect=4,\n",
+    ")\n",
+    "g.set(xlabel=\"Fare\", ylabel=\"\")\n",
+    "g.set_titles(row_template=\"{row_name} class\")\n",
+    "for ax in g.axes.flat:\n",
+    "    ax.xaxis.set_major_formatter('${x:.0f}')"
    ]
   },
   {
@@ -554,16 +577,16 @@
    "source": [
     ".. raw:: html\n",
     "\n",
-    "    </div>"
+    "    </div>row_template="
    ]
   }
  ],
  "metadata": {
   "celltoolbar": "Tags",
   "kernelspec": {
-   "display_name": "seaborn-py38-latest",
+   "display_name": "seaborn-py39-latest",
    "language": "python",
-   "name": "seaborn-py38-latest"
+   "name": "seaborn-py39-latest"
   },
   "language_info": {
    "codemirror_mode": {
@@ -575,7 +598,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/doc/tutorial/error_bars.ipynb b/doc/tutorial/error_bars.ipynb
index c8db4600f4..2a72dc9e97 100644
--- a/doc/tutorial/error_bars.ipynb
+++ b/doc/tutorial/error_bars.ipynb
@@ -37,8 +37,7 @@
    },
    "outputs": [],
    "source": [
-    "%matplotlib inline\n",
-    "np.random.seed(sum(map(ord, \"relational\")))"
+    "np.random.seed(sum(map(ord, \"errorbars\")))"
    ]
   },
   {
@@ -48,30 +47,20 @@
     "Statistical estimation and error bars\n",
     "=====================================\n",
     "\n",
-    ".. raw:: html\n",
-    "\n",
-    "   <div class=col-md-9>\n",
-    "\n",
     "Data visualization sometimes involves a step of aggregation or estimation, where multiple data points are reduced to a summary statistic such as the mean or median. When showing a summary statistic, it is usually appropriate to add *error bars*, which provide a visual cue about how well the summary represents the underlying data points.\n",
     "\n",
     "Several seaborn functions will automatically calculate both summary statistics and the error bars when \n",
     "given a full dataset. This chapter explains how you can control what the error bars show and why you might choose each of the options that seaborn affords."
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# TODO example plot pointing out what the error bar is"
-   ]
-  },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "The error bars around an estimate of central tendency can show one of two things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.\n",
+    "Error bar options\n",
+    "-----------------\n",
+    "\n",
+    "The error bars around an estimate of central tendency can show one of two general things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.\n",
     "\n",
     "In seaborn, there are two approaches for constructing each kind of error bar. One approach is parametric, using a formula that relies on assumptions about the shape of the distribution. The other approach is nonparametric, using only the data that you provide.\n",
     "\n",
@@ -88,7 +77,7 @@
    },
    "outputs": [],
    "source": [
-    "f, axs = plt.subplots(2, 2, figsize=(6, 4), sharex=True, sharey=True)\n",
+    "f, axs = plt.subplots(2, 2, figsize=(9, 5), sharex=True, sharey=True,)\n",
     "\n",
     "plt.setp(axs, xlim=(-1, 1), ylim=(-1, 1), xticks=[], yticks=[])\n",
     "for ax, color in zip(axs.flat, [\"C0\", \"C1\", \"C3\", \"C2\"]):\n",
@@ -106,9 +95,9 @@
     "axs[1, 0].text(s='errorbar=(\"pi\", width)', **kws)\n",
     "axs[1, 1].text(s='errorbar=(\"ci\", width)', **kws)\n",
     "\n",
-    "kws = dict(size=16)\n",
-    "axs[1, 0].set_xlabel(\"Spread\", **kws)\n",
-    "axs[1, 1].set_xlabel(\"Uncertainty\", **kws)\n",
+    "kws = dict(size=18)\n",
+    "axs[0, 0].set_title(\"Spread\", **kws)\n",
+    "axs[0, 1].set_title(\"Uncertainty\", **kws)\n",
     "axs[0, 0].set_ylabel(\"Parametric\", **kws)\n",
     "axs[1, 0].set_ylabel(\"Nonparametric\", **kws)\n",
     "\n",
@@ -144,7 +133,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "x = np.random.normal(0, 1, 500)\n",
+    "sns.pointplot(x=x, errorbar=\"sd\", capsize=.5)"
    ]
   },
   {
@@ -164,23 +154,14 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=(\"pi\", 50), capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to \"impossible\" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# TODO demonstrate with figure"
+    "The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to \"impossible\" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data."
    ]
   },
   {
@@ -195,7 +176,7 @@
     "Standard error bars\n",
     "~~~~~~~~~~~~~~~~~~~\n",
     "\n",
-    "The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar=\"se\"`, draws an interval +/- 1 standard error from the mean, but you can draw a different interval by scaling:"
+    "The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar=\"se\"`, draws an interval +/-1 standard error from the mean:"
    ]
   },
   {
@@ -204,7 +185,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"se\", capsize=.5)"
    ]
   },
   {
@@ -216,7 +197,7 @@
     "\n",
     "The nonparametric approach to representing uncertainty uses *bootstrapping*: a procedure where the dataset is randomly resampled with replacement a number of times, and the estimate is recalculated from each resample. This procedure creates a distribution of statistics approximating the distribution of values that you could have gotten for your estimate if you had a different sample.\n",
     "\n",
-    "The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar=\"ci\"` draws a 95% confidence interval, but you can choose a smaller or larger one by setting a different width:"
+    "The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar=\"ci\"` draws a 95% confidence interval:"
    ]
   },
   {
@@ -225,7 +206,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"ci\", capsize=.5)"
    ]
   },
   {
@@ -241,7 +222,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=(\"se\", 2), capsize=.5)"
    ]
   },
   {
@@ -257,14 +238,30 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"ci\", estimator=\"median\", capsize=.5)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Bootstrapping involves randomness, and the error bars will appear slightly different each time you run the code that creates them. A few parameters control this. One sets the number of iterations (`n_boot`): with more iterations, the resulting intervals will be more stable. The other sets the `seed` for the random number generator, which will ensure identical results:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sns.pointplot(x=x, errorbar=\"ci\", n_boot=5000, seed=10, capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "On the other hand, the bootstrap procedure is much more computationally-intensive. For large datasets, bootstrap intervals can be expensive to compute. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread."
+    "Because of its iterative process, bootstrap intervals can be expensive to compute, especially for large datasets. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread."
    ]
   },
   {
@@ -283,13 +280,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=lambda x: (x.min(), x.max()), capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
+    "(In practice, you could show the full range of the data with `errorbar=(\"pi\", 100)` rather than the custom function shown above).\n",
+    "\n",
     "Note that seaborn functions cannot currently draw error bars from values that have been calculated externally, although matplotlib functions can be used to add such error bars to seaborn plots."
    ]
   },
@@ -309,14 +308,16 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "x = np.random.normal(0, 1, 50)\n",
+    "y = x * 2 + np.random.normal(0, 2, size=x.size)\n",
+    "sns.regplot(x=x, y=y)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "Most of the same options apply. A regression line represents a conditional mean: the estimated average value of the *y* variable given a specific *x* value. So a standard error or confidence interval represents the uncertainty around the estimate of that conditional mean, whereas a standard deviation interval represents a prediction about the range of *y* would would see if given that *x*. Because the regression model extrapolates beyond the data at hand, it is not possible to draw a percentile interval around it."
+    "Currently, the error bars on a regression estimate are less flexible, only showing a confidence interval with a size set through `ci=`. This may change in the future."
    ]
   },
   {
@@ -336,9 +337,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "seaborn-py38-latest",
+   "display_name": "seaborn-py39-latest",
    "language": "python",
-   "name": "seaborn-py38-latest"
+   "name": "seaborn-py39-latest"
   },
   "language_info": {
    "codemirror_mode": {
@@ -350,7 +351,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,
diff --git a/seaborn/_statistics.py b/seaborn/_statistics.py
index a0ff848fcb..e745f3fb70 100644
--- a/seaborn/_statistics.py
+++ b/seaborn/_statistics.py
@@ -462,8 +462,7 @@ def __init__(self, estimator, errorbar=None, **boot_kws):
         errorbar : string, (string, number) tuple, or callable
             Name of errorbar method (either "ci", "pi", "se", or "sd"), or a tuple
             with a method name and a level parameter, or a function that maps from a
-            vector to a (min, max) interval. See the :ref:`tutorial <errorbar_tutorial>`
-            for more information.
+            vector to a (min, max) interval.
         boot_kws
             Additional keywords are passed to bootstrap when error_method is "ci".

@@ -487,7 +486,7 @@ def __call__(self, data, var):
         elif len(data) <= 1:
             err_min = err_max = np.nan

-        # Generic errorbars from use-supplied function
+        # Generic errorbars from user-supplied function
         elif callable(self.error_method):
             err_min, err_max = self.error_method(vals)

@@ -514,7 +513,7 @@ def _percentile_interval(data, width):
     """Return a percentile interval from data of a given width."""
     edge = (100 - width) / 2
     percentiles = edge, 100 - edge
-    return np.percentile(data, percentiles)
+    return np.nanpercentile(data, percentiles)

 def _validate_errorbar_arg(arg):
diff --git a/seaborn/categorical.py b/seaborn/categorical.py
index 3b69c855f7..fe2c6fd1e2 100644
--- a/seaborn/categorical.py
+++ b/seaborn/categorical.py
@@ -26,7 +26,7 @@
 from seaborn.relational import _RelationalPlotter
 from seaborn import utils
 from seaborn.utils import remove_na, _normal_quantile_func, _draw_figure, _default_color
-from seaborn.algorithms import bootstrap
+from seaborn._statistics import EstimateAggregator
 from seaborn.palettes import color_palette, husl_palette, light_palette, dark_palette
 from seaborn.axisgrid import FacetGrid, _facet_docs

@@ -1435,7 +1435,7 @@ def nested_width(self):
             width = self.width
         return width

-    def estimate_statistic(self, estimator, ci, n_boot, seed):
+    def estimate_statistic(self, estimator, errorbar, n_boot, seed):

         if self.hue_names is None:
             statistic = []
@@ -1444,99 +1444,48 @@ def estimate_statistic(self, estimator, ci, n_boot, seed):
             statistic = [[] for _ in self.plot_data]
             confint = [[] for _ in self.plot_data]

+        var = {"v": "y", "h": "x"}[self.orient]
+
+        agg = EstimateAggregator(estimator, errorbar, n_boot=n_boot, seed=seed)
+
         for i, group_data in enumerate(self.plot_data):

             # Option 1: we have a single layer of grouping
             # --------------------------------------------
-
             if self.plot_hues is None:

-                if self.plot_units is None:
-                    stat_data = remove_na(group_data)
-                    unit_data = None
-                else:
-                    unit_data = self.plot_units[i]
-                    have = pd.notnull(np.c_[group_data, unit_data]).all(axis=1)
-                    stat_data = group_data[have]
-                    unit_data = unit_data[have]
-
-                # Estimate a statistic from the vector of data
-                if not stat_data.size:
-                    statistic.append(np.nan)
-                else:
-                    statistic.append(estimator(stat_data))
-
-                # Get a confidence interval for this estimate
-                if ci is not None:
+                df = pd.DataFrame({var: group_data})
+                if self.plot_units is not None:
+                    df["units"] = self.plot_units[i]

-                    if stat_data.size < 2:
-                        confint.append([np.nan, np.nan])
-                        continue
-
-                    if ci == "sd":
-
-                        estimate = estimator(stat_data)
-                        sd = np.std(stat_data)
-                        confint.append((estimate - sd, estimate + sd))
+                res = agg(df, var)

-                    else:
-
-                        boots = bootstrap(stat_data, func=estimator,
-                                          n_boot=n_boot,
-                                          units=unit_data,
-                                          seed=seed)
-                        confint.append(utils.ci(boots, ci))
+                statistic.append(res[var])
+                if errorbar is not None:
+                    confint.append((res[f"{var}min"], res[f"{var}max"]))

             # Option 2: we are grouping by a hue layer
             # ----------------------------------------

             else:
-                for j, hue_level in enumerate(self.hue_names):
+                for hue_level in self.hue_names:

                     if not self.plot_hues[i].size:
                         statistic[i].append(np.nan)
-                        if ci is not None:
+                        if errorbar is not None:
                             confint[i].append((np.nan, np.nan))
                         continue

                     hue_mask = self.plot_hues[i] == hue_level
-                    if self.plot_units is None:
-                        stat_data = remove_na(group_data[hue_mask])
-                        unit_data = None
-                    else:
-                        group_units = self.plot_units[i]
-                        have = pd.notnull(
-                            np.c_[group_data, group_units]
-                        ).all(axis=1)
-                        stat_data = group_data[hue_mask & have]
-                        unit_data = group_units[hue_mask & have]
-
-                    # Estimate a statistic from the vector of data
-                    if not stat_data.size:
-                        statistic[i].append(np.nan)
-                    else:
-                        statistic[i].append(estimator(stat_data))
-
-                    # Get a confidence interval for this estimate
-                    if ci is not None:
-
-                        if stat_data.size < 2:
-                            confint[i].append([np.nan, np.nan])
-                            continue
+                    df = pd.DataFrame({var: group_data[hue_mask]})
+                    if self.plot_units is not None:
+                        df["units"] = self.plot_units[i][hue_mask]

-                        if ci == "sd":
+                    res = agg(df, var)

-                            estimate = estimator(stat_data)
-                            sd = np.std(stat_data)
-                            confint[i].append((estimate - sd, estimate + sd))
-
-                        else:
-
-                            boots = bootstrap(stat_data, func=estimator,
-                                              n_boot=n_boot,
-                                              units=unit_data,
-                                              seed=seed)
-                            confint[i].append(utils.ci(boots, ci))
+                    statistic[i].append(res[var])
+                    if errorbar is not None:
+                        confint[i].append((res[f"{var}min"], res[f"{var}max"]))

         # Save the resulting values for plotting
         self.statistic = np.array(statistic)
@@ -1572,17 +1521,16 @@ def draw_confints(self, ax, at_group, confint, colors,

 class _BarPlotter(_CategoricalStatPlotter):
-    """Show point estimates and confidence intervals with bars."""

     def __init__(self, x, y, hue, data, order, hue_order,
-                 estimator, ci, n_boot, units, seed,
+                 estimator, errorbar, n_boot, units, seed,
                  orient, color, palette, saturation, width,
                  errcolor, errwidth, capsize, dodge):
         """Initialize the plotter."""
         self.establish_variables(x, y, hue, data, orient,
                                  order, hue_order, units)
         self.establish_colors(color, palette, saturation)
-        self.estimate_statistic(estimator, ci, n_boot, seed)
+        self.estimate_statistic(estimator, errorbar, n_boot, seed)

         self.dodge = dodge
         self.width = width
@@ -1645,16 +1593,15 @@ class _PointPlotter(_CategoricalStatPlotter):

     default_palette = "dark"

-    """Show point estimates and confidence intervals with (joined) points."""
     def __init__(self, x, y, hue, data, order, hue_order,
-                 estimator, ci, n_boot, units, seed,
+                 estimator, errorbar, n_boot, units, seed,
                  markers, linestyles, dodge, join, scale,
                  orient, color, palette, errwidth=None, capsize=None):
         """Initialize the plotter."""
         self.establish_variables(x, y, hue, data, orient,
                                  order, hue_order, units)
         self.establish_colors(color, palette, 1)
-        self.estimate_statistic(estimator, ci, n_boot, seed)
+        self.estimate_statistic(estimator, errorbar, n_boot, seed)

         # Override the default palette for single-color plots
         if hue is None and color is None and palette is None:
@@ -2113,13 +2060,12 @@ def plot(self, ax, boxplot_kws):
         inferred from the data objects.\
     """),
     stat_api_params=dedent("""\
-    estimator : callable that maps vector -> scalar, optional
+    estimator : string or callable that maps vector -> scalar, optional
         Statistical function to estimate within each categorical bin.
-    ci : float or "sd" or None, optional
-        Size of confidence intervals to draw around estimated values.  If
-        "sd", skip bootstrapping and draw the standard deviation of the
-        observations. If ``None``, no bootstrapping will be performed, and
-        error bars will not be drawn.
+    errorbar : string, (string, number) tuple, or callable
+        Name of errorbar method (either "ci", "pi", "se", or "sd"), or a tuple
+        with a method name and a level parameter, or a function that maps from a
+        vector to a (min, max) interval.
     n_boot : int, optional
         Number of bootstrap samples used to compute confidence intervals.
     units : name of variable in ``data`` or vector data, optional
@@ -2743,15 +2689,22 @@ def swarmplot(

 def barplot(
     data=None, *, x=None, y=None, hue=None, order=None, hue_order=None,
-    estimator=np.mean, ci=95, n_boot=1000, units=None, seed=None,
+    estimator="mean", errorbar=("ci", 95), n_boot=1000, units=None, seed=None,
     orient=None, color=None, palette=None, saturation=.75, width=.8,
-    errcolor=".26", errwidth=None, capsize=None, dodge=True,
+    errcolor=".26", errwidth=None, capsize=None, dodge=True, ci="deprecated",
     ax=None,
     **kwargs,
 ):

+    errorbar = utils._deprecate_ci(errorbar, ci)
+
+    # Be backwards compatible with len passed directly, which
+    # does not work in Series.agg (maybe a pandas bug?)
+    if estimator is len:
+        estimator = "size"
+
     plotter = _BarPlotter(x, y, hue, data, order, hue_order,
-                          estimator, ci, n_boot, units, seed,
+                          estimator, errorbar, n_boot, units, seed,
                           orient, color, palette, saturation,
                           width, errcolor, errwidth, capsize, dodge)

@@ -2763,7 +2716,7 @@ def barplot(

 barplot.__doc__ = dedent("""\
-    Show point estimates and confidence intervals as rectangular bars.
+    Show point estimates and errors as rectangular bars.

     A bar plot represents an estimate of central tendency for a numeric
     variable with the height of each rectangle and provides some indication of
@@ -2826,14 +2779,16 @@ def barplot(

 def pointplot(
     data=None, *, x=None, y=None, hue=None, order=None, hue_order=None,
-    estimator=np.mean, ci=95, n_boot=1000, units=None, seed=None,
+    estimator="mean", errorbar=("ci", 95), n_boot=1000, units=None, seed=None,
     markers="o", linestyles="-", dodge=False, join=True, scale=1,
-    orient=None, color=None, palette=None, errwidth=None,
+    orient=None, color=None, palette=None, errwidth=None, ci="deprecated",
     capsize=None, ax=None,
 ):

+    errorbar = utils._deprecate_ci(errorbar, ci)
+
     plotter = _PointPlotter(x, y, hue, data, order, hue_order,
-                            estimator, ci, n_boot, units, seed,
+                            estimator, errorbar, n_boot, units, seed,
                             markers, linestyles, dodge, join, scale,
                             orient, color, palette, errwidth, capsize)

@@ -2845,7 +2800,7 @@ def pointplot(

 pointplot.__doc__ = dedent("""\
-    Show point estimates and confidence intervals using dot marks.
+    Show point estimates and errors using dot marks.

     A point plot represents an estimate of central tendency for a numeric
     variable by the position of the dot and provides some indication of the
@@ -2916,8 +2871,8 @@ def countplot(
     dodge=True, ax=None, **kwargs
 ):

-    estimator = len
-    ci = None
+    estimator = "size"
+    errorbar = None
     n_boot = 0
     units = None
     seed = None
@@ -2936,7 +2891,7 @@ def countplot(

     plotter = _CountPlotter(
         x, y, hue, data, order, hue_order,
-        estimator, ci, n_boot, units, seed,
+        estimator, errorbar, n_boot, units, seed,
         orient, color, palette, saturation,
         width, errcolor, errwidth, capsize, dodge
     )
@@ -2996,12 +2951,12 @@ def countplot(

 def catplot(
     data=None, *, x=None, y=None, hue=None, row=None, col=None,
-    col_wrap=None, estimator=np.mean, ci=95, n_boot=1000,
+    col_wrap=None, estimator="mean", errorbar=("ci", 95), n_boot=1000,
     units=None, seed=None, order=None, hue_order=None, row_order=None,
     col_order=None, height=5, aspect=1, kind="strip", native_scale=False,
     formatter=None, orient=None, color=None, palette=None, hue_norm=None,
     legend="auto", legend_out=True, sharex=True, sharey=True,
-    margin_titles=False, facet_kws=None,
+    margin_titles=False, facet_kws=None, ci="deprecated",
     **kwargs
 ):

@@ -3225,8 +3180,10 @@ def catplot(
     plot_kws.update(kwargs)

     if kind in ["bar", "point"]:
+        errorbar = utils._deprecate_ci(errorbar, ci)
         plot_kws.update(
-            estimator=estimator, ci=ci, n_boot=n_boot, units=units, seed=seed,
+            estimator=estimator, errorbar=errorbar,
+            n_boot=n_boot, units=units, seed=seed,
         )

     # Initialize the facets

diff --git a/tests/test_categorical.py b/tests/test_categorical.py
index 70554b3c81..686fb7bfc4 100644
--- a/tests/test_categorical.py
+++ b/tests/test_categorical.py
@@ -517,7 +517,7 @@ def test_no_bootstrappig(self):

         p = cat._CategoricalStatPlotter()
         p.establish_variables("g", "y", data=self.df)
-        p.estimate_statistic(np.mean, None, 100, None)
+        p.estimate_statistic("mean", None, 100, None)
         npt.assert_array_equal(p.confint, np.array([]))

         p.establish_variables("g", "y", hue="h", data=self.df)
@@ -532,7 +532,7 @@ def test_single_layer_stats(self):
         y = pd.Series(np.random.RandomState(0).randn(300))

         p.establish_variables(g, y)
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)

         assert p.statistic.shape == (3,)
         assert p.confint.shape == (3, 2)
@@ -558,11 +558,11 @@ def test_single_layer_stats_with_units(self):
         y[u == "y"] += 3

         p.establish_variables(g, y)
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)
         stat1, ci1 = p.statistic, p.confint

         p.establish_variables(g, y, units=u)
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)
         stat2, ci2 = p.statistic, p.confint

         npt.assert_array_equal(stat1, stat2)
@@ -578,7 +578,7 @@ def test_single_layer_stats_with_missing_data(self):
         y = pd.Series(np.random.RandomState(0).randn(300))

         p.establish_variables(g, y, order=list("abdc"))
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)

         assert p.statistic.shape == (4,)
         assert p.confint.shape == (4, 2)
@@ -603,7 +603,7 @@ def test_nested_stats(self):
         y = pd.Series(np.random.RandomState(0).randn(300))

         p.establish_variables(g, y, h)
-        p.estimate_statistic(np.mean, 95, 50000, None)
+        p.estimate_statistic("mean", ("ci", 95), 50000, None)

         assert p.statistic.shape == (3, 2)
         assert p.confint.shape == (3, 2, 2)
@@ -628,9 +628,9 @@ def test_bootstrap_seed(self):
         y = pd.Series(np.random.RandomState(0).randn(300))

         p.establish_variables(g, y, h)
-        p.estimate_statistic(np.mean, 95, 1000, 0)
+        p.estimate_statistic("mean", ("ci", 95), 1000, 0)
         confint_1 = p.confint
-        p.estimate_statistic(np.mean, 95, 1000, 0)
+        p.estimate_statistic("mean", ("ci", 95), 1000, 0)
         confint_2 = p.confint

         npt.assert_array_equal(confint_1, confint_2)
@@ -647,11 +647,11 @@ def test_nested_stats_with_units(self):
         y[u == "k"] += 3

         p.establish_variables(g, y, h)
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)
         stat1, ci1 = p.statistic, p.confint

         p.establish_variables(g, y, h, units=u)
-        p.estimate_statistic(np.mean, 95, 10000, None)
+        p.estimate_statistic("mean", ("ci", 95), 10000, None)
         stat2, ci2 = p.statistic, p.confint

         npt.assert_array_equal(stat1, stat2)
@@ -670,7 +670,7 @@ def test_nested_stats_with_missing_data(self):
         p.establish_variables(g, y, h,
                               order=list("abdc"),
                               hue_order=list("zyx"))
-        p.estimate_statistic(np.mean, 95, 50000, None)
+        p.estimate_statistic("mean", ("ci", 95), 50000, None)

         assert p.statistic.shape == (4, 3)
         assert p.confint.shape == (4, 3, 2)
@@ -2199,8 +2199,8 @@ class TestSwarmPlot(SharedScatterTests):
 class TestBarPlotter(CategoricalFixture):

     default_kws = dict(
-        x=None, y=None, hue=None, data=None,
-        estimator=np.mean, ci=95, n_boot=100, units=None, seed=None,
+        data=None, x=None, y=None, hue=None, units=None,
+        estimator="mean", errorbar=("ci", 95), n_boot=100, seed=None,
         order=None, hue_order=None,
         orient=None, color=None, palette=None,
         saturation=.75, width=0.8,
@@ -2451,12 +2451,25 @@ def test_simple_barplots(self):
         assert ax.get_ylabel() == "g"
         plt.close("all")

+    def test_errorbar(self, long_df):
+
+        ax = cat.barplot(data=long_df, x="a", y="y", errorbar=("sd", 2))
+        order = categorical_order(long_df["a"])
+
+        for i, line in enumerate(ax.lines):
+            sub_df = long_df.loc[long_df["a"] == order[i], "y"]
+            mean = sub_df.mean()
+            sd = sub_df.std()
+            expected = mean - 2 * sd, mean + 2 * sd
+            assert_array_equal(line.get_ydata(), expected)
+

 class TestPointPlotter(CategoricalFixture):

     default_kws = dict(
         x=None, y=None, hue=None, data=None,
-        estimator=np.mean, ci=95, n_boot=100, units=None, seed=None,
+        estimator="mean", errorbar=("ci", 95),
+        n_boot=100, units=None, seed=None,
         order=None, hue_order=None,
         markers="o", linestyles="-", dodge=0,
         join=True, scale=1,
@@ -2716,6 +2729,20 @@ def test_simple_pointplots(self):
         assert ax.get_ylabel() == "g"
         plt.close("all")

+    def test_errorbar(self, long_df):
+
+        ax = cat.pointplot(
+            data=long_df, x="a", y="y", errorbar=("sd", 2), join=False
+        )
+        order = categorical_order(long_df["a"])
+
+        for i, line in enumerate(ax.lines):
+            sub_df = long_df.loc[long_df["a"] == order[i], "y"]
+            mean = sub_df.mean()
+            sd = sub_df.std()
+            expected = mean - 2 * sd, mean + 2 * sd
+            assert_array_equal(line.get_ydata(), expected)
+

 class TestCountPlot(CategoricalFixture):

--- a/doc/docstrings/barplot.ipynb
+++ b/doc/docstrings/barplot.ipynb
@@ -8,13 +8,13 @@
 Add a second layer of grouping:
 sns.barplot(data=df, x="island", y="body_mass_g", hue="sex")
 Use the error bars to show the standard deviation rather than a confidence interval:
-sns.barplot(data=df, x="island", y="body_mass_g", ci="sd")
+sns.barplot(data=df, x="island", y="body_mass_g", errorbar="sd")
 Customize the appearance of the plot:
 sns.barplot(

     data=df, x="body_mass_g", y="island",

-    ci="sd", capsize=.4, errcolor=".5",
+    errorbar=("pi", 50), capsize=.4, errcolor=".5",

     linewidth=3, edgecolor=".5", facecolor=(0, 0, 0, 0),

--- a/doc/docstrings/pointplot.ipynb
+++ b/doc/docstrings/pointplot.ipynb
@@ -10,12 +10,12 @@
 Adjust the artists along the categorical axis to reduce overplotting:
 sns.pointplot(data=df, x="sex", y="bill_depth_mm", hue="island", dodge=True)
 Use the error bars to show the standard deviation rather than a confidence interval:
-sns.pointplot(data=df, x="island", y="body_mass_g", ci="sd")
+sns.pointplot(data=df, x="island", y="body_mass_g", errorbar="sd")
 Customize the appearance of the plot:
 sns.pointplot(

     data=df, x="body_mass_g", y="island",

-    ci="sd", capsize=.4, join=False, color=".5",
+    errorbar=("pi", 100), capsize=.4, join=False, color=".5",

 )

--- a/doc/tutorial/categorical.ipynb
+++ b/doc/tutorial/categorical.ipynb
@@ -68,14 +68,13 @@

 In this tutorial, we'll mostly focus on the figure-level interface, :func:`catplot`. Remember that this function is a higher-level interface each of the functions above, so we'll reference them when we show each kind of plot, keeping the more verbose kind-specific API documentation at hand.
+import numpy as np
+
 import seaborn as sns

 import matplotlib.pyplot as plt

 sns.set_theme(style="ticks", color_codes=True)
-%matplotlib inline
-
-import numpy as np

 np.random.seed(sum(map(ord, "categorical")))
 Categorical scatterplots
@@ -87,19 +86,19 @@
 The default representation of the data in :func:`catplot` uses a scatterplot. There are actually two different categorical scatter plots in seaborn. They take different approaches to resolving the main challenge in representing categorical data with a scatter plot, which is that all of the points belonging to one category would fall on the same position along the axis corresponding to the categorical variable. The approach used by :func:`stripplot`, which is the default "kind" in :func:`catplot` is to adjust the positions of points on the categorical axis with a small amount of random "jitter":
 tips = sns.load_dataset("tips")

-sns.catplot(x="day", y="total_bill", data=tips)
+sns.catplot(data=tips, x="day", y="total_bill")
 The ``jitter`` parameter controls the magnitude of jitter or disables it altogether:
-sns.catplot(x="day", y="total_bill", jitter=False, data=tips)
+sns.catplot(data=tips, x="day", y="total_bill", jitter=False)
 The second approach adjusts the points along the categorical axis using an algorithm that prevents them from overlapping. It can give a better representation of the distribution of observations, although it only works well for relatively small datasets. This kind of plot is sometimes called a "beeswarm" and is drawn in seaborn by :func:`swarmplot`, which is activated by setting ``kind="swarm"`` in :func:`catplot`:
-sns.catplot(x="day", y="total_bill", kind="swarm", data=tips)
+sns.catplot(data=tips, x="day", y="total_bill", kind="swarm")
 Similar to the relational plots, it's possible to add another dimension to a categorical plot by using a ``hue`` semantic. (The categorical plots do not currently support ``size`` or ``style`` semantics). Each different categorical plotting function handles the ``hue`` semantic differently. For the scatter plots, it is only necessary to change the color of the points:
-sns.catplot(x="day", y="total_bill", hue="sex", kind="swarm", data=tips)
+sns.catplot(data=tips, x="day", y="total_bill", hue="sex", kind="swarm")
 Unlike with numerical data, it is not always obvious how to order the levels of the categorical variable along its axis. In general, the seaborn categorical plotting functions try to infer the order of categories from the data. If your data have a pandas ``Categorical`` datatype, then the default order of the categories can be set there. If the variable passed to the categorical axis looks numerical, the levels will be sorted. But the data are still treated as categorical and drawn at ordinal positions on the categorical axes (specifically, at 0, 1, ...) even when numbers are used to label them:
-sns.catplot(x="size", y="total_bill", data=tips)
+sns.catplot(data=tips.query("size != 3"), x="size", y="total_bill")
 The other option for choosing a default ordering is to take the levels of the category as they appear in the dataset. The ordering can also be controlled on a plot-specific basis using the ``order`` parameter. This can be important when drawing multiple categorical plots in the same figure, which we'll see more of below:
-sns.catplot(x="smoker", y="tip", order=["No", "Yes"], data=tips)
+sns.catplot(data=tips, x="smoker", y="tip", order=["No", "Yes"])
 We've referred to the idea of "categorical axis". In these examples, that's always corresponded to the horizontal axis. But it's often helpful to put the categorical variable on the vertical axis (particularly when the category names are relatively long or there are many categories). To do this, swap the assignment of variables to axes:
-sns.catplot(x="total_bill", y="day", hue="time", kind="swarm", data=tips)
+sns.catplot(data=tips, x="total_bill", y="day", hue="time", kind="swarm")
 Distributions of observations within categories

 -----------------------------------------------
@@ -117,21 +116,29 @@

 The first is the familiar :func:`boxplot`. This kind of plot shows the three quartile values of the distribution along with extreme values. The "whiskers" extend to points that lie within 1.5 IQRs of the lower and upper quartile, and then observations that fall outside this range are displayed independently. This means that each value in the boxplot corresponds to an actual observation in the data.
-sns.catplot(x="day", y="total_bill", kind="box", data=tips)
+sns.catplot(data=tips, x="day", y="total_bill", kind="box")
 When adding a ``hue`` semantic, the box for each level of the semantic variable is moved along the categorical axis so they don't overlap:
-sns.catplot(x="day", y="total_bill", hue="smoker", kind="box", data=tips)
+sns.catplot(data=tips, x="day", y="total_bill", hue="smoker", kind="box")
 This behavior is called "dodging" and is turned on by default because it is assumed that the semantic variable is nested within the main categorical variable. If that's not the case, you can disable the dodging:
 tips["weekend"] = tips["day"].isin(["Sat", "Sun"])

-sns.catplot(x="day", y="total_bill", hue="weekend",
-
-            kind="box", dodge=False, data=tips)
+sns.catplot(
+
+    data=tips, x="day", y="total_bill", hue="weekend",
+
+    kind="box", dodge=False,
+
+)
 A related function, :func:`boxenplot`, draws a plot that is similar to a box plot but optimized for showing more information about the shape of the distribution. It is best suited for larger datasets:
 diamonds = sns.load_dataset("diamonds")

-sns.catplot(x="color", y="price", kind="boxen",
-
-            data=diamonds.sort_values("color"))
+sns.catplot(
+
+    data=diamonds.sort_values("color"),
+
+    x="color", y="price", kind="boxen",
+
+)
 Violinplots

 ^^^^^^^^^^^
@@ -139,29 +146,39 @@

 A different approach is a :func:`violinplot`, which combines a boxplot with the kernel density estimation procedure described in the :ref:`distributions <distribution_tutorial>` tutorial:
-sns.catplot(x="total_bill", y="day", hue="sex",
-
-            kind="violin", data=tips)
+sns.catplot(
+
+    data=tips, x="total_bill", y="day", hue="sex", kind="violin",
+
+)
 This approach uses the kernel density estimate to provide a richer description of the distribution of values. Additionally, the quartile and whisker values from the boxplot are shown inside the violin. The downside is that, because the violinplot uses a KDE, there are some other parameters that may need tweaking, adding some complexity relative to the straightforward boxplot:
-sns.catplot(x="total_bill", y="day", hue="sex",
-
-            kind="violin", bw=.15, cut=0,
-
-            data=tips)
+sns.catplot(
+
+    data=tips, x="total_bill", y="day", hue="sex",
+
+    kind="violin", bw=.15, cut=0,
+
+)
 It's also possible to "split" the violins when the hue parameter has only two levels, which can allow for a more efficient use of space:
-sns.catplot(x="day", y="total_bill", hue="sex",
-
-            kind="violin", split=True, data=tips)
+sns.catplot(
+
+    data=tips, x="day", y="total_bill", hue="sex",
+
+    kind="violin", split=True,
+
+)
 Finally, there are several options for the plot that is drawn on the interior of the violins, including ways to show each individual observation instead of the summary boxplot values:
-sns.catplot(x="day", y="total_bill", hue="sex",
-
-            kind="violin", inner="stick", split=True,
-
-            palette="pastel", data=tips)
+sns.catplot(
+
+    data=tips, x="day", y="total_bill", hue="sex",
+
+    kind="violin", inner="stick", split=True, palette="pastel",
+
+)
 It can also be useful to combine :func:`swarmplot` or :func:`stripplot` with a box plot or violin plot to show each observation along with a summary of the distribution:
-g = sns.catplot(x="day", y="total_bill", kind="violin", inner=None, data=tips)
-
-sns.swarmplot(x="day", y="total_bill", color="k", size=3, data=tips, ax=g.ax)
+g = sns.catplot(data=tips, x="day", y="total_bill", kind="violin", inner=None)
+
+sns.swarmplot(data=tips, x="day", y="total_bill", color="k", size=3, ax=g.ax)
 Statistical estimation within categories

 ----------------------------------------
@@ -181,15 +198,19 @@
 A familiar style of plot that accomplishes this goal is a bar plot. In seaborn, the :func:`barplot` function operates on a full dataset and applies a function to obtain the estimate (taking the mean by default). When there are multiple observations in each category, it also uses bootstrapping to compute a confidence interval around the estimate, which is plotted using error bars:
 titanic = sns.load_dataset("titanic")

-sns.catplot(x="sex", y="survived", hue="class", kind="bar", data=titanic)
+sns.catplot(data=titanic, x="sex", y="survived", hue="class", kind="bar")
+The default error bars show 95% confidence intervals, but (starting in v0.12), it is possible to select from a number of other representations:
+sns.catplot(data=titanic, x="age", y="deck", errorbar=("pi", 95), kind="bar")
 A special case for the bar plot is when you want to show the number of observations in each category rather than computing a statistic for a second variable. This is similar to a histogram over a categorical, rather than quantitative, variable. In seaborn, it's easy to do so with the :func:`countplot` function:
-sns.catplot(x="deck", kind="count", palette="ch:.25", data=titanic)
+sns.catplot(data=titanic, x="deck", kind="count", palette="ch:.25")
 Both :func:`barplot` and :func:`countplot` can be invoked with all of the options discussed above, along with others that are demonstrated in the detailed documentation for each function:
-sns.catplot(y="deck", hue="class", kind="count",
-
-            palette="pastel", edgecolor=".6",
-
-            data=titanic)
+sns.catplot(
+
+    data=titanic, y="deck", hue="class", kind="count",
+
+    palette="pastel", edgecolor=".6",
+
+)
 Point plots

 ^^^^^^^^^^^
@@ -197,15 +218,19 @@

 An alternative style for visualizing the same information is offered by the :func:`pointplot` function. This function also encodes the value of the estimate with height on the other axis, but rather than showing a full bar, it plots the point estimate and confidence interval. Additionally, :func:`pointplot` connects points from the same ``hue`` category. This makes it easy to see how the main relationship is changing as a function of the hue semantic, because your eyes are quite good at picking up on differences of slopes:
-sns.catplot(x="sex", y="survived", hue="class", kind="point", data=titanic)
+sns.catplot(data=titanic, x="sex", y="survived", hue="class", kind="point")
 While the categorical functions lack the ``style`` semantic of the relational functions, it can still be a good idea to vary the marker and/or linestyle along with the hue to make figures that are maximally accessible and reproduce well in black and white:
-sns.catplot(x="class", y="survived", hue="sex",
-
-            palette={"male": "g", "female": "m"},
-
-            markers=["^", "o"], linestyles=["-", "--"],
-
-            kind="point", data=titanic)
+sns.catplot(
+
+    data=titanic, x="class", y="survived", hue="sex",
+
+    palette={"male": "g", "female": "m"},
+
+    markers=["^", "o"], linestyles=["-", "--"],
+
+    kind="point"
+
+)
 Plotting "wide-form" data

 -------------------------
@@ -217,11 +242,11 @@

 sns.catplot(data=iris, orient="h", kind="box")
 Additionally, the axes-level functions accept vectors of Pandas or numpy objects rather than variables in a ``DataFrame``:
-sns.violinplot(x=iris.species, y=iris.sepal_length)
+sns.violinplot(x=iris["species"], y=iris["sepal_length"])
 To control the size and shape of plots made by the functions discussed above, you must set up the figure yourself using matplotlib commands:
 f, ax = plt.subplots(figsize=(7, 3))

-sns.countplot(y="deck", data=titanic, color="c")
+sns.countplot(data=titanic, y="deck", color="c")
 This is the approach you should take when you need a categorical figure to happily coexist in a more complex figure with other kinds of plots.

@@ -233,21 +258,37 @@

 Just like :func:`relplot`, the fact that :func:`catplot` is built on a :class:`FacetGrid` means that it is easy to add faceting variables to visualize higher-dimensional relationships:
-sns.catplot(x="day", y="total_bill", hue="smoker",
-
-            col="time", aspect=.7,
-
-            kind="swarm", data=tips)
+sns.catplot(
+
+    data=tips, x="day", y="total_bill", hue="smoker",
+
+    kind="swarm", col="time", aspect=.7,
+
+)
 For further customization of the plot, you can use the methods on the :class:`FacetGrid` object that it returns:
-g = sns.catplot(x="fare", y="survived", row="class",
-
-                kind="box", orient="h", height=1.5, aspect=4,
-
-                data=titanic.query("fare > 0"))
-
-g.set(xscale="log")
+g = sns.catplot(
+
+    data=titanic,
+
+    x="fare", y="embark_town", row="class",
+
+    kind="box", orient="h",
+
+    sharex=False, margin_titles=True,
+
+    height=1.5, aspect=4,
+
+)
+
+g.set(xlabel="Fare", ylabel="")
+
+g.set_titles(row_template="{row_name} class")
+
+for ax in g.axes.flat:
+
+    ax.xaxis.set_major_formatter('${x:.0f}')
 .. raw:: html

-    </div>+    </div>row_template=

--- a/doc/tutorial/error_bars.ipynb
+++ b/doc/tutorial/error_bars.ipynb
@@ -14,23 +14,13 @@
 import matplotlib.pyplot as plt

 sns.set_theme(style="darkgrid")
-%matplotlib inline
-
-np.random.seed(sum(map(ord, "relational")))
+np.random.seed(sum(map(ord, "errorbars")))
 Statistical estimation and error bars

 =====================================

-.. raw:: html
-
-
-
-   <div class=col-md-9>
-
-
-
 Data visualization sometimes involves a step of aggregation or estimation, where multiple data points are reduced to a summary statistic such as the mean or median. When showing a summary statistic, it is usually appropriate to add *error bars*, which provide a visual cue about how well the summary represents the underlying data points.

@@ -38,8 +28,13 @@
 Several seaborn functions will automatically calculate both summary statistics and the error bars when

 given a full dataset. This chapter explains how you can control what the error bars show and why you might choose each of the options that seaborn affords.
-# TODO example plot pointing out what the error bar is
-The error bars around an estimate of central tendency can show one of two things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.
+Error bar options
+
+-----------------
+
+
+
+The error bars around an estimate of central tendency can show one of two general things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.

@@ -48,7 +43,7 @@

 Your choice is made with the `errorbar` parameter, which exists for each function that does estimation as part of plotting. This parameter accepts the name of the method to use and, optionally, a parameter that controls the size of the interval. The choices can be defined in a 2D taxonomy that depends on what is shown and how it is constructed:
-f, axs = plt.subplots(2, 2, figsize=(6, 4), sharex=True, sharey=True)
+f, axs = plt.subplots(2, 2, figsize=(9, 5), sharex=True, sharey=True,)

@@ -84,11 +79,11 @@

-kws = dict(size=16)
-
-axs[1, 0].set_xlabel("Spread", **kws)
-
-axs[1, 1].set_xlabel("Uncertainty", **kws)
+kws = dict(size=18)
+
+axs[0, 0].set_title("Spread", **kws)
+
+axs[0, 1].set_title("Uncertainty", **kws)

 axs[0, 0].set_ylabel("Parametric", **kws)

@@ -117,7 +112,9 @@

 Standard deviation error bars are the simplest to explain, because the standard deviation is a familiar statistic. It is the average distance from each data point to the sample mean. By default, `errorbar="sd"` will draw error bars at +/- 1 sd around the estimate, but the range can be increased by passing a scaling size parameter. Note that, assuming normally-distributed data, ~68% of the data will lie within one standard deviation, ~95% will lie within two, and ~99.7% will lie within three:
-# TODO demonstrate with figure
+x = np.random.normal(0, 1, 500)
+
+sns.pointplot(x=x, errorbar="sd", capsize=.5)
 Percentile interval error bars

 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -127,9 +124,8 @@
 Percentile intervals also represent the range where some amount of the data fall, but they do so by

 computing those percentiles directly from your sample. By default, `errorbar="pi"` will show a 95% interval, ranging from the 2.5 to the 97.5 percentiles. You can chose a different range by passing a size parameter, e.g., to show the inter-quartile range:
-# TODO demonstrate with figure
-The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to "impossible" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data:
-# TODO demonstrate with figure
+sns.pointplot(x=x, errorbar=("pi", 50), capsize=.5)
+The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to "impossible" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data.
 Measures of estimate uncertainty

 --------------------------------
@@ -146,8 +142,8 @@

-The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar="se"`, draws an interval +/- 1 standard error from the mean, but you can draw a different interval by scaling:
-# TODO demonstrate with figure
+The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar="se"`, draws an interval +/-1 standard error from the mean:
+sns.pointplot(x=x, errorbar="se", capsize=.5)
 Confidence interval error bars

 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -158,13 +154,15 @@

-The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar="ci"` draws a 95% confidence interval, but you can choose a smaller or larger one by setting a different width:
-# TODO demonstrate with figure
+The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar="ci"` draws a 95% confidence interval:
+sns.pointplot(x=x, errorbar="ci", capsize=.5)
 The seaborn terminology is somewhat specific, because a confidence interval in statistics can be parametric or nonparametric. To draw a parametric confidence interval, you scale the standard error, using a formula similar to the one mentioned above. For example, an approximate 95% confidence interval can be constructed by taking the mean +/- two standard errors:
-# TODO demonstrate with figure
+sns.pointplot(x=x, errorbar=("se", 2), capsize=.5)
 The nonparametric bootstrap has advantages similar to those of the percentile interval: it will naturally adapt to skwewed and bounded data in a way that a standard error interval cannot. It is also more general. While the standard error formula is specific to the mean, error bars can be computed using the boootstrap for any estimator:
-# TODO demonstrate with figure
-On the other hand, the bootstrap procedure is much more computationally-intensive. For large datasets, bootstrap intervals can be expensive to compute. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread.
+sns.pointplot(x=x, errorbar="ci", estimator="median", capsize=.5)
+Bootstrapping involves randomness, and the error bars will appear slightly different each time you run the code that creates them. A few parameters control this. One sets the number of iterations (`n_boot`): with more iterations, the resulting intervals will be more stable. The other sets the `seed` for the random number generator, which will ensure identical results:
+sns.pointplot(x=x, errorbar="ci", n_boot=5000, seed=10, capsize=.5)
+Because of its iterative process, bootstrap intervals can be expensive to compute, especially for large datasets. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread.
 Custom error bars

 -----------------
@@ -172,7 +170,11 @@

 If these recipes are not sufficient, it is also possible to pass a generic function to the `errorbar` parameter. This function should take a vector and produce a pair of values representing the minimum and maximum points of the interval:
-# TODO demonstrate with figure
+sns.pointplot(x=x, errorbar=lambda x: (x.min(), x.max()), capsize=.5)
+(In practice, you could show the full range of the data with `errorbar=("pi", 100)` rather than the custom function shown above).
+
+
+
 Note that seaborn functions cannot currently draw error bars from values that have been calculated externally, although matplotlib functions can be used to add such error bars to seaborn plots.
 Error bars on regression models

@@ -181,8 +183,12 @@

 The preceding discussion has focused on error bars shown around parameter estimates for aggregate data. Error bars also arise in seaborn when estimating regression models to visualize relationships. Here, the error bars will be represented by a "band" around the regression line:
-# TODO demonstrate with figure
-Most of the same options apply. A regression line represents a conditional mean: the estimated average value of the *y* variable given a specific *x* value. So a standard error or confidence interval represents the uncertainty around the estimate of that conditional mean, whereas a standard deviation interval represents a prediction about the range of *y* would would see if given that *x*. Because the regression model extrapolates beyond the data at hand, it is not possible to draw a percentile interval around it.
+x = np.random.normal(0, 1, 50)
+
+y = x * 2 + np.random.normal(0, 2, size=x.size)
+
+sns.regplot(x=x, y=y)
+Currently, the error bars on a regression estimate are less flexible, only showing a confidence interval with a size set through `ci=`. This may change in the future.
 Are error bars enough?

 ----------------------

diff --git a/doc/docstrings/barplot.ipynb b/doc/docstrings/barplot.ipynb
index 685d7291..b693cefa 100644
--- a/doc/docstrings/barplot.ipynb
+++ b/doc/docstrings/barplot.ipynb
@@ -67,7 +67,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.barplot(data=df, x=\"island\", y=\"body_mass_g\", ci=\"sd\")"
+    "sns.barplot(data=df, x=\"island\", y=\"body_mass_g\", errorbar=\"sd\")"
    ]
   },
   {
@@ -87,7 +87,7 @@
    "source": [
     "sns.barplot(\n",
     "    data=df, x=\"body_mass_g\", y=\"island\",\n",
-    "    ci=\"sd\", capsize=.4, errcolor=\".5\",\n",
+    "    errorbar=(\"pi\", 50), capsize=.4, errcolor=\".5\",\n",
     "    linewidth=3, edgecolor=\".5\", facecolor=(0, 0, 0, 0),\n",
     ")"
    ]

diff --git a/doc/docstrings/pointplot.ipynb b/doc/docstrings/pointplot.ipynb
index 69cc0625..8334bf89 100644
--- a/doc/docstrings/pointplot.ipynb
+++ b/doc/docstrings/pointplot.ipynb
@@ -85,7 +85,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.pointplot(data=df, x=\"island\", y=\"body_mass_g\", ci=\"sd\")"
+    "sns.pointplot(data=df, x=\"island\", y=\"body_mass_g\", errorbar=\"sd\")"
    ]
   },
   {
@@ -105,7 +105,7 @@
    "source": [
     "sns.pointplot(\n",
     "    data=df, x=\"body_mass_g\", y=\"island\",\n",
-    "    ci=\"sd\", capsize=.4, join=False, color=\".5\",\n",
+    "    errorbar=(\"pi\", 100), capsize=.4, join=False, color=\".5\",\n",
     ")"
    ]
   },

diff --git a/doc/releases/v0.12.0.txt b/doc/releases/v0.12.0.txt
index 6beda839..463568db 100644
--- a/doc/releases/v0.12.0.txt
+++ b/doc/releases/v0.12.0.txt
@@ -20,6 +20,8 @@ This release begins the process of modernizing the :ref:`categorical plots <cate

 - |Enhancement| It is now possible to assign `hue` when using only one coordinate variable (i.e. only `x` or `y`).

+- |Enhancement| It is now possible to disable the legend.
+
 The updates also harmonize behavior with functions that have been more recently introduced. This should be relatively non-disruptive, although a few defaults will change:

 - |Defaults| The functions now hook into matplotlib's unit system for plotting categorical data. (Seaborn's categorical functions actually predate support for categorical data in matplotlib.) This should mostly be transparent to the user, but it may resolve a few edge cases. For example, matplotlib interactivity should work better (e.g., for showing the data value under the cursor).
@@ -42,14 +44,23 @@ Similar enhancements / updates should be expected to roll out to other categoric

 - |Fix| In :func:`swarmplot`, the order of the points in each swarm now matches the order in the original dataset; previously they were sorted. This affects only the underlying data stored in the matplotlib artist, not the visual representation (:pr:`2443`).

+More flexible errorbars
+~~~~~~~~~~~~~~~~~~~~~~~
+
+|API| |Feature|
+
+Increased the flexibility of what can be shown by the internally-calculated errorbars for :func:`lineplot`, :func:`barplot`, and :func:`pointplot`.
+
+With the new `errorbar` parameter, it is now possible to select bootstrap confidence intervals, percentile / predictive intervals, or intervals formed by scaled standard deviations or standard errors. The parameter also accepts an arbitrary function that maps from a vector to an interval. There is a new :doc:`user guide chapter <errorbar_tutorial>` demonstrating these options and explaining when you might want to use each one.
+
+As a consequence of this change, the `ci` parameter has been deprecated. Note that :func:`regplot` does not yet have access to this feature, but it likely will be added there in a future release (:pr:`2407`, :pr:`2866`).
+
 Other updates
 ~~~~~~~~~~~~~

-- |Feature| Increased the flexibility of what can be shown by the internally-calculated errorbars for :func:`lineplot`. With the new `errorbar` parameter, it is now possible to select bootstrap confidence intervals, percentile / predictive intervals, or intervals formed by scaled standard deviations or standard errors. As a consequence of this change, the `ci` parameter has been deprecated. Similar changes will be made to other functions that aggregate data points in future releases. (:pr:`2407`).
-
 - |Feature| It is now possible to aggregate / sort a :func:`lineplot` along the y axis using `orient="y"` (:pr:`2854`).

-- |Enhancement| Example datasets are now stored in an OS-specific cache location (as determined by `appdirs`) rather than in the user's home directory. Users should feel free to remove `~/seaborn-data` if desired (:pr:`2773`).
+- |Feature| It is now possible to specify `estimator` as a string in :func:`barplot` and :func:`pointplot`, in addition to a callable (:pr:`2866`).

 - |Enhancement| Added a `width` parameter to :func:`barplot` (:pr:`2860`).

@@ -87,6 +98,8 @@ Other updates

 - |Dependencies| Following `NEP29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_, dropped support for Python 3.6 and bumped the minimally-supported versions of the library dependencies.

+- |Dependencies| Example datasets are now stored in an OS-specific cache location (as determined by `appdirs`) rather than in the user's home directory. Users should feel free to remove `~/seaborn-data` if desired (:pr:`2773`).
+
 - |Dependencies| The unit test suite is no longer part of the source or wheel distribution. Seaborn has never had a runtime API for exercising the tests, so this should not have workflow implications (:pr:`2833`).

 - |API| Removed the previously-deprecated `factorplot` along with several previously-deprecated utility functions (`iqr`, `percentiles`, `pmf_hist`, and `sort_df`).

diff --git a/doc/tutorial/categorical.ipynb b/doc/tutorial/categorical.ipynb
index fb0a9226..5d3a2df4 100644
--- a/doc/tutorial/categorical.ipynb
+++ b/doc/tutorial/categorical.ipynb
@@ -54,23 +54,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "import numpy as np\n",
     "import seaborn as sns\n",
     "import matplotlib.pyplot as plt\n",
-    "sns.set_theme(style=\"ticks\", color_codes=True)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {
-    "tags": [
-     "hide"
-    ]
-   },
-   "outputs": [],
-   "source": [
-    "%matplotlib inline\n",
-    "import numpy as np\n",
+    "sns.set_theme(style=\"ticks\", color_codes=True)\n",
     "np.random.seed(sum(map(ord, \"categorical\")))"
    ]
   },
@@ -91,7 +78,7 @@
    "outputs": [],
    "source": [
     "tips = sns.load_dataset(\"tips\")\n",
-    "sns.catplot(x=\"day\", y=\"total_bill\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\")"
    ]
   },
   {
@@ -107,7 +94,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", jitter=False, data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", jitter=False)"
    ]
   },
   {
@@ -123,7 +110,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"swarm\")"
    ]
   },
   {
@@ -139,7 +126,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\", kind=\"swarm\")"
    ]
   },
   {
@@ -155,7 +142,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"size\", y=\"total_bill\", data=tips)"
+    "sns.catplot(data=tips.query(\"size != 3\"), x=\"size\", y=\"total_bill\")"
    ]
   },
   {
@@ -171,7 +158,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"smoker\", y=\"tip\", order=[\"No\", \"Yes\"], data=tips)"
+    "sns.catplot(data=tips, x=\"smoker\", y=\"tip\", order=[\"No\", \"Yes\"])"
    ]
   },
   {
@@ -187,7 +174,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"time\", kind=\"swarm\", data=tips)"
+    "sns.catplot(data=tips, x=\"total_bill\", y=\"day\", hue=\"time\", kind=\"swarm\")"
    ]
   },
   {
@@ -211,7 +198,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", kind=\"box\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"box\")"
    ]
   },
   {
@@ -227,7 +214,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"smoker\", kind=\"box\", data=tips)"
+    "sns.catplot(data=tips, x=\"day\", y=\"total_bill\", hue=\"smoker\", kind=\"box\")"
    ]
   },
   {
@@ -244,8 +231,10 @@
    "outputs": [],
    "source": [
     "tips[\"weekend\"] = tips[\"day\"].isin([\"Sat\", \"Sun\"])\n",
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"weekend\",\n",
-    "            kind=\"box\", dodge=False, data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"weekend\",\n",
+    "    kind=\"box\", dodge=False,\n",
+    ")"
    ]
   },
   {
@@ -262,8 +251,10 @@
    "outputs": [],
    "source": [
     "diamonds = sns.load_dataset(\"diamonds\")\n",
-    "sns.catplot(x=\"color\", y=\"price\", kind=\"boxen\",\n",
-    "            data=diamonds.sort_values(\"color\"))"
+    "sns.catplot(\n",
+    "    data=diamonds.sort_values(\"color\"),\n",
+    "    x=\"color\", y=\"price\", kind=\"boxen\",\n",
+    ")"
    ]
   },
   {
@@ -282,8 +273,9 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
-    "            kind=\"violin\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"total_bill\", y=\"day\", hue=\"sex\", kind=\"violin\",\n",
+    ")"
    ]
   },
   {
@@ -299,9 +291,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
-    "            kind=\"violin\", bw=.15, cut=0,\n",
-    "            data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"total_bill\", y=\"day\", hue=\"sex\",\n",
+    "    kind=\"violin\", bw=.15, cut=0,\n",
+    ")"
    ]
   },
   {
@@ -317,8 +310,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
-    "            kind=\"violin\", split=True, data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
+    "    kind=\"violin\", split=True,\n",
+    ")"
    ]
   },
   {
@@ -334,9 +329,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
-    "            kind=\"violin\", inner=\"stick\", split=True,\n",
-    "            palette=\"pastel\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"sex\",\n",
+    "    kind=\"violin\", inner=\"stick\", split=True, palette=\"pastel\",\n",
+    ")"
    ]
   },
   {
@@ -352,8 +348,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "g = sns.catplot(x=\"day\", y=\"total_bill\", kind=\"violin\", inner=None, data=tips)\n",
-    "sns.swarmplot(x=\"day\", y=\"total_bill\", color=\"k\", size=3, data=tips, ax=g.ax)"
+    "g = sns.catplot(data=tips, x=\"day\", y=\"total_bill\", kind=\"violin\", inner=None)\n",
+    "sns.swarmplot(data=tips, x=\"day\", y=\"total_bill\", color=\"k\", size=3, ax=g.ax)"
    ]
   },
   {
@@ -378,7 +374,23 @@
    "outputs": [],
    "source": [
     "titanic = sns.load_dataset(\"titanic\")\n",
-    "sns.catplot(x=\"sex\", y=\"survived\", hue=\"class\", kind=\"bar\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"sex\", y=\"survived\", hue=\"class\", kind=\"bar\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The default error bars show 95% confidence intervals, but (starting in v0.12), it is possible to select from a number of other representations:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sns.catplot(data=titanic, x=\"age\", y=\"deck\", errorbar=(\"pi\", 95), kind=\"bar\")"
    ]
   },
   {
@@ -394,7 +406,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"deck\", kind=\"count\", palette=\"ch:.25\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"deck\", kind=\"count\", palette=\"ch:.25\")"
    ]
   },
   {
@@ -410,9 +422,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(y=\"deck\", hue=\"class\", kind=\"count\",\n",
-    "            palette=\"pastel\", edgecolor=\".6\",\n",
-    "            data=titanic)"
+    "sns.catplot(\n",
+    "    data=titanic, y=\"deck\", hue=\"class\", kind=\"count\",\n",
+    "    palette=\"pastel\", edgecolor=\".6\",\n",
+    ")"
    ]
   },
   {
@@ -431,7 +444,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"sex\", y=\"survived\", hue=\"class\", kind=\"point\", data=titanic)"
+    "sns.catplot(data=titanic, x=\"sex\", y=\"survived\", hue=\"class\", kind=\"point\")"
    ]
   },
   {
@@ -447,10 +460,12 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"class\", y=\"survived\", hue=\"sex\",\n",
-    "            palette={\"male\": \"g\", \"female\": \"m\"},\n",
-    "            markers=[\"^\", \"o\"], linestyles=[\"-\", \"--\"],\n",
-    "            kind=\"point\", data=titanic)"
+    "sns.catplot(\n",
+    "    data=titanic, x=\"class\", y=\"survived\", hue=\"sex\",\n",
+    "    palette={\"male\": \"g\", \"female\": \"m\"},\n",
+    "    markers=[\"^\", \"o\"], linestyles=[\"-\", \"--\"],\n",
+    "    kind=\"point\"\n",
+    ")"
    ]
   },
   {
@@ -486,7 +501,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.violinplot(x=iris.species, y=iris.sepal_length)"
+    "sns.violinplot(x=iris[\"species\"], y=iris[\"sepal_length\"])"
    ]
   },
   {
@@ -503,7 +518,7 @@
    "outputs": [],
    "source": [
     "f, ax = plt.subplots(figsize=(7, 3))\n",
-    "sns.countplot(y=\"deck\", data=titanic, color=\"c\")"
+    "sns.countplot(data=titanic, y=\"deck\", color=\"c\")"
    ]
   },
   {
@@ -524,9 +539,10 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "sns.catplot(x=\"day\", y=\"total_bill\", hue=\"smoker\",\n",
-    "            col=\"time\", aspect=.7,\n",
-    "            kind=\"swarm\", data=tips)"
+    "sns.catplot(\n",
+    "    data=tips, x=\"day\", y=\"total_bill\", hue=\"smoker\",\n",
+    "    kind=\"swarm\", col=\"time\", aspect=.7,\n",
+    ")"
    ]
   },
   {
@@ -542,10 +558,17 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "g = sns.catplot(x=\"fare\", y=\"survived\", row=\"class\",\n",
-    "                kind=\"box\", orient=\"h\", height=1.5, aspect=4,\n",
-    "                data=titanic.query(\"fare > 0\"))\n",
-    "g.set(xscale=\"log\")"
+    "g = sns.catplot(\n",
+    "    data=titanic,\n",
+    "    x=\"fare\", y=\"embark_town\", row=\"class\",\n",
+    "    kind=\"box\", orient=\"h\",\n",
+    "    sharex=False, margin_titles=True,\n",
+    "    height=1.5, aspect=4,\n",
+    ")\n",
+    "g.set(xlabel=\"Fare\", ylabel=\"\")\n",
+    "g.set_titles(row_template=\"{row_name} class\")\n",
+    "for ax in g.axes.flat:\n",
+    "    ax.xaxis.set_major_formatter('${x:.0f}')"
    ]
   },
   {
@@ -554,16 +577,16 @@
    "source": [
     ".. raw:: html\n",
     "\n",
-    "    </div>"
+    "    </div>row_template="
    ]
   }
  ],
  "metadata": {
   "celltoolbar": "Tags",
   "kernelspec": {
-   "display_name": "seaborn-py38-latest",
+   "display_name": "seaborn-py39-latest",
    "language": "python",
-   "name": "seaborn-py38-latest"
+   "name": "seaborn-py39-latest"
   },
   "language_info": {
    "codemirror_mode": {
@@ -575,7 +598,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,

diff --git a/doc/tutorial/error_bars.ipynb b/doc/tutorial/error_bars.ipynb
index c8db4600..2a72dc9e 100644
--- a/doc/tutorial/error_bars.ipynb
+++ b/doc/tutorial/error_bars.ipynb
@@ -37,8 +37,7 @@
    },
    "outputs": [],
    "source": [
-    "%matplotlib inline\n",
-    "np.random.seed(sum(map(ord, \"relational\")))"
+    "np.random.seed(sum(map(ord, \"errorbars\")))"
    ]
   },
   {
@@ -48,30 +47,20 @@
     "Statistical estimation and error bars\n",
     "=====================================\n",
     "\n",
-    ".. raw:: html\n",
-    "\n",
-    "   <div class=col-md-9>\n",
-    "\n",
     "Data visualization sometimes involves a step of aggregation or estimation, where multiple data points are reduced to a summary statistic such as the mean or median. When showing a summary statistic, it is usually appropriate to add *error bars*, which provide a visual cue about how well the summary represents the underlying data points.\n",
     "\n",
     "Several seaborn functions will automatically calculate both summary statistics and the error bars when \n",
     "given a full dataset. This chapter explains how you can control what the error bars show and why you might choose each of the options that seaborn affords."
    ]
   },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# TODO example plot pointing out what the error bar is"
-   ]
-  },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "The error bars around an estimate of central tendency can show one of two things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.\n",
+    "Error bar options\n",
+    "-----------------\n",
+    "\n",
+    "The error bars around an estimate of central tendency can show one of two general things: either the range of uncertainty about the estimate or the spread of the underlying data around it. These measures are related: given the same sample size, estimates will be more uncertain when data has a broader spread. But uncertainty will decrease as sample sizes grow, whereas spread will not.\n",
     "\n",
     "In seaborn, there are two approaches for constructing each kind of error bar. One approach is parametric, using a formula that relies on assumptions about the shape of the distribution. The other approach is nonparametric, using only the data that you provide.\n",
     "\n",
@@ -88,7 +77,7 @@
    },
    "outputs": [],
    "source": [
-    "f, axs = plt.subplots(2, 2, figsize=(6, 4), sharex=True, sharey=True)\n",
+    "f, axs = plt.subplots(2, 2, figsize=(9, 5), sharex=True, sharey=True,)\n",
     "\n",
     "plt.setp(axs, xlim=(-1, 1), ylim=(-1, 1), xticks=[], yticks=[])\n",
     "for ax, color in zip(axs.flat, [\"C0\", \"C1\", \"C3\", \"C2\"]):\n",
@@ -106,9 +95,9 @@
     "axs[1, 0].text(s='errorbar=(\"pi\", width)', **kws)\n",
     "axs[1, 1].text(s='errorbar=(\"ci\", width)', **kws)\n",
     "\n",
-    "kws = dict(size=16)\n",
-    "axs[1, 0].set_xlabel(\"Spread\", **kws)\n",
-    "axs[1, 1].set_xlabel(\"Uncertainty\", **kws)\n",
+    "kws = dict(size=18)\n",
+    "axs[0, 0].set_title(\"Spread\", **kws)\n",
+    "axs[0, 1].set_title(\"Uncertainty\", **kws)\n",
     "axs[0, 0].set_ylabel(\"Parametric\", **kws)\n",
     "axs[1, 0].set_ylabel(\"Nonparametric\", **kws)\n",
     "\n",
@@ -144,7 +133,8 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "x = np.random.normal(0, 1, 500)\n",
+    "sns.pointplot(x=x, errorbar=\"sd\", capsize=.5)"
    ]
   },
   {
@@ -164,23 +154,14 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=(\"pi\", 50), capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to \"impossible\" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data:"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "# TODO demonstrate with figure"
+    "The standard deviation error bars will always be symmetrical around the estimate. This can be a problem when the data are skewed, especially if there are natural bounds (e.g., if the data represent a quantity that can only be positive). In some cases, standard deviation error bars may extend to \"impossible\" values. The nonparametric approach does not have this problem, because it can account for asymmetrical spread and will never extend beyond the range of the data."
    ]
   },
   {
@@ -195,7 +176,7 @@
     "Standard error bars\n",
     "~~~~~~~~~~~~~~~~~~~\n",
     "\n",
-    "The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar=\"se\"`, draws an interval +/- 1 standard error from the mean, but you can draw a different interval by scaling:"
+    "The standard error statistic is related to the standard deviation: in fact it is just the standard deviation divided by the square root of the sample size. The default, with `errorbar=\"se\"`, draws an interval +/-1 standard error from the mean:"
    ]
   },
   {
@@ -204,7 +185,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"se\", capsize=.5)"
    ]
   },
   {
@@ -216,7 +197,7 @@
     "\n",
     "The nonparametric approach to representing uncertainty uses *bootstrapping*: a procedure where the dataset is randomly resampled with replacement a number of times, and the estimate is recalculated from each resample. This procedure creates a distribution of statistics approximating the distribution of values that you could have gotten for your estimate if you had a different sample.\n",
     "\n",
-    "The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar=\"ci\"` draws a 95% confidence interval, but you can choose a smaller or larger one by setting a different width:"
+    "The confidence interval is constructed by taking a percentile interval of the *bootstrap distribution*. By default `errorbar=\"ci\"` draws a 95% confidence interval:"
    ]
   },
   {
@@ -225,7 +206,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"ci\", capsize=.5)"
    ]
   },
   {
@@ -241,7 +222,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=(\"se\", 2), capsize=.5)"
    ]
   },
   {
@@ -257,14 +238,30 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=\"ci\", estimator=\"median\", capsize=.5)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Bootstrapping involves randomness, and the error bars will appear slightly different each time you run the code that creates them. A few parameters control this. One sets the number of iterations (`n_boot`): with more iterations, the resulting intervals will be more stable. The other sets the `seed` for the random number generator, which will ensure identical results:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "sns.pointplot(x=x, errorbar=\"ci\", n_boot=5000, seed=10, capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "On the other hand, the bootstrap procedure is much more computationally-intensive. For large datasets, bootstrap intervals can be expensive to compute. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread."
+    "Because of its iterative process, bootstrap intervals can be expensive to compute, especially for large datasets. But because uncertainty decreases with sample size, it may be more informative in that case to use an error bar that represents data spread."
    ]
   },
   {
@@ -283,13 +280,15 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "sns.pointplot(x=x, errorbar=lambda x: (x.min(), x.max()), capsize=.5)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
+    "(In practice, you could show the full range of the data with `errorbar=(\"pi\", 100)` rather than the custom function shown above).\n",
+    "\n",
     "Note that seaborn functions cannot currently draw error bars from values that have been calculated externally, although matplotlib functions can be used to add such error bars to seaborn plots."
    ]
   },
@@ -309,14 +308,16 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "# TODO demonstrate with figure"
+    "x = np.random.normal(0, 1, 50)\n",
+    "y = x * 2 + np.random.normal(0, 2, size=x.size)\n",
+    "sns.regplot(x=x, y=y)"
    ]
   },
   {
    "cell_type": "raw",
    "metadata": {},
    "source": [
-    "Most of the same options apply. A regression line represents a conditional mean: the estimated average value of the *y* variable given a specific *x* value. So a standard error or confidence interval represents the uncertainty around the estimate of that conditional mean, whereas a standard deviation interval represents a prediction about the range of *y* would would see if given that *x*. Because the regression model extrapolates beyond the data at hand, it is not possible to draw a percentile interval around it."
+    "Currently, the error bars on a regression estimate are less flexible, only showing a confidence interval with a size set through `ci=`. This may change in the future."
    ]
   },
   {
@@ -336,9 +337,9 @@
  ],
  "metadata": {
   "kernelspec": {
-   "display_name": "seaborn-py38-latest",
+   "display_name": "seaborn-py39-latest",
    "language": "python",
-   "name": "seaborn-py38-latest"
+   "name": "seaborn-py39-latest"
   },
   "language_info": {
    "codemirror_mode": {
@@ -350,7 +351,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.9.9"
   }
  },
  "nbformat": 4,