Skip to content

Commit cc92ad4

Browse files
authored
Merge pull request #24 from tdhopper/add-lets-plot
Add Lets-Plot column to the plotting comparison
2 parents b67bc5c + 32d2273 commit cc92ad4

7 files changed

Lines changed: 339 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
pythonplot.com is a static website that provides a visual comparison of different Python plotting libraries (pandas, matplotlib, seaborn, plotnine, plotly, altair) and R's ggplot2 for exploratory data analysis. It serves as a "Rosetta Stone" showing how to create the same plots across different libraries.
7+
pythonplot.com is a static website that provides a visual comparison of different Python plotting libraries (pandas, matplotlib, seaborn, plotnine, lets-plot, plotly, altair) and R's ggplot2 for exploratory data analysis. It serves as a "Rosetta Stone" showing how to create the same plots across different libraries.
88

99
## Architecture
1010

@@ -115,7 +115,7 @@ All plots are rendered to static PNG images inside the executed notebook:
115115

116116
## Dependencies
117117

118-
Python dependencies are declared in `pyproject.toml` and locked in `uv.lock` (committed). Key libraries: pandas, matplotlib, seaborn, plotnine, plotly (+kaleido), altair (+vl-convert-python), statsmodels, rpy2, Jinja2 with jinja2-highlight.
118+
Python dependencies are declared in `pyproject.toml` and locked in `uv.lock` (committed). Key libraries: pandas, matplotlib, seaborn, plotnine, lets-plot, plotly (+kaleido), altair (+vl-convert-python), statsmodels, rpy2, Jinja2 with jinja2-highlight.
119119

120120
R (system install) with ggplot2 and mgcv, installed by `setup_r.sh`.
121121

Examples.ipynb

