Skip to content

Commit 5804fa5

Browse files
committed
Add another matplotlib tutorial
1 parent a1b1d47 commit 5804fa5

4 files changed

Lines changed: 296 additions & 3 deletions

File tree

_docs_src/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ nav:
99
- Examples & Tutorials:
1010
- Getting Started: getting-started.md
1111
- Matplotlib Example: matplotlib.md
12+
- Matplotlib Example 2: matplotlib-2.md
1213
- Rules:
1314
- Types: types.md
1415
- Function & Argument Names: names.md

_docs_src/src/img/brown_plot.png

18.6 KB
Loading

_docs_src/src/matplotlib-2.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
---
2+
description: In this example, we bind a multiple matplotlib Python classes for plotting line charts.
3+
---
4+
5+
# Another matplotlib example
6+
7+
Let's take another look at [matplotlib](https://matplotlib.org/). This one will be a little different in that we will generate direct bindings for a couple of matplotlib classes and module functions.
8+
9+
* [Axes](https://matplotlib.org/stable/api/axes_api.html#matplotlib.axes.Axes)
10+
* [Figure](https://matplotlib.org/stable/api/figure_api.html#module-matplotlib.figure)
11+
* [matplotlib.pyplot.subplots](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.subplots.html#matplotlib.pyplot.subplots)
12+
13+
This example will also show you some of the current limitations of `pyml_bindgen` :)
14+
15+
## Value specs
16+
17+
For this example, we won't bother binding all the arguments that these methods take since we won't be using them.
18+
19+
For each of these, I will put down the arguments as shown in the matplotlib docs, the follow it with the OCaml value spec we will use.
20+
21+
### `Axes.set_title`
22+
23+
([docs](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.set_title.html#matplotlib.axes.Axes.set_title))
24+
25+
Python:
26+
27+
```
28+
Axes.set_title(label, fontdict=None, loc=None, pad=None, *, y=None, **kwargs)
29+
```
30+
31+
OCaml:
32+
33+
```ocaml
34+
val set_title : t -> label:string -> unit -> unit
35+
```
36+
37+
### `Axes.plot`
38+
39+
([docs](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html#matplotlib.axes.Axes.plot))
40+
41+
Python:
42+
43+
```
44+
Axes.plot(*args, scalex=True, scaley=True, data=None, **kwargs)
45+
```
46+
47+
OCaml:
48+
49+
```ocaml
50+
val plot : t -> x:float list -> y:float list -> ?color:string -> unit -> unit
51+
```
52+
53+
This value spec will generate an `Axes.plot` function actually has a bug. If you check the docs, you can't actually pass `x` and `y` as keyword arguments. Oops! You have to go in and edit the binding by hand. Below, I will show a [patch file](TODO) with the changes you need to make.
54+
55+
You may be thinking, well, that's pretty annoying...I agree! For this function, I would probably just write it by hand from the start. I'm showing it here partly as a reminder that I want to change the behaviour of `pyml_bindgen` in a future release to handle methods like this one. But for now, you have to deal with it yourself :)
56+
57+
### `Figure.savefig`
58+
59+
([docs](https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.savefig))
60+
61+
```
62+
savefig(fname, *, transparent=None, **kwargs)
63+
```
64+
65+
66+
```ocaml
67+
val savefig : t -> fname:string -> unit -> unit
68+
```
69+
70+
## Generating `Axes` & `Figure` modules
71+
72+
Let's go over the arguments and options for `pyml_bindgen` that we will need.
73+
74+
Note that we need to specify the correct Python module from which the `Axes` and `Figure` classes come.
75+
76+
* For `Axes`, that's `matplotlib.axes`.
77+
* For `Figure`, that's `matplotlib.figure`.
78+
79+
We use `--caml-module` option again to tell `pyml_bindgen` to generate the module signature as well as the implementation.
80+
81+
We use `-a class` to specify that we want to generate class-associated methods. Note that this is the default option.
82+
83+
For both invocations, we pipe the output directly to `ocamlformat`.
84+
85+
For now, `pyml_bindgen` always generates the `filter_opt` helper function. If you're generating multiple modules and concatenating them, you'll have to delete the function by hand or with `grep` or something. In our case, I use `grep` to remove the line from the `Figure` generating command. In later versions, you will be able to control this from the command line.
86+
87+
### Run `pyml_bindgen`
88+
89+
Here are the commands.
90+
91+
```
92+
pyml_bindgen axes_specs.txt matplotlib.axes Axes --caml-module Axes -a class \
93+
| ocamlformat --enable-outside-detected-project --name=a.ml - \
94+
> py_class.ml
95+
96+
printf "\n" >> py_class.ml
97+
98+
pyml_bindgen figure_specs.txt matplotlib.figure Figure --caml-module Figure -a class \
99+
| grep -v 'let filter_opt' \
100+
| ocamlformat --enable-outside-detected-project --name=a.ml - \
101+
>> py_class.ml
102+
```
103+
104+
### Patch the file
105+
106+
Above, I mentioned that you would need to change the implementation for the `Axes` module a bit. Here is the patch for the lines you need to change.
107+
108+
Here is a patch showing the change I mean
109+
110+
```
111+
--- py_class_bug.ml 2021-10-20 20:21:00.000000000 -0400
112+
+++ py_class.ml 2021-10-20 20:21:00.000000000 -0400
113+
@@ -30,17 +30,21 @@
114+
115+
let plot t ~x ~y ?color () =
116+
let callable = Py.Object.find_attr_string t "plot" in
117+
+ let args =
118+
+ [|
119+
+ Py.List.of_list_map Py.Float.of_float x;
120+
+ Py.List.of_list_map Py.Float.of_float y;
121+
+ |]
122+
+ in
123+
let kwargs =
124+
filter_opt
125+
[
126+
- Some ("x", Py.List.of_list_map Py.Float.of_float x);
127+
- Some ("y", Py.List.of_list_map Py.Float.of_float y);
128+
(match color with
129+
| Some color -> Some ("color", Py.String.of_string color)
130+
| None -> None);
131+
]
132+
in
133+
- ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs
134+
+ ignore @@ Py.Callable.to_function_with_keywords callable args kwargs
135+
end
136+
137+
module Figure : sig
138+
```
139+
140+
### Generated output
141+
142+
Here's the whole of the generated output including the patch.
143+
144+
```ocaml
145+
let filter_opt l = List.filter_map Fun.id l
146+
147+
module Axes : sig
148+
type t
149+
150+
val of_pyobject : Pytypes.pyobject -> t option
151+
152+
val to_pyobject : t -> Pytypes.pyobject
153+
154+
val set_title : t -> label:string -> unit -> unit
155+
156+
val plot : t -> x:float list -> y:float list -> ?color:string -> unit -> unit
157+
end = struct
158+
let import_module () = Py.Import.import_module "matplotlib.axes"
159+
160+
type t = Pytypes.pyobject
161+
162+
let is_instance pyo =
163+
let py_class = Py.Module.get (import_module ()) "Axes" in
164+
Py.Object.is_instance pyo py_class
165+
166+
let of_pyobject pyo = if is_instance pyo then Some pyo else None
167+
168+
let to_pyobject x = x
169+
170+
let set_title t ~label () =
171+
let callable = Py.Object.find_attr_string t "set_title" in
172+
let kwargs = filter_opt [ Some ("label", Py.String.of_string label) ] in
173+
ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs
174+
175+
let plot t ~x ~y ?color () =
176+
let callable = Py.Object.find_attr_string t "plot" in
177+
let args =
178+
[|
179+
Py.List.of_list_map Py.Float.of_float x;
180+
Py.List.of_list_map Py.Float.of_float y;
181+
|]
182+
in
183+
let kwargs =
184+
filter_opt
185+
[
186+
(match color with
187+
| Some color -> Some ("color", Py.String.of_string color)
188+
| None -> None);
189+
]
190+
in
191+
ignore @@ Py.Callable.to_function_with_keywords callable args kwargs
192+
end
193+
194+
module Figure : sig
195+
type t
196+
197+
val of_pyobject : Pytypes.pyobject -> t option
198+
199+
val to_pyobject : t -> Pytypes.pyobject
200+
201+
val savefig : t -> fname:string -> unit -> unit
202+
end = struct
203+
let import_module () = Py.Import.import_module "matplotlib.figure"
204+
205+
type t = Pytypes.pyobject
206+
207+
let is_instance pyo =
208+
let py_class = Py.Module.get (import_module ()) "Figure" in
209+
Py.Object.is_instance pyo py_class
210+
211+
let of_pyobject pyo = if is_instance pyo then Some pyo else None
212+
213+
let to_pyobject x = x
214+
215+
let savefig t ~fname () =
216+
let callable = Py.Object.find_attr_string t "savefig" in
217+
let kwargs = filter_opt [ Some ("fname", Py.String.of_string fname) ] in
218+
ignore @@ Py.Callable.to_function_with_keywords callable [||] kwargs
219+
end
220+
```
221+
222+
## Write the `Pyplot` module
223+
224+
For a little variety, and because we don't need any of the extra stuff that `pyml_bindgen` generates (again, you will be able to control this eventually), let's write this one by hand.
225+
226+
Then you can make a `pyplot.ml` file
227+
228+
```ocaml
229+
open Py_class
230+
231+
let import_module () = Py.Import.import_module "matplotlib.pyplot"
232+
233+
let subplots () =
234+
let callable = Py.Module.get (import_module ()) "subplots" in
235+
let args = [||] in
236+
let kwargs = [] in
237+
let tup = Py.Callable.to_function_with_keywords callable args kwargs in
238+
let fig, ax = Py.Tuple.to_tuple2 tup in
239+
match (Figure.of_pyobject fig, Axes.of_pyobject ax) with
240+
| Some f, Some a -> Some (f, a)
241+
| Some _, None | None, Some _ | None, None -> None
242+
```
243+
244+
Note that there are more compact ways to write this with `pyml`, but we will leave it like this to keep it similar to the rest of the generated functions.
245+
246+
## Set up the Dune project and run it
247+
248+
Now we need a dune file and a driver to run our plotting code. Save these two files in the same directory in as the other files.
249+
250+
`dune`
251+
252+
```
253+
(executable
254+
(name run)
255+
(libraries pyml))
256+
```
257+
258+
`run.ml`
259+
260+
```ocaml
261+
open Py_class
262+
263+
let () = Py.initialize ()
264+
265+
let figure, axes =
266+
match Pyplot.subplots () with
267+
| Some (fig, ax) -> (fig, ax)
268+
| None -> failwith "Failed to make figure and axes!"
269+
270+
let x = [ 1.; 2.; 3.; 4.; 5. ]
271+
let y = [ 1.; 1.5; 2.; 3.; 3.5 ]
272+
273+
let () = Axes.set_title axes ~label:"Brown Plot" ()
274+
let () = Axes.plot axes ~x ~y ~color:"tab:brown" ()
275+
let () = Figure.savefig figure ~fname:"brown_plot.png" ()
276+
```
277+
278+
Run it like so:
279+
280+
```
281+
$ dune exec ./run.exe
282+
```
283+
284+
If all goes well, you should see a nice, brown line plot:
285+
286+
![brown_plot.png](img/brown_plot.png)
287+
288+
## Wrap up
289+
290+
In this tutorial, we generating bindings for a couple of matplotlib classes and functions. You saw how to combine multiple generated modules as well as some of the little workarounds you still have to do.
291+
292+
Like all the examples so far, we're only binding a couple of classes & functions. For such a small thing, feel free to write your bindings by hand. These two classes alone have tons of functions though, so if you were binding them all, that would be a pain to write by hand!

pyml_bindgen.opam

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# This file is generated by dune, edit dune-project instead
22
opam-version: "2.0"
33
version: "0.1.0"
4-
synopsis: "Generate pyml bindings from OCaml signatures"
5-
maintainer: ["Ryan Moore"]
6-
authors: ["Ryan Moore"]
4+
synopsis: "Generate pyml bindings from OCaml value specifications"
5+
maintainer: ["Ryan M. Moore"]
6+
authors: ["Ryan M. Moore"]
77
license: "MIT"
88
homepage: "https://github.com/mooreryan/pyml_bindgen"
99
doc: "https://github.com/mooreryan/pyml_bindgen"

0 commit comments

Comments
 (0)