├── tests
├── __init__.py
└── test_grid.py
├── requirements.txt
├── mplgrid
├── __about__.py
├── __init__.py
└── mplgrid.py
├── .idea
├── .gitignore
├── vcs.xml
├── misc.xml
├── inspectionProfiles
│ └── profiles_settings.xml
├── modules.xml
└── mplgrid.iml
├── README.md
├── environment.yml
├── .readthedocs.yml
├── LICENSE
├── CHANGELOG.md
├── pyproject.toml
└── .gitignore
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib
2 |
--------------------------------------------------------------------------------
/mplgrid/__about__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.3.0"
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mplgrid
2 |
3 | mplgrid is a Python library for creating a grid of axes in Matplotlib
4 |
--------------------------------------------------------------------------------
/environment.yml:
--------------------------------------------------------------------------------
1 | name: mplgrid
2 | channels:
3 | - conda-forge
4 | dependencies:
5 | - matplotlib
6 | - pip
7 | - pip:
8 | - -e .
9 |
--------------------------------------------------------------------------------
/mplgrid/__init__.py:
--------------------------------------------------------------------------------
1 | """ This module imports the mplgrid functions"""
2 | from .mplgrid import grid, grid_dimensions
3 | from .__about__ import __version__
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/mplgrid.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | build:
9 | os: "ubuntu-20.04"
10 | tools:
11 | python: "mambaforge-4.10"
12 |
13 | # Build documentation in the docs/ directory with Sphinx
14 | sphinx:
15 | configuration: docs/source/conf.py
16 |
17 | # Optionally build your docs in additional formats such as PDF
18 | #formats:
19 | # - pdf
20 |
21 | conda:
22 | environment: environment.yml
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Andrew Rowlinson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | :rocket: Version 0.3.0
2 | ----------------------
3 |
4 | Fixes
5 | - Fix the aspect argument as it was setting the aspect ratio, which produced the wrong size \
6 | of chart when data was added.
7 |
8 | :rocket: Version 0.2.1
9 | ----------------------
10 |
11 | Fixes
12 | - Fix docstrings for aspect (height / width)
13 |
14 | :rocket: Version 0.2
15 | --------------------
16 |
17 | Changed
18 | - removed the ability to create endnote and title axes.
19 | - grid now returns axes and figure instead of a dictonary.
20 | - grid uses matplotlib plt.subplots instead of manually adding the axes with fig.add_axes()
21 | - aspect is flipped to match matplotlib (height / width) instead of the previous (width / height)
22 | - arguments are now renamed: grid_height to height, grid_width to width, and max_grid to max_side.
23 |
24 | :rocket: Version 0.1.0
25 | ----------------------
26 |
27 | Changed
28 | - renamed ax_key to grid_key and changed the default to 'grid'
29 | - changed the defaults so there is no title or endnote
30 | - changed the returned result so if there is no endnote or title \
31 | it returns the axes (e.g. numpy array of axes) rather than a dictionary.
32 |
33 | Added
34 | - added a test for the figsize
35 |
36 |
37 | :rocket: Version 0.0.0
38 | ----------------------
39 |
40 | Initial version of mplgrid with functions for creating a grid of Matplotlib axes and calculating the
41 | ideal grid dimensions.
42 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["hatchling"]
3 | build-backend = "hatchling.build"
4 |
5 | [project]
6 | name = "mplgrid"
7 | description = 'mplgrid is a Python library for creating a grid of axes in Matplotlib.'
8 | readme = "README.md"
9 | requires-python = ">=3.7"
10 | license = "MIT"
11 | keywords = ["matplotlib"]
12 | authors = [
13 | { name = "Andrew Rowlinson", email = "rowlinsonandy@gmail.com" },
14 | ]
15 | classifiers = [
16 | "Development Status :: 4 - Beta",
17 | "Framework :: Matplotlib",
18 | "Programming Language :: Python",
19 | "Programming Language :: Python :: 3.7",
20 | "Programming Language :: Python :: 3.8",
21 | "Programming Language :: Python :: 3.9",
22 | "Programming Language :: Python :: 3.10",
23 | "Programming Language :: Python :: 3.11",
24 | "Programming Language :: Python :: Implementation :: CPython",
25 | "Programming Language :: Python :: Implementation :: PyPy",
26 | ]
27 | dependencies = ["matplotlib"]
28 | dynamic = ["version"]
29 |
30 | [project.urls]
31 | Documentation = "https://github.com/andrewRowlinson/mplgrid#readme"
32 | Issues = "https://github.com/andrewRowlinson/mplgrid/issues"
33 | Source = "https://github.com/andrewRowlinson/mplgrid/"
34 |
35 | [tool.hatch.version]
36 | path = "mplgrid/__about__.py"
37 |
38 | [tool.hatch.envs.default]
39 | dependencies = [
40 | "pytest",
41 | "pytest-cov",
42 | ]
43 | [tool.hatch.envs.default.scripts]
44 | cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=temporary --cov=tests {args}"
45 | no-cov = "cov --no-cov {args}"
46 |
47 | [[tool.hatch.envs.test.matrix]]
48 | python = ["37", "38", "39", "310", "311"]
49 |
50 | [tool.coverage.run]
51 | branch = true
52 | parallel = true
53 | omit = [
54 | "temporary/__about__.py",
55 | ]
56 |
57 | [tool.coverage.report]
58 | exclude_lines = [
59 | "no cov",
60 | "if __name__ == .__main__.:",
61 | "if TYPE_CHECKING:",
62 | ]
63 |
--------------------------------------------------------------------------------
/tests/test_grid.py:
--------------------------------------------------------------------------------
1 | """ Tests for mplgrid."""
2 |
3 | from math import isclose
4 | from random import randint, uniform
5 |
6 | import matplotlib.pyplot as plt
7 |
8 | from mplgrid import grid, grid_dimensions
9 |
10 |
11 | def test_figsize():
12 | """ Test that the right figure size is created."""
13 | for i in range(100):
14 | aspect = uniform(0.3, 3)
15 | nrows = randint(1, 6)
16 | ncols = randint(1, 6)
17 | figwidth = uniform(0.5, 10)
18 | figheight = uniform(0.5, 10)
19 | max_side = uniform(0.5, 1)
20 | space = uniform(0, 0.2)
21 |
22 | width, height = grid_dimensions(aspect=aspect,
23 | figwidth=figwidth,
24 | figheight=figheight,
25 | nrows=nrows,
26 | ncols=ncols,
27 | max_side=max_side,
28 | space=space,
29 | )
30 | assert (isclose(height - max_side, 0, abs_tol=1e-09) or
31 | isclose(width - max_side, 0, abs_tol=1e-09)
32 | )
33 |
34 | fig, ax = grid(aspect=aspect,
35 | figheight=figheight,
36 | nrows=nrows,
37 | ncols=ncols,
38 | height=height,
39 | width=width,
40 | space=space,
41 | )
42 | check_figwidth, check_figheight = fig.get_size_inches()
43 |
44 | assert isclose(check_figwidth - figwidth, 0, abs_tol=1e-09)
45 | assert isclose(check_figheight - figheight, 0, abs_tol=1e-09)
46 | plt.close(fig)
47 |
48 |
49 | def test_return_shape():
50 | """ Test that the return shape is the same as plt.subplots."""
51 | for nrows in range(1, 4):
52 | for ncols in range(1, 4):
53 | fig1, ax1 = plt.subplots(nrows=nrows, ncols=ncols)
54 | fig2, ax2 = grid(nrows=nrows, ncols=ncols)
55 | if nrows > 1 or ncols > 1:
56 | assert ax1.shape == ax2.shape
57 | plt.close(fig1)
58 | plt.close(fig2)
59 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/mplgrid/mplgrid.py:
--------------------------------------------------------------------------------
1 | """ Functions to plot a grid of matplotlib Axes."""
2 |
3 | import matplotlib.pyplot as plt
4 |
5 | __all__ = ['grid', 'grid_dimensions']
6 |
7 |
8 | def _grid_dimensions(aspect=1,
9 | figheight=9,
10 | nrows=1,
11 | ncols=1,
12 | height=0.715,
13 | width=0.95,
14 | space=0.05,
15 | left=None,
16 | bottom=None,
17 | ):
18 | """ A helper to calculate the grid dimensions.
19 |
20 | Parameters
21 | ----------
22 | aspect : float, default 1
23 | The aspect ratio of the grid's axis (height divided by width).
24 | figheight : float, default 9
25 | The figure height in inches.
26 | nrows, ncols : int, default 1
27 | Number of rows/columns of axes in the grid.
28 | height : float, default 0.715
29 | The height of the grid in fractions of the figure height.
30 | The default is the grid height is 71.5% of the figure height.
31 | width : float, default 0.95
32 | The width of the grid in fractions of the figure width.
33 | The default is the grid is 95% of the figure width.
34 | space : float, default 0.05
35 | The total amount of the grid height reserved for spacing between the grid axes.
36 | Expressed as a fraction of the height. The default is 5% of the grid height.
37 | The spacing across the grid width is automatically calculated to maintain even spacing.
38 | left : float, default None
39 | The location of the left-hand side of the axes in fractions of the figure width.
40 | The default of None places the axes in the middle of the figure.
41 | bottom : float, default None
42 | The location of the bottom axes in fractions of the figure height.
43 | The default of None places the axes grid in the middle of the figure.
44 |
45 | Returns
46 | -------
47 | dimensions : dict[dimension, value]
48 | A dictionary holding the axes and figure dimensions.
49 | """
50 | if left is None:
51 | left = (1 - width) / 2
52 |
53 | if bottom is None:
54 | bottom = (1 - height) / 2
55 |
56 | if bottom + height > 1:
57 | error_msg_height = ('The axes extends past the figure height. '
58 | 'Reduce one of the bottom, or height so the total is ≤ 1.')
59 | raise ValueError(error_msg_height)
60 |
61 | if left + width > 1:
62 | error_msg_width = ('The grid axes extends past the figure width. '
63 | 'Reduce one of the width or left so the total is ≤ 1.')
64 | raise ValueError(error_msg_width)
65 |
66 | if (nrows > 1) and (ncols > 1):
67 | figwidth = (figheight * height / width * (((1 - space) / aspect * ncols / nrows) +
68 | (space * (ncols - 1) / (nrows - 1))))
69 | spaceheight = height * space / (nrows - 1)
70 | spacewidth = spaceheight * figheight / figwidth
71 | axheight = height * (1 - space) / nrows
72 | axwidth = (width - (spacewidth * (ncols - 1))) / ncols
73 | wspace = spacewidth / axwidth
74 | hspace = spaceheight / axheight
75 |
76 | elif (nrows > 1) and (ncols == 1):
77 | figwidth = figheight * height / width * (1 - space) / aspect / nrows
78 | axheight = height * (1 - space) / nrows
79 | spaceheight = height * space / (nrows - 1)
80 | wspace = 0
81 | hspace = spaceheight / axheight
82 |
83 | elif (nrows == 1) and (ncols > 1):
84 | figwidth = figheight * height / width * (space + ncols / aspect)
85 | spacewidth = height * space * figheight / figwidth / (ncols - 1)
86 | axwidth = (width - (spacewidth * (ncols - 1))) / ncols
87 | wspace = spacewidth / axwidth
88 | hspace = 0
89 |
90 | else: # nrows=1, ncols=1
91 | figwidth = figheight * height / aspect / width
92 | wspace = 0
93 | hspace = 0
94 |
95 | return {'figheight': figheight,
96 | 'figwidth': figwidth,
97 | 'nrows': nrows,
98 | 'ncols': ncols,
99 | 'aspect': aspect,
100 | 'left': left,
101 | 'right': left + width,
102 | 'bottom': bottom,
103 | 'top': bottom + height,
104 | 'hspace': hspace,
105 | 'wspace': wspace,
106 | }
107 |
108 |
109 | def grid(aspect=1,
110 | figheight=9,
111 | nrows=1,
112 | ncols=1,
113 | height=0.715,
114 | width=0.95,
115 | space=0.05,
116 | left=None,
117 | bottom=None,
118 | ):
119 | """ Create a grid of axes in a specified location
120 |
121 | Parameters
122 | ----------
123 | aspect : float, default 1
124 | The aspect ratio of the grid's axis (height divided by width).
125 | figheight : float, default 9
126 | The figure height in inches.
127 | nrows, ncols : int, default 1
128 | Number of rows/columns of axes in the grid.
129 | height : float, default 0.715
130 | The height of the grid in fractions of the figure height.
131 | The default is the grid height is 71.5% of the figure height.
132 | width : float, default 0.95
133 | The width of the grid in fractions of the figure width.
134 | The default is the grid is 95% of the figure width.
135 | space : float, default 0.05
136 | The total amount of the grid height reserved for spacing between the grid axes.
137 | Expressed as a fraction of the height. The default is 5% of the grid height.
138 | The spacing across the grid width is automatically calculated to maintain even spacing.
139 | left : float, default None
140 | The location of the left-hand side of the axes in fractions of the figure width.
141 | The default of None places the axes in the middle of the figure.
142 | bottom : float, default None
143 | The location of the bottom axes in fractions of the figure height.
144 | The default of None places the axes grid in the middle of the figure.
145 |
146 | Returns
147 | -------
148 | fig : matplotlib.figure.Figure
149 | axs : matplotlib Axes or array of Axes
150 | """
151 | dim = _grid_dimensions(aspect=aspect,
152 | figheight=figheight,
153 | width=width,
154 | height=height,
155 | nrows=nrows,
156 | ncols=ncols,
157 | space=space,
158 | left=left,
159 | bottom=bottom)
160 | fig, ax = plt.subplots(figsize=(dim['figwidth'], dim['figheight']),
161 | nrows=dim['nrows'], ncols=dim['ncols'],
162 | gridspec_kw={'left': dim['left'],
163 | 'bottom': dim['bottom'],
164 | 'right': dim['right'],
165 | 'top': dim['top'],
166 | 'wspace': dim['wspace'],
167 | 'hspace': dim['hspace'],
168 | },
169 | )
170 | return fig, ax
171 |
172 |
173 | def grid_dimensions(aspect,
174 | figwidth,
175 | figheight,
176 | nrows,
177 | ncols,
178 | max_side,
179 | space,
180 | ):
181 | """ Propose a grid_width and grid_height for grid based on the inputs.
182 |
183 | Parameters
184 | ----------
185 | aspect : float, default 1
186 | The aspect ratio of the grid's axis (height divided by width).
187 | figwidth, figheight : float
188 | The figure width/height in inches.
189 | nrows, ncols : int
190 | Number of rows/columns of axes in the grid.
191 | max_side : float
192 | The longest side of the grid in fractions of the figure width / height.
193 | Should be between zero and one.
194 | space : float
195 | The total amount of the grid height reserved for spacing between the grid axes.
196 | Expressed as a fraction of the grid_height.
197 |
198 | Returns
199 | -------
200 | grid_width, grid_height : the suggested grid_width and grid_height
201 | """
202 | # grid1 = calculate the width given the max_grid as height
203 | # grid2 = calculate height given the max_grid as width
204 |
205 | if ncols > 1 and nrows == 1:
206 | grid1 = max_side * figheight / figwidth * (space + ncols / aspect)
207 | grid2 = max_side / figheight * figwidth / (space + ncols / aspect)
208 | elif ncols > 1 or nrows > 1:
209 | extra = space * (ncols - 1) / (nrows - 1)
210 | grid1 = max_side * figheight / figwidth * (((1 - space) / aspect *
211 | ncols / nrows) + extra)
212 | grid2 = max_side / figheight * figwidth / (((1 - space) / aspect *
213 | ncols / nrows) + extra)
214 | else: # nrows=1, ncols=1
215 | grid1 = max_side * figheight / figwidth / aspect
216 | grid2 = max_side / figheight * figwidth * aspect
217 |
218 | # decide whether the max_grid is the width or height and set the other value
219 | if (grid1 > 1) | ((grid2 >= grid1) & (grid2 <= 1)):
220 | return max_side, grid2
221 | return grid1, max_side
222 |
--------------------------------------------------------------------------------