From a7409d01b9bac9df22d59bfa0f1c95a97c2be1ad Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:39:16 -0400 Subject: [PATCH 1/3] Add Box Plot and Heatmap rows to the comparison Box plot shows hwy by class from mpg; heatmap shows diamonds counts by cut and clarity. seaborn.objects is skipped for both: 0.13 has no boxplot mark and no rect mark, and pandas has no heatmap. --- Examples.ipynb | 346 ++++++++++++++++++++++++++++++++++++++++++++ render.py | 2 + tests/test_plots.py | 21 +++ 3 files changed, 369 insertions(+) diff --git a/Examples.ipynb b/Examples.ipynb index 9b3948f..8e0f098 100644 --- a/Examples.ipynb +++ b/Examples.ipynb @@ -698,6 +698,174 @@ "sns.histplot(mpg, x='cty', binwidth=2);" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Box Plot of a Continuous Value by Category" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:ggplot" + ] + }, + "outputs": [], + "source": [ + "%%R -w 10 -h 10 -u in\n", + "ggplot(data=mpg) +\n", + " aes(x=class, y=hwy) +\n", + " geom_boxplot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:pandas" + ] + }, + "outputs": [], + "source": [ + "mpg.boxplot(column='hwy', by='class',\n", + " rot=45);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:plotnine" + ] + }, + "outputs": [], + "source": [ + "(ggplot(mpg) +\n", + " aes(x=\"class\", y=\"hwy\") +\n", + " geom_boxplot())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:lets-plot" + ] + }, + "outputs": [], + "source": [ + "(lp.ggplot(mpg) +\n", + " lp.aes(x=\"class\", y=\"hwy\") +\n", + " lp.geom_boxplot())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:plotly" + ] + }, + "outputs": [], + "source": [ + "px.box(\n", + " mpg, x=\"class\", y=\"hwy\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:altair" + ] + }, + "outputs": [], + "source": [ + "(\n", + " alt.Chart(mpg)\n", + " .mark_boxplot()\n", + " .encode(x=\"class\", y=\"hwy\")\n", + " .properties(width=400)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:hvplot" + ] + }, + "outputs": [], + "source": [ + "mpg.hvplot.box(y=\"hwy\", by=\"class\",\n", + " rot=45)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:matplotlib" + ] + }, + "outputs": [], + "source": [ + "\"\"\"`boxplot` takes a list of arrays, so the\n", + "groups are split by hand.\n", + "\"\"\"\n", + "groups = mpg.groupby('class')['hwy']\n", + "fig, ax = pyplot.subplots()\n", + "ax.boxplot([v for _, v in groups],\n", + " tick_labels=list(groups.groups))\n", + "ax.set_xlabel('class')\n", + "ax.set_ylabel('hwy')\n", + "pyplot.xticks(rotation=45);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:box-plot", + "package:seaborn" + ] + }, + "outputs": [], + "source": [ + "ax = sns.boxplot(mpg, x='class', y='hwy')\n", + "ax.tick_params(axis='x', rotation=45);" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -2855,6 +3023,184 @@ "source": [ "sns.lineplot(ts, x='date', y='value');" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Heatmap of Counts by Two Categories" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:ggplot" + ] + }, + "outputs": [], + "source": [ + "%%R -w 10 -h 10 -u in\n", + "ggplot(data=diamonds) +\n", + " aes(x=cut, y=clarity) +\n", + " geom_bin2d()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:plotnine" + ] + }, + "outputs": [], + "source": [ + "(ggplot(diamonds) +\n", + " aes(x=\"cut\", y=\"clarity\") +\n", + " geom_bin2d())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:lets-plot" + ] + }, + "outputs": [], + "source": [ + "(lp.ggplot(diamonds) +\n", + " lp.aes(x=\"cut\", y=\"clarity\") +\n", + " lp.geom_bin2d())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:plotly" + ] + }, + "outputs": [], + "source": [ + "px.density_heatmap(\n", + " diamonds, x=\"cut\", y=\"clarity\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:altair" + ] + }, + "outputs": [], + "source": [ + "alt.data_transformers.disable_max_rows()\n", + "(\n", + " alt.Chart(diamonds)\n", + " .mark_rect()\n", + " .encode(\n", + " x=\"cut\", y=\"clarity\",\n", + " color=\"count()\",\n", + " )\n", + " .properties(width=400, height=400)\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:hvplot" + ] + }, + "outputs": [], + "source": [ + "\"\"\"hvPlot needs the counts up front; `C` names\n", + "the column that colors each tile.\n", + "\"\"\"\n", + "(diamonds\n", + " .groupby([\"cut\", \"clarity\"])\n", + " .size()\n", + " .reset_index(name=\"count\")\n", + " .hvplot.heatmap(x=\"cut\", y=\"clarity\",\n", + " C=\"count\", rot=45))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:matplotlib" + ] + }, + "outputs": [], + "source": [ + "\"\"\"Matplotlib has no heatmap function, so the\n", + "counts are pivoted and drawn with `pcolormesh`.\n", + "\"\"\"\n", + "counts = (diamonds\n", + " .groupby(['clarity', 'cut'])\n", + " .size()\n", + " .unstack())\n", + "fig, ax = pyplot.subplots()\n", + "mesh = ax.pcolormesh(counts.values)\n", + "ax.set_xticks(\n", + " np.arange(len(counts.columns)) + .5,\n", + " counts.columns, rotation=45)\n", + "ax.set_yticks(\n", + " np.arange(len(counts.index)) + .5,\n", + " counts.index)\n", + "ax.set_xlabel('cut')\n", + "ax.set_ylabel('clarity')\n", + "fig.colorbar(mesh, label='count');" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [ + "ex", + "name:heatmap", + "package:seaborn" + ] + }, + "outputs": [], + "source": [ + "\"\"\"`heatmap` colors a matrix, so the counts are\n", + "pivoted into one first.\n", + "\"\"\"\n", + "counts = (diamonds\n", + " .groupby(['clarity', 'cut'])\n", + " .size()\n", + " .unstack())\n", + "sns.heatmap(counts,\n", + " cbar_kws=dict(label='count'));" + ] } ], "metadata": { diff --git a/render.py b/render.py index 8f2dc57..cbe12d4 100644 --- a/render.py +++ b/render.py @@ -32,6 +32,7 @@ names = { "bar-counts": "Bar Chart", "simple-histogram": "Histogram", + "box-plot": "Box Plot", "scatter-plot": "Scatter Plot", "timeseries": "Time Series", "scatter-plot-with-colors": "Scatter Plot with Faceted with Color", @@ -43,6 +44,7 @@ "stacked-bar-chart": "Stacked Bar Chart", "dodged-bar-chart": "Dodged Bar Chart", "stacked-kde": "Stacked KDE Plot", + "heatmap": "Heatmap", } with open("INTRO.md", "r") as f: diff --git a/tests/test_plots.py b/tests/test_plots.py index 4c067ca..b6aec6c 100644 --- a/tests/test_plots.py +++ b/tests/test_plots.py @@ -14,6 +14,17 @@ 'altair', 'ggplot', ], + 'box-plot': [ + 'pandas', + 'matplotlib', + 'seaborn', + 'plotnine', + 'lets-plot', + 'plotly', + 'hvplot', + 'altair', + 'ggplot', + ], 'dodged-bar-chart': [ 'pandas', 'matplotlib', @@ -26,6 +37,16 @@ 'altair', 'ggplot', ], + 'heatmap': [ + 'matplotlib', + 'seaborn', + 'plotnine', + 'lets-plot', + 'plotly', + 'hvplot', + 'altair', + 'ggplot', + ], 'scatter-plot': [ 'pandas', 'matplotlib', From 48bbc092ed45048cbc549ea1972a7731619fafd7 Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:39:58 -0400 Subject: [PATCH 2/3] Update the intro's per-library coverage counts for the new rows --- INTRO.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/INTRO.md b/INTRO.md index 001c8a9..bb93238 100644 --- a/INTRO.md +++ b/INTRO.md @@ -14,11 +14,11 @@ Put the Matplotlib and ggplot2 versions of the two-variable faceted scatter plot #### Matplotlib-Based Libraries -[Pandas plotting](https://pandas.pydata.org/docs/user_guide/visualization.html "pandas user guide: Chart Visualization") provides "the basics ... to easily create decent looking plots" from data frames. That is about 70% of what I do day-to-day. It has no faceting, no categorical color mapping, and no smoothing, so five of the examples below have no pandas column. +[Pandas plotting](https://pandas.pydata.org/docs/user_guide/visualization.html "pandas user guide: Chart Visualization") provides "the basics ... to easily create decent looking plots" from data frames. That is about 70% of what I do day-to-day. It has no faceting, no categorical color mapping, no smoothing, and no heatmap, so six of the examples below have no pandas column. Seaborn calls itself "[statistical data visualization](https://seaborn.pydata.org/ "seaborn: statistical data visualization")." Its classic interface is a set of named functions (`histplot`, `scatterplot`, `countplot`, `lmplot`, `kdeplot`) plus [FacetGrid](http://seaborn.pydata.org/tutorial/axis_grids.html), which I use for faceting more than anything else in the library. It covers every plot below, once you know which function to reach for. -Seaborn 0.12 added [seaborn.objects](https://seaborn.pydata.org/tutorial/objects_interface.html), a second interface built on the grammar of graphics. It composes a plot from marks and statistical transforms instead of dispatching to a named plotting function. The interface has no loess smoother and no regression confidence band, so those two examples are missing. +Seaborn 0.12 added [seaborn.objects](https://seaborn.pydata.org/tutorial/objects_interface.html), a second interface built on the grammar of graphics. It composes a plot from marks and statistical transforms instead of dispatching to a named plotting function. The interface has no loess smoother, no regression confidence band, and neither a box-plot nor a rectangle mark, so four of the examples are missing. "[plotnine](https://plotnine.org/) is a data visualization package for Python based on the grammar of graphics." It tracks ggplot2 closely enough that most R code translates line for line, down to the `+` for layering. I reach for it when I want ggplot2 semantics without leaving Python. From 84f2022d5b099de552d5f4eec2c219694fe1eaa5 Mon Sep 17 00:00:00 2001 From: Tim Hopper Date: Tue, 4 Aug 2026 21:45:12 -0400 Subject: [PATCH 3/3] Draw the heatmap row with geom_tile on precomputed counts ggplot2's geom_bin2d bins discrete positions with a continuous binwidth, so the R heatmap came out as tiny disconnected tiles. Counting first and drawing geom_tile is the idiomatic recipe and matches the pivot-then-draw approach the matplotlib, seaborn, and hvPlot columns already take. Also stop seaborn from rotating the clarity labels. --- Examples.ipynb | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Examples.ipynb b/Examples.ipynb index 8e0f098..31185e4 100644 --- a/Examples.ipynb +++ b/Examples.ipynb @@ -3044,9 +3044,13 @@ "outputs": [], "source": [ "%%R -w 10 -h 10 -u in\n", - "ggplot(data=diamonds) +\n", - " aes(x=cut, y=clarity) +\n", - " geom_bin2d()" + "counts <- as.data.frame(\n", + " table(cut=diamonds$cut,\n", + " clarity=diamonds$clarity),\n", + " responseName=\"count\")\n", + "ggplot(data=counts) +\n", + " aes(x=cut, y=clarity, fill=count) +\n", + " geom_tile()" ] }, { @@ -3061,9 +3065,13 @@ }, "outputs": [], "source": [ - "(ggplot(diamonds) +\n", - " aes(x=\"cut\", y=\"clarity\") +\n", - " geom_bin2d())" + "counts = (diamonds\n", + " .groupby([\"cut\", \"clarity\"])\n", + " .size()\n", + " .reset_index(name=\"count\"))\n", + "(ggplot(counts) +\n", + " aes(x=\"cut\", y=\"clarity\", fill=\"count\") +\n", + " geom_tile())" ] }, { @@ -3078,9 +3086,14 @@ }, "outputs": [], "source": [ - "(lp.ggplot(diamonds) +\n", - " lp.aes(x=\"cut\", y=\"clarity\") +\n", - " lp.geom_bin2d())" + "counts = (diamonds\n", + " .groupby([\"cut\", \"clarity\"])\n", + " .size()\n", + " .reset_index(name=\"count\"))\n", + "(lp.ggplot(counts) +\n", + " lp.aes(x=\"cut\", y=\"clarity\",\n", + " fill=\"count\") +\n", + " lp.geom_tile())" ] }, { @@ -3198,8 +3211,9 @@ " .groupby(['clarity', 'cut'])\n", " .size()\n", " .unstack())\n", - "sns.heatmap(counts,\n", - " cbar_kws=dict(label='count'));" + "ax = sns.heatmap(\n", + " counts, cbar_kws=dict(label='count'))\n", + "ax.tick_params(axis='y', rotation=0);" ] } ],