plotis 0.1.8__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {plotis-0.1.8 → plotis-0.2.0}/.github/workflows/publish_prod.yml +11 -4
  2. {plotis-0.1.8 → plotis-0.2.0}/.github/workflows/publish_test.yml +2 -2
  3. plotis-0.2.0/.github/workflows/tests.yml +55 -0
  4. {plotis-0.1.8 → plotis-0.2.0}/.gitignore +7 -0
  5. plotis-0.2.0/CHANGELOG.md +40 -0
  6. plotis-0.2.0/PKG-INFO +177 -0
  7. plotis-0.2.0/README.md +153 -0
  8. plotis-0.2.0/examples/ex1/Makefile +27 -0
  9. plotis-0.2.0/examples/ex1/plot_script.py +47 -0
  10. plotis-0.2.0/examples/ex2/Makefile +24 -0
  11. plotis-0.2.0/examples/ex2/house.mplstyle +10 -0
  12. plotis-0.2.0/examples/ex2/main.tex +32 -0
  13. plotis-0.2.0/examples/ex2/plot_script.py +67 -0
  14. plotis-0.2.0/plotis/__init__.py +10 -0
  15. plotis-0.2.0/plotis/plotis.py +296 -0
  16. {plotis-0.1.8 → plotis-0.2.0}/pyproject.toml +11 -4
  17. plotis-0.2.0/tests/conftest.py +31 -0
  18. plotis-0.2.0/tests/test_features.py +215 -0
  19. plotis-0.1.8/.github/workflows/tests.yml +0 -39
  20. plotis-0.1.8/PKG-INFO +0 -117
  21. plotis-0.1.8/README.md +0 -100
  22. plotis-0.1.8/examples/ex1/Makefile +0 -23
  23. plotis-0.1.8/examples/ex1/main.pdf +0 -0
  24. plotis-0.1.8/examples/ex1/plot_sript.py +0 -51
  25. plotis-0.1.8/plotis/__init__.py +0 -1
  26. plotis-0.1.8/plotis/plotis.py +0 -334
  27. plotis-0.1.8/tests/conftest.py +0 -16
  28. plotis-0.1.8/tests/data/__init__.py +0 -0
  29. plotis-0.1.8/tests/data/sample_calling_file.py +0 -112
  30. plotis-0.1.8/tests/data/sample_calling_file_linecount.py +0 -37
  31. plotis-0.1.8/tests/test_plotis.py +0 -112
  32. {plotis-0.1.8 → plotis-0.2.0}/LICENCE +0 -0
  33. {plotis-0.1.8 → plotis-0.2.0}/examples/ex1/main.tex +0 -0
  34. {plotis-0.1.8 → plotis-0.2.0}/tests/__init__.py +0 -0
@@ -30,7 +30,7 @@ jobs:
30
30
  run: python3 -m build
31
31
 
32
32
  - name: Store the distribution packages
33
- uses: actions/upload-artifact@v3
33
+ uses: actions/upload-artifact@v4
34
34
  with:
35
35
  name: python-package-distributions
36
36
  path: dist/
@@ -50,7 +50,7 @@ jobs:
50
50
 
51
51
  steps:
52
52
  - name: Download all the dists
53
- uses: actions/download-artifact@v3
53
+ uses: actions/download-artifact@v4
54
54
  with:
55
55
  name: python-package-distributions
56
56
  path: dist/
@@ -71,11 +71,18 @@ jobs:
71
71
  id-token: write # IMPORTANT: mandatory for sigstore
72
72
 
73
73
  steps:
74
+ - uses: actions/checkout@v4
74
75
  - name: Download all the dists
75
- uses: actions/download-artifact@v3
76
+ uses: actions/download-artifact@v4
76
77
  with:
77
78
  name: python-package-distributions
78
79
  path: dist/