Lines changed: 241 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"execution_count": null,
66
"metadata": {},
77
"outputs": [],
8-
"source": "%matplotlib inline\n\nimport readline\nimport altair as alt\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib import pyplot\nfrom plotnine import *\nimport numpy as np\n\nfrom plotly import figure_factory\nfrom plotly import graph_objects\nimport plotly.express as px\nfrom IPython.core.magic import Magics, magics_class, cell_magic\n\nfrom IPython.display import Image\n\nfrom pylab import rcParams\n\nsize = 20\nparams = {\n \"legend.fontsize\": size,\n \"figure.figsize\": (15, 5),\n \"axes.labelsize\": size,\n \"axes.titlesize\": size,\n \"xtick.labelsize\": size,\n \"ytick.labelsize\": size,\n \"axes.titlesize\": 1.5 * size,\n \"figure.figsize\": (12, 12),\n}\nrcParams.update(params)\ntheme_update(\n figure_size=(9, 9),\n title=element_text(size=size),\n text=element_text(size=0.6 * size),\n) # for plotnine\n\n\nimport plotly.io as pio\npio.renderers.default = \"png\"\npio.renderers[\"png\"].width = 750\npio.renderers[\"png\"].height = 750\n\n# Render Altair charts as PNG via vl-convert\nalt.renderers.enable(\"png\", scale_factor=2.0)"
8+
"source": "%matplotlib inline\n\nimport readline\nimport altair as alt\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib import pyplot\nfrom plotnine import *\nimport numpy as np\n\nfrom plotly import figure_factory\nfrom plotly import graph_objects\nimport plotly.express as px\nfrom IPython.core.magic import Magics, magics_class, cell_magic\n\nfrom IPython.display import Image\n\nfrom pylab import rcParams\n\nsize = 20\nparams = {\n \"legend.fontsize\": size,\n \"figure.figsize\": (15, 5),\n \"axes.labelsize\": size,\n \"axes.titlesize\": size,\n \"xtick.labelsize\": size,\n \"ytick.labelsize\": size,\n \"axes.titlesize\": 1.5 * size,\n \"figure.figsize\": (12, 12),\n}\nrcParams.update(params)\ntheme_update(\n figure_size=(9, 9),\n title=element_text(size=size),\n text=element_text(size=0.6 * size),\n) # for plotnine\n\n\nimport plotly.io as pio\npio.renderers.default = \"png\"\npio.renderers[\"png\"].width = 750\npio.renderers[\"png\"].height = 750\n\n# Render Altair charts as PNG via vl-convert\nalt.renderers.enable(\"png\", scale_factor=2.0)\n\nimport tempfile\n\nimport lets_plot as lp\nfrom lets_plot.export import ggsave\n\nlp.LetsPlot.setup_html()\nlp.LetsPlot.set_theme(\n lp.theme(text=lp.element_text(size=16), title=lp.element_text(size=20))\n)\n\n_LETS_PLOT_DIR = tempfile.mkdtemp(prefix=\"lets-plot-\")\n\n\ndef _lets_plot_png(plot):\n \"\"\"Render a Lets-Plot spec to PNG so the cell output carries an image.\"\"\"\n path = ggsave(plot + lp.ggsize(750, 750), \"plot.png\", path=_LETS_PLOT_DIR)\n with open(path, \"rb\") as f:\n return f.read()\n\n\nget_ipython().display_formatter.formatters[\"image/png\"].for_type(\n lp.plot.core.PlotSpec, _lets_plot_png\n)"
99
},
1010
{
1111
"cell_type": "code",
@@ -227,6 +227,28 @@
227227
")"
228228
]
229229
},
230+
{
231+
"cell_type": "code",
232+
"execution_count": null,
233+
"metadata": {
234+
"tags": [
235+
"ex",
236+
"name:bar-counts",
237+
"package:lets-plot"
238+
]
239+
},
240+
"outputs": [],
241+
"source": [
242+
"\"\"\"Lets-Plot mirrors the ggplot2 grammar. It is imported as `lp` here\n",
243+
"so its names don't collide with plotnine's.\n",
244+
"\"\"\"\n",
245+
"(lp.ggplot(mpg) +\n",
246+
" lp.aes(x=\"manufacturer\") +\n",
247+
" lp.geom_bar() +\n",
248+
" lp.coord_flip() +\n",
249+
" lp.ggtitle(\"Number of Cars by Make\"))"
250+
]
251+
},
230252
{
231253
"cell_type": "code",
232254
"execution_count": null,
@@ -344,6 +366,23 @@
344366
" geom_histogram(binwidth=2))"
345367
]
346368
},
369+
{
370+
"cell_type": "code",
371+
"execution_count": null,
372+
"metadata": {
373+
"tags": [
374+
"ex",
375+
"name:simple-histogram",
376+
"package:lets-plot"
377+
]
378+
},
379+
"outputs": [],
380+
"source": [
381+
"(lp.ggplot(mpg) +\n",
382+
" lp.aes(x=\"cty\") +\n",
383+
" lp.geom_histogram(binwidth=2))"
384+
]
385+
},
347386
{
348387
"cell_type": "code",
349388
"execution_count": null,
@@ -452,6 +491,26 @@
452491
" ylab(\"Highway MPG\"))"
453492
]
454493
},
494+
{
495+
"cell_type": "code",
496+
"execution_count": null,
497+
"metadata": {
498+
"tags": [
499+
"ex",
500+
"name:scatter-plot",
501+
"package:lets-plot"
502+
]
503+
},
504+
"outputs": [],
505+
"source": [
506+
"(lp.ggplot(mpg) +\n",
507+
" lp.aes(x=\"displ\", y=\"hwy\") +\n",
508+
" lp.geom_point() +\n",
509+
" lp.ggtitle(\"Engine Displacement in Liters vs Highway MPG\") +\n",
510+
" lp.xlab(\"Engine Displacement in Liters\") +\n",
511+
" lp.ylab(\"Highway MPG\"))"
512+
]
513+
},
455514
{
456515
"cell_type": "code",
457516
"execution_count": null,
@@ -543,6 +602,24 @@
543602
" geom_smooth(method=\"lm\"))"
544603
]
545604
},
605+
{
606+
"cell_type": "code",
607+
"execution_count": null,
608+
"metadata": {
609+
"tags": [
610+
"ex",
611+
"name:scatter-with-regression",
612+
"package:lets-plot"
613+
]
614+
},
615+
"outputs": [],
616+
"source": [
617+
"(lp.ggplot(mpg) +\n",
618+
" lp.aes(\"displ\", \"hwy\") +\n",
619+
" lp.geom_point() +\n",
620+
" lp.geom_smooth(method=\"lm\"))"
621+
]
622+
},
546623
{
547624
"cell_type": "code",
548625
"execution_count": null,
@@ -762,6 +839,26 @@
762839
" ylab(\"Highway MPG\"))"
763840
]
764841
},
842+
{
843+
"cell_type": "code",
844+
"execution_count": null,
845+
"metadata": {
846+
"tags": [
847+
"ex",
848+
"name:scatter-plot-with-colors",
849+
"package:lets-plot"
850+
]
851+
},
852+
"outputs": [],
853+
"source": [
854+
"(lp.ggplot(mpg) +\n",
855+
" lp.aes(x=\"displ\", y=\"hwy\", color=\"class\") +\n",
856+
" lp.geom_point() +\n",
857+
" lp.ggtitle(\"Engine Displacement in Liters vs Highway MPG\") +\n",
858+
" lp.xlab(\"Engine Displacement in Liters\") +\n",
859+
" lp.ylab(\"Highway MPG\"))"
860+
]
861+
},
765862
{
766863
"cell_type": "code",
767864
"execution_count": null,
@@ -883,6 +980,23 @@
883980
" geom_point(alpha=.5))"
884981
]
885982
},
983+
{
984+
"cell_type": "code",
985+
"execution_count": null,
986+
"metadata": {
987+
"tags": [
988+
"ex",
989+
"name:scatter-plot-with-size",
990+
"package:lets-plot"
991+
]
992+
},
993+
"outputs": [],
994+
"source": [
995+
"(lp.ggplot(mpg) +\n",
996+
" lp.aes(x=\"cty\", y=\"hwy\", size=\"cyl\") +\n",
997+
" lp.geom_point(alpha=.5))"
998+
]
999+
},
8861000
{
8871001
"cell_type": "code",
8881002
"execution_count": null,
@@ -989,6 +1103,24 @@
9891103
" facet_wrap(\" ~ c\", nrow = 2))"
9901104
]
9911105
},
1106+
{
1107+
"cell_type": "code",
1108+
"execution_count": null,
1109+
"metadata": {
1110+
"tags": [
1111+
"ex",
1112+
"name:scatter-plot-with-facet",
1113+
"package:lets-plot"
1114+
]
1115+
},
1116+
"outputs": [],
1117+
"source": [
1118+
"(lp.ggplot(mpg) +\n",
1119+
" lp.aes(x=\"displ\", y=\"hwy\") +\n",
1120+
" lp.geom_point() +\n",
1121+
" lp.facet_wrap(facets=\"class\", nrow=2))"
1122+
]
1123+
},
9921124
{
9931125
"cell_type": "code",
9941126
"execution_count": null,
@@ -1097,6 +1229,24 @@
10971229
" facet_grid(\"drv ~ cyl\"))"
10981230
]
10991231
},
1232+
{
1233+
"cell_type": "code",
1234+
"execution_count": null,
1235+
"metadata": {
1236+
"tags": [
1237+
"ex",
1238+
"name:scatter-plot-with-facets",
1239+
"package:lets-plot"
1240+
]
1241+
},
1242+
"outputs": [],
1243+
"source": [
1244+
"(lp.ggplot(mpg) +\n",
1245+
" lp.aes(x=\"displ\", y=\"hwy\") +\n",
1246+
" lp.geom_point() +\n",
1247+
" lp.facet_grid(x=\"cyl\", y=\"drv\"))"
1248+
]
1249+
},
11001250
{
11011251
"cell_type": "code",
11021252
"execution_count": null,
@@ -1214,6 +1364,26 @@
12141364
" ))"
12151365
]
12161366
},
1367+
{
1368+
"cell_type": "code",
1369+
"execution_count": null,
1370+
"metadata": {
1371+
"tags": [
1372+
"ex",
1373+
"name:stacked-smooth-line-and-scatter",
1374+
"package:lets-plot"
1375+
]
1376+
},
1377+
"outputs": [],
1378+
"source": [
1379+
"(lp.ggplot(mpg) +\n",
1380+
" lp.aes(x=\"displ\", y=\"hwy\") +\n",
1381+
" lp.geom_point(lp.aes(color=\"class\")) +\n",
1382+
" lp.geom_smooth(data=mpg[mpg[\"class\"] == \"subcompact\"],\n",
1383+
" se=False,\n",
1384+
" method=\"loess\"))"
1385+
]
1386+
},
12171387
{
12181388
"cell_type": "code",
12191389
"execution_count": null,
@@ -1301,6 +1471,23 @@
13011471
" geom_bar())"
13021472
]
13031473
},
1474+
{
1475+
"cell_type": "code",
1476+
"execution_count": null,
1477+
"metadata": {
1478+
"tags": [
1479+
"ex",
1480+
"name:stacked-bar-chart",
1481+
"package:lets-plot"
1482+
]
1483+
},
1484+
"outputs": [],
1485+
"source": [
1486+
"(lp.ggplot(diamonds) +\n",
1487+
" lp.aes(x=\"cut\", fill=\"clarity\") +\n",
1488+
" lp.geom_bar())"
1489+
]
1490+
},
13041491
{
13051492
"cell_type": "code",
13061493
"execution_count": null,
@@ -1415,6 +1602,23 @@
14151602
" geom_bar(position = \"dodge\"))\n"
14161603
]
14171604
},
1605+
{
1606+
"cell_type": "code",
1607+
"execution_count": null,
1608+
"metadata": {
1609+
"tags": [
1610+
"ex",
1611+
"name:dodged-bar-chart",
1612+
"package:lets-plot"
1613+
]
1614+
},
1615+
"outputs": [],
1616+
"source": [
1617+
"(lp.ggplot(diamonds) +\n",
1618+
" lp.aes(x=\"cut\", fill=\"clarity\") +\n",
1619+
" lp.geom_bar(position=\"dodge\"))"
1620+
]
1621+
},
14181622
{
14191623
"cell_type": "code",
14201624
"execution_count": null,
@@ -1530,6 +1734,24 @@
15301734
" geom_density(alpha=0.1))"
15311735
]
15321736
},
1737+
{
1738+
"cell_type": "code",
1739+
"execution_count": null,
1740+
"metadata": {
1741+
"tags": [
1742+
"ex",
1743+
"name:stacked-kde",
1744+
"package:lets-plot"
1745+
]
1746+
},
1747+
"outputs": [],
1748+
"source": [
1749+
"(lp.ggplot(diamonds) +\n",
1750+
" lp.aes(\"depth\", fill=\"cut\", color=\"cut\") +\n",
1751+
" lp.geom_density(alpha=0.1) +\n",
1752+
" lp.xlim(55, 70))"
1753+
]
1754+
},
15331755
{
15341756
"cell_type": "code",
15351757
"execution_count": null,
@@ -1614,6 +1836,23 @@
16141836
" + geom_line())"
16151837
]
16161838
},
1839+
{
1840+
"cell_type": "code",
1841+
"execution_count": null,
1842+
"metadata": {
1843+
"tags": [
1844+
"ex",
1845+
"name:timeseries",
1846+
"package:lets-plot"
1847+
]
1848+
},
1849+
"outputs": [],
1850+
"source": [
1851+
"(lp.ggplot(ts)\n",
1852+
" + lp.aes(\"date\", \"value\")\n",
1853+
" + lp.geom_line())"
1854+
]
1855+
},
16171856
{
16181857
"cell_type": "code",
16191858
"execution_count": null,
@@ -1686,4 +1925,4 @@
16861925
},
16871926
"nbformat": 4,
16881927
"nbformat_minor": 1
1689-
}
1928+
}

