Displaying Python plots from Clojure
This is part of the Scicloj Clojure Data Scrapbook. |
This notebook demonstrates a self-contained workflow for visualizing Python plots in current Clojure tooling using the Kindly convention.
The only dependency necessary is the Libpython-clj bridge. Some Kindly-compatible tool is needed to make the visualization visible. This was rendered using Clay as an extra dev dependency.
The implementation is inspired by the Parens for Pyplot tutorial by Carin Meier from Jan 2020. It has been part of the Noj library till version 1-alpha34
, but as of July 2024, it is part of a dedicated library, Kind-pyplot.
Setup
Let us require the relevant namespaces from Libpyton-clj:
ns index
(:require [libpython-clj2.require :refer [require-python]]
(:refer [py. py.. py.-] :as py])) [libpython-clj2.python
Now we can require the relevant Python modules from Matplotlib:
'matplotlib.pyplot
(require-python 'matplotlib.backends.backend_agg)
:ok
Implementation
defmacro with-pyplot
("Takes forms with mathplotlib.pyplot and returns a showable (SVG) plot.
E.g.:
(with-pyplot
(matplotlib.pyplot/plot
[1 2 3]
[1 4 9]))
"
[& forms]let [_# (matplotlib.pyplot/clf)
`(fig# (matplotlib.pyplot/figure)
agg-canvas# (matplotlib.backends.backend_agg/FigureCanvasAgg fig#)
path# (.getAbsolutePath
"plot-" ".svg"))]
(java.io.File/createTempFile cons 'do forms)
~(agg-canvas# "draw")
(py. path#)
(matplotlib.pyplot/savefig ;; Take the SVG file path and turn it into
;; a Clojure value that can be displayed in Kindly-compatible tools.
-> path#
(slurp
vector
with-meta {:kindly/kind :kind/html})))) (
defn pyplot
("Takes a function plotting using mathplotlib.pyplot, and returns a showable (SVG) plot.
E.g.:
(pyplot
#(matplotlib.pyplot/plot
[1 2 3]
[1 4 9]))
"
[plotting-function]
(with-pyplot (plotting-function)))
Examples
From the Parens for Pyplot blogpost:
:as np]) (require-python '[numpy
:ok
require '[clojure.math :as math]) (
def sine-data
(let [x (range 0 (* 3 np/pi) 0.1)]
(-> {:x (vec x)
(:y (mapv math/sin x)})))
(with-pyplot
(matplotlib.pyplot/plot:x sine-data)
(:y sine-data))) (
(pyplot
#(matplotlib.pyplot/plot:x sine-data)
(:y sine-data))) (
From the Seaborn intro:
:as sns]) (require-python '[seaborn
:ok
let [tips (sns/load_dataset "tips")]
(
(sns/set_theme)
(pyplot:data tips
#(sns/relplot :x "total_bill"
:y "tip"
:col "time"
:hue "smoker"
:style "smoker"
:size "size")))