80
+ - name: Extract release notes from CHANGELOG.md
81
+ # Prints the section between "## [<version>]" and the next "## [" heading
82
+ run: >-
83
+ awk -v v="${GITHUB_REF_NAME#v}"
84
+ '/^## \[/ { p = index($0, "## [" v "]") == 1; next } p'
85
+ CHANGELOG.md > release-notes.md
79
86
  - name: Sign the dists with Sigstore
80
87
  uses: sigstore/gh-action-sigstore-python@v2.1.1
81
88
  with:
@@ -89,7 +96,7 @@ jobs:
89
96
  gh release create
90
97
  '${{ github.ref_name }}'
91
98
  --repo '${{ github.repository }}'
92
- --notes ""
99
+ --notes-file release-notes.md
93
100
  - name: Upload artifact signatures to GitHub Release
94
101
  env:
95
102
  GITHUB_TOKEN: ${{ github.token }}
@@ -30,7 +30,7 @@ jobs:
30
30
  run: python3 -m build
31
31
 
32
32
  - name: Store the distribution packages
33
- uses: actions/upload-artifact@v3
33
+ uses: actions/upload-artifact@v4
34
34
  with:
35
35
  name: python-package-distributions
36
36
  path: dist/
@@ -50,7 +50,7 @@ jobs:
50
50
 
51
51
  steps:
52
52
  - name: Download all the dists
53
- uses: actions/download-artifact@v3
53
+ uses: actions/download-artifact@v4
54
54
  with:
55
55
  name: python-package-distributions
56
56
  path: dist/
@@ -0,0 +1,55 @@
1
+ # Installs the package with its dev extras, lints, runs the tests and the
2
+ # ex2 example on every supported Python version.
3
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
4
+
5
+ name: Test workflow
6
+
7
+ on:
8
+ push:
9
+ branches: [ "main" ]
10
+ pull_request:
11
+ branches: [ "main" ]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ tests:
18
+ name: tests (${{ matrix.python-version }})
19
+ runs-on: ubuntu-latest
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ - name: Install package with dev extras
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ pip install -e ".[dev]"
35
+ - name: Lint with flake8
36
+ run: |
37
+ # stop the build if there are Python syntax errors or undefined names
38
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv
39
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
40
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.venv
41
+ - name: Test with pytest
42
+ run: pytest
43
+ - name: Run the ex2 example and reproduce its figures (no LaTeX)
44
+ run: make -C examples/ex2 reproduce
45
+
46
+ # Single check named "test" that branch protection on main requires.
47
+ # It passes only if every Python version in the matrix passed, so the
48
+ # matrix can change without updating the protection rule.
49
+ test:
50
+ needs: tests
51
+ if: always()
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - name: Check that all matrix jobs passed
55
+ run: test "${{ needs.tests.result }}" = "success"
@@ -468,3 +468,10 @@ TSWLatexianTemp*
468
468
 
469
469
  # glossaries
470
470
  *.glstex