INTRO.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ There are several tools that can make the kinds of plots described here. At pres
2929

3030
"[plotly](https://plot.ly/ "Plotly - Make charts and dashboards online")'s Python graphing library makes interactive, publication-quality graphs online. Examples of how to make line plots, scatter plots, area charts, bar charts, error bars, box plots, histograms, heatmaps, subplots, multiple-axes, polar charts, and bubble charts." I provide plotly examples rendered as static images.
3131

32+
"[Lets-Plot](https://lets-plot.org/ "Lets-Plot: an open-source plotting library for statistical data") is an open-source plotting library for statistical data," written by JetBrains and modeled on the grammar of graphics. Its Python API tracks ggplot2 closely enough that most of the examples below translate line for line. I provide Lets-Plot examples rendered as static images.
33+
3234
"[Bokeh](http://bokeh.pydata.org/en/latest/ "Python interactive visualization library") is a Python interactive visualization library that targets modern web browsers for presentation."
3335

3436
"[bqplot](https://github.com/bloomberg/bqplot) is a Grammar of Graphics-based interactive plotting framework for the Jupyter notebook."

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
"matplotlib>=3.7",
1919
"seaborn>=0.13",
2020
"plotnine>=0.13",
21+
"lets-plot>=4.11",
2122
"plotly>=5.24",
2223
"altair>=5.0",
2324
"vl-convert-python>=1.0", # Required for Altair PNG export

render.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"matplotlib": "Matplotlib",
2222
"seaborn": "Seaborn",
2323
"plotnine": "plotnine",
24+
"lets-plot": "lets-plot",
2425
"plotly": "plotly",
2526
"altair": "Altair",
2627
"ggplot": "ggplot2 (R)",

0 commit comments

Comments
 (0)