471
+
472
+ ### plotis examples ###
473
+ # Built from source by the example Makefiles
474
+ examples/*/main.pdf
475
+ examples/*/figures/
476
+ examples/*/moved/
477
+ examples/ex1/sinus_curve/
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [0.2.0]
8
+
9
+ ### Added
10
+ - `format`, `savefig_kwargs`, `style` and `render` arguments to `PlotIs`.
11
+ - `PlotIsError`, raised when a context cannot be isolated.
12
+ - `plotis.__version__`.
13
+ - `examples/ex2`: every option, a LaTeX document and a `make reproduce` target.
14
+
15
+ ### Changed
16
+ - The `with` statement is located by parsing the calling file with `ast`
17
+ instead of regexes and line counting. Multi-line `with` statements,
18
+ keyword arguments in any order, one-line bodies and nested blocks work.
19
+ - `run.py` resolves `data.csv`, the style file and the figure relative to
20
+ itself, so the folder can be moved and run from any working directory.
21
+ - The data is captured when the context is entered, so changes the body makes
22
+ to the data frame are not saved to `data.csv` (and applied twice by `run.py`).
23
+ - `savefig()`/`show()` statements are replaced by `pass` in `run.py`.
24
+ - `data.csv` is written without the index column.
25
+ - Nothing is written if the context raises an exception.
26
+ - `numpy` is no longer a runtime dependency.
27
+
28
+ ### Breaking
29
+ - Python 3.9 is no longer supported (end-of-life); 3.10 or newer is required.
30
+ - Errors are raised as `PlotIsError` (or `TypeError`/`FileNotFoundError` for
31
+ invalid arguments) instead of a generic `Exception`.
32
+ - `data` must be passed as a plain variable name.
33
+ - `data.csv` has no index column: read it with `pd.read_csv(path)` and
34
+ without `index_col=0`.
35
+ - `run.py` saves `figure.<format>` next to itself. In 0.1 the path was
36
+ `<figpath>/figure.png` relative to the working directory, so `run.py`
37
+ only worked when run from the same directory as the original script.
38
+
39
+ ## [0.1.9]
40
+ - Last release of the regex-based implementation.
plotis-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: plotis
3
+ Version: 0.2.0
4
+ Summary: Plot isolation tool for python
5
+ Project-URL: Homepage, https://github.com/victorbrun/plotis
6
+ Project-URL: Issues, https://github.com/victorbrun/plotis/issues
7
+ Author-email: Victor Brun <me@victorbrun.com>
8
+ License-File: LICENCE
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: matplotlib
18
+ Requires-Dist: pandas
19
+ Provides-Extra: dev
20
+ Requires-Dist: flake8; extra == 'dev'
21
+ Requires-Dist: numpy; extra == 'dev'
22
+ Requires-Dist: pytest; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # `plotis`
26
+ `plotis` is a plot isolation tool for Python. It saves the data and Python code needed to reproduce a plot independently, using a pythonic interface.
27
+
28
+ This project started as a fix for repositories that balloon in size in documentation projects where images are updated often. The most common image formats are not plain text, so Git cannot version them efficiently and stores every version of every image. Instead of converting images to a plain-text format or pruning the Git history, I chose to overengineer this solution: a Python context manager extracts the code and data needed to reproduce each plot.
29
+
30
+ # Installation
31
+ ```bash
32
+ pip install plotis
33
+ ```
34
+
35
+ # Usage
36
+ ## Simple example
37
+ `plotis` relies on the context manager `PlotIs`. Put the plotting logic inside a `PlotIs` context and pass it the data used for plotting, as shown below. On exit, `PlotIs` creates a folder containing the data (`<figfoldername>/data.csv`) and code (`<figfoldername>/run.py`) needed to reproduce the plot independently.
38
+ ```python
39
+ import numpy as np
40
+ import pandas as pd
41
+ import matplotlib.pyplot as plt
42
+
43
+ from plotis import PlotIs
44
+
45
+ # Generates sample data
46
+ x = np.linspace(0, 4 * np.pi)
47
+ sample_data = pd.DataFrame({"x": x, "y": np.sin(x)})
48
+
49
+ # Creates the plot inside a PlotIs context. The folder fig1 will contain
50
+ # the code and data needed to reproduce the plot independently.
51
+ with PlotIs("fig1", sample_data):
52
+ sample_data.plot(x="x", y="y")
53
+ plt.title("Sine function")
54
+ plt.xlabel("x")
55
+ plt.ylabel("sin(x)")
56
+
57
+ # Shows the plot here; replaced by `pass` in run.py
58
+ plt.show()
59
+ ```
60
+
61
+ Running `python fig1/run.py`, from any directory, writes `fig1/figure.png`.
62
+
63
+ ## Options
64
+ ```python
65
+ with PlotIs(
66
+ "figures/sine", # folder for data.csv, run.py (and the figure)
67
+ sample_data, # must be a plain variable name
68
+ format="pdf", # any format plt.savefig understands (default "png")
69
+ savefig_kwargs={"bbox_inches": "tight"}, # literals only
70
+ style="house.mplstyle", # applied in the context, copied next to run.py
71
+ render=True, # also write figures/sine/figure.pdf on exit
72
+ ):
73
+ fig, ax = plt.subplots()
74
+ for name, group in sample_data.groupby("series"): # nested blocks are fine
75
+ ax.plot(group["x"], group["y"], label=name)
76
+ ax.legend()
77
+ ```
78
+
79
+ - `run.py` resolves `data.csv`, the style file and the output figure relative to its own location, so the folder can be moved and the script run from any working directory.
80
+ - The `with` statement is parsed with `ast`: it may span several lines, use keyword arguments in any order, or put the body on the same line.
81
+ - The data is captured when the context is entered, so changes the body makes to the data frame are not saved (and are not applied twice by `run.py`).
82
+ - `data.csv` is written without the index column.
83
+ - Nothing is written if the context raises an exception.
84
+ - `savefig()`/`show()` statements in the context are replaced by `pass` in `run.py`.
85
+
86
+ `examples/ex2` uses every option above. Run `make` there to build the figures and a LaTeX document, and `make reproduce` to regenerate the figures from the moved `run.py` files alone.
87
+
88
+ ## Limitations
89
+ `run.py` contains only the body of the `with` block, preceded by `import pandas as pd`, `import matplotlib.pyplot as plt` and the data variable loaded from `data.csv`. So the body:
90
+
91
+ - may only use `pd`, `plt`, the data variable and names it defines or imports itself. Other names from the surrounding script, such as `np`, helper functions or other variables, do not exist in `run.py`.
92
+ - must produce a single figure, with at most one `savefig()` and one `show()` call.
93
+ - cannot contain another `PlotIs` context.
94
+
95
+ With `render=True`, a `plt.show()` in the body can close the figure on interactive backends, which leaves the rendered file empty. Use `render=True` without `show()`, or use a non-interactive backend such as `Agg`.
96
+
97
+ `PlotIs` must be used in a source file (not an interactive prompt or notebook cell), since it reads the `with` block from the calling file.
98
+
99
+ ## Errors
100
+ | Exception | Raised when |
101
+ |---|---|
102
+ | `PlotIsError` | `PlotIs` is not used as a `with` statement, `data` is not a plain variable name, the body nests `PlotIs` or has more than one `savefig()`/`show()` call, `savefig_kwargs` holds non-literal values, or the calling source file cannot be read. |
103
+ | `TypeError` | `data` is not a `pandas.DataFrame`. |
104
+ | `FileNotFoundError` | The `style` file does not exist. |
105
+
106
+ ## Migrating from 0.1
107
+ - Python 3.10 or newer is required.
108
+ - Catch `PlotIsError` instead of `Exception`.
109
+ - Pass `data` as a plain variable name (`PlotIs(path, df)`, not `PlotIs(path, df.copy())`).
110
+ - `data.csv` has no index column. Read it with `pd.read_csv(path)`, without `index_col=0`.
111
+ - `run.py` now saves `figure.<format>` next to itself instead of relative to the working directory.
112
+
113
+ See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
114
+
115
+ ## LaTeX project example
116
+ Since this package was motivated by large documentation projects, this example shows how to hook into the compilation of a LaTeX document to insert the figure produced by the example above.
117
+
118
+ Consider a LaTeX document with the following content:
119
+ ```latex
120
+ \documentclass[12pt]{article}
121
+ \usepackage{graphicx}
122
+ \usepackage{hyperref}
123
+
124
+ \title{PlotIs example one}
125
+ \author{Victor Brun\\ \href{https://victorbrun.com}{victorbrun.com}}
126
+ \date{\today}
127
+
128
+ \begin{document}
129
+
130
+ \maketitle
131
+
132
+ \section{The sine function}
133
+ The sine function is a trigonometric function which we denote by $\sin{x}$. In figure \ref{fig:sin} we can see that it looks like a wave.
134
+
135
+ \begin{figure}[h]
136
+ \centering
137
+ \includegraphics[width=8cm]{figures/sinus_curve.png}
138
+ \caption{The sine function plotted on the interval $[0, 4\pi]$.}
139
+ \label{fig:sin}
140
+ \end{figure}
141
+
142
+ \end{document}
143
+ ```
144
+
145
+ To keep the repository small, `figures/sinus_curve.png` is not committed, since it is not a plain-text format. The plotting script (`plot_script.py`, using `PlotIs` as above) writes `sinus_curve/data.csv` and `sinus_curve/run.py`, which are enough to recreate the figure. A Makefile then runs `run.py`, copies the image to where the LaTeX document expects it and compiles the document:
146
+ ```make
147
+ PYTHON ?= python3
148
+ FIG := sinus_curve
149
+
150
+ all: main.pdf
151
+
152
+ # Runs the plotting script, which writes the isolated data and code
153
+ $(FIG)/run.py $(FIG)/data.csv: plot_script.py
154
+ $(PYTHON) plot_script.py
155
+
156
+ # Runs the automatically generated python script to create the plot
157
+ $(FIG)/figure.png: $(FIG)/run.py $(FIG)/data.csv
158
+ $(PYTHON) $(FIG)/run.py
159
+
160
+ # Copies the produced figure to where main.tex expects it
161
+ figures/$(FIG).png: $(FIG)/figure.png
162
+ mkdir -p figures
163
+ cp $< $@
164
+
165
+ # Runs pdflatex twice so that references are resolved
166
+ main.pdf: main.tex figures/$(FIG).png
167
+ pdflatex main.tex
168
+ pdflatex main.tex
169
+ ```
170
+
171
+ In a real project you would commit `sinus_curve/` and run only the last three rules. The working example is in `examples/ex1`.
172
+
173
+ # Development
174
+ ```bash
175
+ pip install -e ".[dev]"
176
+ pytest
177
+ ```
plotis-0.2.0/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # `plotis`
2
+ `plotis` is a plot isolation tool for Python. It saves the data and Python code needed to reproduce a plot independently, using a pythonic interface.
3
+
4
+ This project started as a fix for repositories that balloon in size in documentation projects where images are updated often. The most common image formats are not plain text, so Git cannot version them efficiently and stores every version of every image. Instead of converting images to a plain-text format or pruning the Git history, I chose to overengineer this solution: a Python context manager extracts the code and data needed to reproduce each plot.
5
+
6
+ # Installation
7
+ ```bash
8
+ pip install plotis
9
+ ```
10
+
11
+ # Usage
12
+ ## Simple example
13
+ `plotis` relies on the context manager `PlotIs`. Put the plotting logic inside a `PlotIs` context and pass it the data used for plotting, as shown below. On exit, `PlotIs` creates a folder containing the data (`<figfoldername>/data.csv`) and code (`<figfoldername>/run.py`) needed to reproduce the plot independently.
14
+ ```python
15
+ import numpy as np
16
+ import pandas as pd
17
+ import matplotlib.pyplot as plt
18
+
19
+ from plotis import PlotIs
20
+
21
+ # Generates sample data
22
+ x = np.linspace(0, 4 * np.pi)
23
+ sample_data = pd.DataFrame({"x": x, "y": np.sin(x)})
24
+
25
+ # Creates the plot inside a PlotIs context. The folder fig1 will contain
26
+ # the code and data needed to reproduce the plot independently.
27
+ with PlotIs("fig1", sample_data):
28
+ sample_data.plot(x="x", y="y")
29
+ plt.title("Sine function")
30
+ plt.xlabel("x")
31
+ plt.ylabel("sin(x)")
32
+
33
+ # Shows the plot here; replaced by `pass` in run.py
34
+ plt.show()
35
+ ```
36
+
37
+ Running `python fig1/run.py`, from any directory, writes `fig1/figure.png`.
38
+
39
+ ## Options
40
+ ```python
41
+ with PlotIs(
42
+ "figures/sine", # folder for data.csv, run.py (and the figure)
43
+ sample_data, # must be a plain variable name
44
+ format="pdf", # any format plt.savefig understands (default "png")
45
+ savefig_kwargs={"bbox_inches": "tight"}, # literals only
46
+ style="house.mplstyle", # applied in the context, copied next to run.py
47
+ render=True, # also write figures/sine/figure.pdf on exit
48
+ ):
49
+ fig, ax = plt.subplots()
50
+ for name, group in sample_data.groupby("series"): # nested blocks are fine
51
+ ax.plot(group["x"], group["y"], label=name)
52
+ ax.legend()
53
+ ```
54
+
55
+ - `run.py` resolves `data.csv`, the style file and the output figure relative to its own location, so the folder can be moved and the script run from any working directory.
56
+ - The `with` statement is parsed with `ast`: it may span several lines, use keyword arguments in any order, or put the body on the same line.
57
+ - The data is captured when the context is entered, so changes the body makes to the data frame are not saved (and are not applied twice by `run.py`).
58
+ - `data.csv` is written without the index column.
59
+ - Nothing is written if the context raises an exception.
60
+ - `savefig()`/`show()` statements in the context are replaced by `pass` in `run.py`.
61
+
62
+ `examples/ex2` uses every option above. Run `make` there to build the figures and a LaTeX document, and `make reproduce` to regenerate the figures from the moved `run.py` files alone.
63
+
64
+ ## Limitations
65
+ `run.py` contains only the body of the `with` block, preceded by `import pandas as pd`, `import matplotlib.pyplot as plt` and the data variable loaded from `data.csv`. So the body:
66
+
67
+ - may only use `pd`, `plt`, the data variable and names it defines or imports itself. Other names from the surrounding script, such as `np`, helper functions or other variables, do not exist in `run.py`.
68
+ - must produce a single figure, with at most one `savefig()` and one `show()` call.
69
+ - cannot contain another `PlotIs` context.
70
+
71
+ With `render=True`, a `plt.show()` in the body can close the figure on interactive backends, which leaves the rendered file empty. Use `render=True` without `show()`, or use a non-interactive backend such as `Agg`.
72
+
73
+ `PlotIs` must be used in a source file (not an interactive prompt or notebook cell), since it reads the `with` block from the calling file.
74
+
75
+ ## Errors
76
+ | Exception | Raised when |
77
+ |---|---|
78
+ | `PlotIsError` | `PlotIs` is not used as a `with` statement, `data` is not a plain variable name, the body nests `PlotIs` or has more than one `savefig()`/`show()` call, `savefig_kwargs` holds non-literal values, or the calling source file cannot be read. |
79
+ | `TypeError` | `data` is not a `pandas.DataFrame`. |
80
+ | `FileNotFoundError` | The `style` file does not exist. |
81
+
82
+ ## Migrating from 0.1
83
+ - Python 3.10 or newer is required.
84
+ - Catch `PlotIsError` instead of `Exception`.
85
+ - Pass `data` as a plain variable name (`PlotIs(path, df)`, not `PlotIs(path, df.copy())`).
86
+ - `data.csv` has no index column. Read it with `pd.read_csv(path)`, without `index_col=0`.
87
+ - `run.py` now saves `figure.<format>` next to itself instead of relative to the working directory.
88
+
89
+ See [CHANGELOG.md](CHANGELOG.md) for the full list of changes.
90
+
91
+ ## LaTeX project example
92
+ Since this package was motivated by large documentation projects, this example shows how to hook into the compilation of a LaTeX document to insert the figure produced by the example above.
93
+
94
+ Consider a LaTeX document with the following content:
95
+ ```latex
96
+ \documentclass[12pt]{article}
97
+ \usepackage{graphicx}
98
+ \usepackage{hyperref}
99
+
100
+ \title{PlotIs example one}
101
+ \author{Victor Brun\\ \href{https://victorbrun.com}{victorbrun.com}}
102
+ \date{\today}
103
+
104
+ \begin{document}
105
+
106
+ \maketitle
107
+
108
+ \section{The sine function}
109
+ The sine function is a trigonometric function which we denote by $\sin{x}$. In figure \ref{fig:sin} we can see that it looks like a wave.
110
+
111
+ \begin{figure}[h]
112
+ \centering
113
+ \includegraphics[width=8cm]{figures/sinus_curve.png}
114
+ \caption{The sine function plotted on the interval $[0, 4\pi]$.}
115
+ \label{fig:sin}
116
+ \end{figure}
117
+
118
+ \end{document}
119
+ ```
120
+
121
+ To keep the repository small, `figures/sinus_curve.png` is not committed, since it is not a plain-text format. The plotting script (`plot_script.py`, using `PlotIs` as above) writes `sinus_curve/data.csv` and `sinus_curve/run.py`, which are enough to recreate the figure. A Makefile then runs `run.py`, copies the image to where the LaTeX document expects it and compiles the document:
122
+ ```make
123
+ PYTHON ?= python3
124
+ FIG := sinus_curve
125
+
126
+ all: main.pdf
127
+
128
+ # Runs the plotting script, which writes the isolated data and code
129
+ $(FIG)/run.py $(FIG)/data.csv: plot_script.py
130
+ $(PYTHON) plot_script.py
131
+
132
+ # Runs the automatically generated python script to create the plot
133
+ $(FIG)/figure.png: $(FIG)/run.py $(FIG)/data.csv
134
+ $(PYTHON) $(FIG)/run.py
135
+
136
+ # Copies the produced figure to where main.tex expects it
137
+ figures/$(FIG).png: $(FIG)/figure.png
138
+ mkdir -p figures
139
+ cp $< $@
140
+
141
+ # Runs pdflatex twice so that references are resolved
142
+ main.pdf: main.tex figures/$(FIG).png
143
+ pdflatex main.tex
144
+ pdflatex main.tex
145
+ ```
146
+
147
+ In a real project you would commit `sinus_curve/` and run only the last three rules. The working example is in `examples/ex1`.
148
+
149
+ # Development
150
+ ```bash
151
+ pip install -e ".[dev]"
152
+ pytest
153
+ ```
@@ -0,0 +1,27 @@
1
+ PYTHON ?= python3
2
+ FIG := sinus_curve
3
+
4
+ all: main.pdf
5
+
6
+ # Runs the plotting script, which writes the isolated data and code
7
+ $(FIG)/run.py $(FIG)/data.csv: plot_script.py
8
+ $(PYTHON) plot_script.py
9
+
10
+ # Runs the automatically generated python script to create the plot
11
+ $(FIG)/figure.png: $(FIG)/run.py $(FIG)/data.csv
12
+ $(PYTHON) $(FIG)/run.py
13
+
14
+ # Copies the produced figure to where main.tex expects it
15
+ figures/$(FIG).png: $(FIG)/figure.png
16
+ mkdir -p figures
17
+ cp $< $@
18
+
19
+ # Runs pdflatex twice so that references are resolved
20
+ main.pdf: main.tex figures/$(FIG).png
21
+ pdflatex main.tex
22
+ pdflatex main.tex
23
+
24
+ clean:
25
+ rm -rf $(FIG) figures main.aux main.log main.out main.pdf
26
+
27
+ .PHONY: all clean
@@ -0,0 +1,47 @@
1
+ """Example showing a simple use case of PlotIs.
2
+
3
+ Run ``make`` in this folder. It runs this script, which writes
4
+ ``sinus_curve/data.csv`` and ``sinus_curve/run.py``, then regenerates the
5
+ figure from ``run.py`` alone and builds ``main.pdf``.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import matplotlib
11
+
12
+ matplotlib.use("Agg")
13
+
14
+ import matplotlib.pyplot as plt # noqa: E402
15
+ import numpy as np # noqa: E402
16
+ import pandas as pd # noqa: E402
17
+
18
+ from plotis import PlotIs # noqa: E402
19
+
20
+ # The figure folder is placed next to this file, whatever the working directory
21
+ FIGURE_FOLDER = Path(__file__).resolve().parent / "sinus_curve"
22
+
23
+
24
+ def generate_data() -> pd.DataFrame:
25
+ """Generate sample data using the sine function."""
26
+ x = np.linspace(0, 4 * np.pi)
27
+ return pd.DataFrame({"x": x, "y": np.sin(x)})
28
+
29
+
30
+ def run() -> None:
31
+ sample_data = generate_data()
32
+
33
+ # Plot the data frame as usual, but inside the PlotIs context. This saves
34
+ # sample_data and the code in the context, so that the plot can be
35
+ # reproduced without any dependency on this file.
36
+ with PlotIs(FIGURE_FOLDER, sample_data):
37
+ sample_data.plot(x="x", y="y")
38
+ plt.title("Sine function")
39
+ plt.xlabel("x")
40
+ plt.ylabel("sin(x)")
41
+
42
+ # Replaced by `pass` in run.py
43
+ plt.show()
44
+
45
+
46
+ if __name__ == "__main__":
47
+ run()
@@ -0,0 +1,24 @@
1
+ PYTHON ?= python3
2
+
3
+ all: figures main.pdf
4
+
5
+ # Runs the plotting script: writes figures/*/{data.csv,run.py,house.mplstyle,figure.pdf}
6
+ figures: plot_script.py house.mplstyle
7
+ $(PYTHON) plot_script.py
8
+
9
+ main.pdf: main.tex figures
10
+ pdflatex main.tex
11
+ pdflatex main.tex
12
+
13
+ # Shows that run.py is self-contained and relocatable: the figure folders are
14
+ # moved, and each figure is regenerated from run.py + data.csv, run from /
15
+ reproduce: figures
16
+ rm -rf moved && mkdir moved && cp -R figures/waves figures/bars moved/
17
+ rm moved/*/figure.pdf
18
+ cd / && for d in "$(CURDIR)"/moved/*; do $(PYTHON) "$$d/run.py"; done
19
+ ls moved/*/figure.pdf
20
+
21
+ clean:
22
+ rm -rf figures moved main.aux main.log main.out main.pdf
23
+
24
+ .PHONY: all figures reproduce clean
@@ -0,0 +1,10 @@
1
+ # Shared house style, applied inside the PlotIs context and copied next to run.py
2
+ figure.figsize: 4.5, 2.8
3
+ font.family: serif
4
+ font.size: 9
5
+ axes.spines.top: False
6
+ axes.spines.right: False
7
+ axes.grid: True
8
+ grid.color: "#e4e3df"
9
+ legend.frameon: False
10
+ axes.prop_cycle: cycler("color", ["2a78d6", "eb6834", "1f9e74"])
@@ -0,0 +1,32 @@
1
+ \documentclass[11pt]{article}
2
+ \usepackage{graphicx}
3
+ \usepackage{hyperref}
4
+ \usepackage[utf8]{inputenc}
5
+
6
+ \title{PlotIs example two: every 0.2 option}
7
+ \author{Victor Brun\\ \href{https://victorbrun.com}{victorbrun.com}}
8
+ \date{\today}
9
+
10
+ \begin{document}
11
+ \maketitle
12
+
13
+ Both figures below are vector PDFs written by \texttt{PlotIs(..., format="pdf",
14
+ render=True)} and included straight from their figure folders. Nothing is
15
+ copied or moved.
16
+
17
+ \begin{figure}[h]
18
+ \centering
19
+ \includegraphics{figures/waves/figure.pdf}
20
+ \caption{Three phase-shifted sine waves, drawn with a \texttt{for} loop and
21
+ an \texttt{if} inside the PlotIs context and styled by \texttt{house.mplstyle}.}
22
+ \label{fig:waves}
23
+ \end{figure}
24
+
25
+ \begin{figure}[h]
26
+ \centering
27
+ \includegraphics{figures/bars/figure.pdf}
28
+ \caption{A bar chart with non-ASCII labels.}
29
+ \label{fig:bars}
30
+ \end{figure}
31
+
32
+ \end{document}