├── .gitignore ├── .pre-commit-config.yaml ├── AUTHORS.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── azure-pipelines.yml ├── codecov.yml ├── docs ├── Makefile ├── code.rst ├── conf.py ├── index.rst ├── make.bat └── requirements-docs.txt ├── images ├── grid_arrangement06.png ├── grid_arrangement07.png ├── grid_arrangement08.png └── grid_arrangement17.png ├── pyproject.toml ├── scripts └── release.py ├── setup.cfg ├── setup.py ├── src └── grid_strategy │ ├── __init__.py │ ├── _abc.py │ └── strategies.py ├── tests ├── test_grids.py └── test_strategies.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Doc files 2 | _build 3 | 4 | # Byte-compiled / optimized / DLL files 5 | *.py[oc] 6 | __pycache__ 7 | 8 | # Build detritus 9 | build/ 10 | dist/ 11 | dists/ 12 | pip-wheel-metadata 13 | .eggs 14 | *.egg-info/ 15 | 16 | # Test detritus 17 | .tox/ 18 | .pytest_cache/ 19 | venv/ 20 | .venv/ 21 | .hypothesis/ 22 | 23 | # Sphinx documentation 24 | docs/_build/ 25 | 26 | .idea 27 | .cache 28 | .mypy_cache 29 | .DS_STORE 30 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | name: black 7 | language: system 8 | entry: black 9 | types: [python] 10 | language_version: python3.6 11 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | This file contains a list with all the contributors to the *grid strategy* project. 2 | 3 | Contributors 4 | ------------ 5 | 6 | - Paul Ganssle 7 | - Konstantinos Oikonomou 8 | - Gurnek Singh Mokha 9 | - Morgan Haworth 10 | - Siping Meng 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Grid Strategy Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.md 2 | include pyproject.toml 3 | include tox.ini 4 | recursive-include tests *.py 5 | recursive-include docs * 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grid-strategy 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/grid-strategy.svg?style=flat-square)](https://pypi.org/project/grid-strategy/) 4 | [![Build Status](https://dev.azure.com/matplotlib/matplotlib/_apis/build/status/matplotlib.grid-strategy?branchName=master)](https://dev.azure.com/matplotlib/matplotlib/_build/latest?definitionId=2&branchName=master) 5 | [![Documentation Status](https://readthedocs.org/projects/grid-strategy/badge/?version=latest)](https://grid-strategy.readthedocs.io/en/latest/?badge=latest) 6 | [![codecov](https://codecov.io/gh/matplotlib/grid-strategy/branch/master/graph/badge.svg)](https://codecov.io/gh/matplotlib/grid-strategy) 7 | 8 | 9 | Grid-strategy is a python package that enables the user 10 | organize _matplotlib_ plots using different **grid strategies**. 11 | 12 | ## Abstract 13 | 14 | This package adds a mechanism for creating a grid of 15 | subplots based on the number of axes to be plotted and 16 | a strategy for how they should be arranged, with some 17 | sensible strategy as the default. 18 | 19 | ## Detailed Description 20 | 21 | It is often the case that you have some number of 22 | plots to display (and this number may be unknown 23 | ahead of time), and want some sensible arrangement 24 | of the plots so that they are all roughly equally 25 | aligned. However, the `subplots` and `gridspec` 26 | methods for creating subplots require both an `x` 27 | and a `y` dimension for creation and population of 28 | a grid. This package would allow users to specify a 29 | strategy for the creation of a grid, and then specify 30 | how many axes they want to plot, and they would 31 | get back a collection of axes arranged according 32 | to their strategy. 33 | 34 | The SquareStrategy alternates rows of x and x-1 columns 35 | to get as close as possible to a square shape for the plots. 36 | Some examples featuring this technique: 37 | 38 | n=6 n=7 39 | 40 | n=8 n=17 41 | 42 | This makes use of a `GridStrategy` object, which populates a `GridSpec`. In general, this concept can likely be implemented as a layer of abstraction *above* `gridspec.GridSpec`. 43 | 44 | Some basic strategies that will be included in the first release: 45 | 46 | - `"Square"` - As implemented in the pictures above - currently this is centered, but the base `SquareStrategy` object has options for `alignment` which include: 47 | - `'center'` (default), `'left'`, `'right'` - empty spaces either center the plots or leave them ragged-left or ragged-right 48 | - `'justified'` - This will fill every column as "fully-justified", with some plots being stretched to fill all of the colums in the row. 49 | 50 | - `"Rectangular"` - Similar to `"Square"`, this would find the largest pair of factors of the number of plots and use that to populate a rectangular grid - so `6` would return a 3x2 grid, `7` would return a 7x1 grid, and `10` would return a 5x2 grid. 51 | 52 | 53 | ### Higher dimensions 54 | 55 | Currently the package is limited to 2-dimensional 56 | grid arrangements, but a "nice-to-have" might be 57 | a higher-order API for `GridStrategy` that also allows 58 | for the proliferation of additional *figures* (e.g. 59 | "if I have more than 10 axes to plot, split them 60 | up as evenly as possible among `n / 10` different 61 | figures"). This would be no harder to implement 62 | in terms of the creation of such strategies, but 63 | may be harder to work with since it would 64 | necessarily spawn axes across multiple figures. 65 | 66 | ### Installation Instructions 67 | Simply run: 68 | `pip install grid-strategy` 69 | 70 | Then, in your project, do: 71 | `from grid_strategy import strategies` 72 | 73 | The strategies module has all usable strategies. 74 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | strategy: 4 | matrix: 5 | Python36: 6 | python.version: '3.6' 7 | Python37: 8 | python.version: '3.7' 9 | Python38: 10 | python.version: '3.8' 11 | macOS: 12 | python.version: '3.7' 13 | POOL_IMAGE: macos-10.13 14 | Windows: 15 | python.version: '3.7' 16 | POOL_IMAGE: vs2017-win2016 17 | installzic: 'windows' 18 | Black: 19 | python.version: '3.7' 20 | TOXENV: black-check 21 | Docs: 22 | python.version: '3.7' 23 | TOXENV: docs 24 | Build: 25 | python.version: '3.7' 26 | TOXENV: build 27 | 28 | variables: 29 | TOXENV: py 30 | POOL_IMAGE: ubuntu-16.04 31 | PYTHON: 'python' 32 | 33 | steps: 34 | - task: UsePythonVersion@0 35 | inputs: 36 | versionSpec: $(python.version) 37 | 38 | - bash: | 39 | $PYTHON -m pip install -U tox 40 | displayName: Ensure prereqs 41 | 42 | - bash: | 43 | $PYTHON -m tox 44 | if [[ $TOXENV == "py" ]]; 45 | then 46 | $PYTHON -m tox -e coverage,codecov 47 | fi 48 | displayName: Run tox 49 | 50 | ... 51 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | changes: false 5 | project: 6 | default: 7 | target: '50' 8 | 9 | comment: false 10 | codecov: 11 | token: f57afce5-949d-4c97-9198-a9d9ba300f50 12 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/code.rst: -------------------------------------------------------------------------------- 1 | .. grid-strategy code documentation 2 | 3 | Code Documentation 4 | ================== 5 | 6 | .. automodule:: grid_strategy 7 | 8 | .. automodule:: grid_strategy.strategies 9 | 10 | This is the code documentation for the implementation of 'SquareStrategy' and 'RectangularStrategy'. 11 | 12 | SquareStrategy 13 | ~~~~~~~~~~~~~~~~~ 14 | 15 | .. autoclass:: grid_strategy.strategies.SquareStrategy 16 | :members: 17 | 18 | RectanglularStrategy 19 | ~~~~~~~~~~~~~~~~~~~~~~~ 20 | 21 | .. autoclass:: grid_strategy.strategies.RectangularStrategy 22 | :members: 23 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | 18 | sys.path.insert(0, os.path.abspath("../src/")) 19 | 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = "grid-strategy" 24 | copyright = "2019, Grid Strategy Authors" 25 | author = "Grid Strategy Authors" 26 | 27 | # The short X.Y version 28 | version = "" 29 | # The full version, including alpha/beta/rc tags 30 | release = "0.0.1" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autodoc", 44 | "sphinx.ext.doctest", 45 | "sphinx.ext.coverage", 46 | "sphinx.ext.imgmath", 47 | "sphinx.ext.viewcode", 48 | ] 49 | 50 | # Add any paths that contain templates here, relative to this directory. 51 | templates_path = ["_templates"] 52 | 53 | # The suffix(es) of source filenames. 54 | # You can specify multiple suffix as a list of string: 55 | # 56 | # source_suffix = ['.rst', '.md'] 57 | source_suffix = ".rst" 58 | 59 | # The master toctree document. 60 | master_doc = "index" 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This pattern also affects html_static_path and html_extra_path. 72 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = None 76 | 77 | 78 | # -- Options for HTML output ------------------------------------------------- 79 | 80 | # The theme to use for HTML and HTML Help pages. See the documentation for 81 | # a list of builtin themes. 82 | # 83 | html_theme = "alabaster" 84 | 85 | # Theme options are theme-specific and customize the look and feel of a theme 86 | # further. For a list of options available for each theme, see the 87 | # documentation. 88 | # 89 | # html_theme_options = {} 90 | 91 | # Add any paths that contain custom static files (such as style sheets) here, 92 | # relative to this directory. They are copied after the builtin static files, 93 | # so a file named "default.css" will overwrite the builtin "default.css". 94 | html_static_path = [] 95 | 96 | # Custom sidebar templates, must be a dictionary that maps document names 97 | # to template names. 98 | # 99 | # The default sidebars (for documents that don't match any pattern) are 100 | # defined by theme itself. Builtin themes are using these templates by 101 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 102 | # 'searchbox.html']``. 103 | # 104 | # html_sidebars = {} 105 | 106 | 107 | # -- Options for HTMLHelp output --------------------------------------------- 108 | 109 | # Output file base name for HTML help builder. 110 | htmlhelp_basename = "grid-strategydoc" 111 | 112 | 113 | # -- Options for LaTeX output ------------------------------------------------ 114 | 115 | latex_elements = { 116 | # The paper size ('letterpaper' or 'a4paper'). 117 | # 118 | # 'papersize': 'letterpaper', 119 | # The font size ('10pt', '11pt' or '12pt'). 120 | # 121 | # 'pointsize': '10pt', 122 | # Additional stuff for the LaTeX preamble. 123 | # 124 | # 'preamble': '', 125 | # Latex figure (float) alignment 126 | # 127 | # 'figure_align': 'htbp', 128 | } 129 | 130 | # Grouping the document tree into LaTeX files. List of tuples 131 | # (source start file, target name, title, 132 | # author, documentclass [howto, manual, or own class]). 133 | latex_documents = [ 134 | ( 135 | master_doc, 136 | "grid-strategy.tex", 137 | "grid-strategy Documentation", 138 | "Grid Strategy Authors", 139 | "manual", 140 | ) 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [(master_doc, "grid-strategy", "grid-strategy Documentation", [author], 1)] 149 | 150 | 151 | # -- Options for Texinfo output ---------------------------------------------- 152 | 153 | # Grouping the document tree into Texinfo files. List of tuples 154 | # (source start file, target name, title, author, 155 | # dir menu entry, description, category) 156 | texinfo_documents = [ 157 | ( 158 | master_doc, 159 | "grid-strategy", 160 | "grid-strategy Documentation", 161 | author, 162 | "grid-strategy", 163 | "One line description of project.", 164 | "Miscellaneous", 165 | ) 166 | ] 167 | 168 | 169 | # -- Options for Epub output ------------------------------------------------- 170 | 171 | # Bibliographic Dublin Core info. 172 | epub_title = project 173 | 174 | # The unique identifier of the text. This can be a ISBN number 175 | # or the project homepage. 176 | # 177 | # epub_identifier = '' 178 | 179 | # A unique identification for the text. 180 | # 181 | # epub_uid = '' 182 | 183 | # A list of files that should not be packed into the epub file. 184 | epub_exclude_files = ["search.html"] 185 | 186 | 187 | # -- Extension configuration ------------------------------------------------- 188 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. grid-strategy documentation master file, created by 2 | sphinx-quickstart on Sat Feb 23 12:01:54 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | =========================== 7 | Grid Strategy Documentation 8 | =========================== 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :caption: Contents: 13 | 14 | code 15 | 16 | Indices and Tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=1.7.3,!=1.8.0 2 | -------------------------------------------------------------------------------- /images/grid_arrangement06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matplotlib/grid-strategy/35587d59cf3880dd5c240ad3f2301c5c4e41da86/images/grid_arrangement06.png -------------------------------------------------------------------------------- /images/grid_arrangement07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matplotlib/grid-strategy/35587d59cf3880dd5c240ad3f2301c5c4e41da86/images/grid_arrangement07.png -------------------------------------------------------------------------------- /images/grid_arrangement08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matplotlib/grid-strategy/35587d59cf3880dd5c240ad3f2301c5c4e41da86/images/grid_arrangement08.png -------------------------------------------------------------------------------- /images/grid_arrangement17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matplotlib/grid-strategy/35587d59cf3880dd5c240ad3f2301c5c4e41da86/images/grid_arrangement17.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>40.6.0", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [tool.coverage.run] 9 | source = ["grid_strategy"] 10 | 11 | [tool.coverage.paths] 12 | source = ["src", ".tox/*/site-packages"] 13 | 14 | [tool.coverage.report] 15 | show_missing = true 16 | skip_covered = true 17 | 18 | [tool.black] 19 | line-length = 88 20 | py36 = true 21 | include = '\.pyi?$' 22 | exclude = ''' 23 | 24 | ( 25 | /( 26 | \.eggs # exclude a few common directories in the 27 | | \.git # root of the project 28 | | \.hg 29 | | \.mypy_cache 30 | | \.tox 31 | | \.venv 32 | | _build 33 | | buck-out 34 | | build 35 | | dist 36 | )/ 37 | | foo.py # also separately exclude a file named foo.py in 38 | # the root of the project 39 | ) 40 | ''' 41 | -------------------------------------------------------------------------------- /scripts/release.py: -------------------------------------------------------------------------------- 1 | #! python3.7 2 | 3 | import glob 4 | import subprocess 5 | 6 | 7 | def upload(release): 8 | if release: 9 | repository = ["-r", "pypi"] 10 | else: 11 | repository = ["--repository-url", "https://test.pypi.org/legacy/"] 12 | 13 | dist_files = glob.glob("dists/*") 14 | args = ["twine", "upload"] + repository + dist_files 15 | 16 | subprocess.check_call(args) 17 | 18 | 19 | if __name__ == "__main__": 20 | import argparse 21 | 22 | parser = argparse.ArgumentParser(description="Make a release") 23 | parser.add_argument( 24 | "--release", action="store_true", help="Used to make a real release" 25 | ) 26 | 27 | args = parser.parse_args() 28 | 29 | upload(args.release) 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = grid-strategy 3 | version = 0.0.1 4 | description = A package for organizing matplotlib plots. 5 | author = Grid Strategy Authors 6 | author-email = paul@ganssle.io 7 | url = https://github.com/matplotlib/grid-strategy 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | license = Apache License 2.0 11 | license_file = LICENSE 12 | classifiers = 13 | Intended Audience :: Science/Research 14 | License :: OSI Approved :: Python Software Foundation License 15 | Programming Language :: Python 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3.6 18 | Programming Language :: Python :: 3.7 19 | Topic :: Scientific/Engineering :: Visualization 20 | 21 | [options] 22 | package_dir = 23 | =src 24 | packages = find: 25 | python_requires = >=3.6 26 | install_requires = 27 | matplotlib 28 | numpy 29 | 30 | [options.packages.find] 31 | where=src 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | setuptools.setup() 4 | -------------------------------------------------------------------------------- /src/grid_strategy/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["GridStrategy"] 2 | 3 | from ._abc import GridStrategy 4 | -------------------------------------------------------------------------------- /src/grid_strategy/_abc.py: -------------------------------------------------------------------------------- 1 | """Proof of concept code for MEP 30: Automatic subplot management.""" 2 | import itertools as it 3 | 4 | from abc import ABCMeta, abstractmethod 5 | 6 | from matplotlib import gridspec 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | 11 | class GridStrategy(metaclass=ABCMeta): 12 | """ 13 | Static class used to compute grid arrangements given the number of subplots 14 | you want to show. By default, it goes for a symmetrical arrangement that is 15 | nearly square (nearly equal in both dimensions). 16 | """ 17 | 18 | def __init__(self, alignment="center"): 19 | self.alignment = alignment 20 | 21 | def get_grid(self, n): 22 | """ 23 | Return a list of axes designed according to the strategy. 24 | Grid arrangements are tuples with the same length as the number of rows, 25 | and each element specifies the number of colums in the row. 26 | Ex (2, 3, 2) leads to the shape 27 | x x 28 | x x x 29 | x x 30 | where each x would be a subplot. 31 | """ 32 | 33 | grid_arrangement = self.get_grid_arrangement(n) 34 | return self.get_gridspec(grid_arrangement) 35 | 36 | @classmethod 37 | @abstractmethod 38 | def get_grid_arrangement(cls, n): # pragma: nocover 39 | pass 40 | 41 | def get_gridspec(self, grid_arrangement): 42 | nrows = len(grid_arrangement) 43 | ncols = max(grid_arrangement) 44 | 45 | # If it has justified alignment, will not be the same as the other alignments 46 | if self.alignment == "justified": 47 | return self._justified(nrows, grid_arrangement) 48 | else: 49 | return self._ragged(nrows, ncols, grid_arrangement) 50 | 51 | def _justified(self, nrows, grid_arrangement): 52 | ax_specs = [] 53 | num_small_cols = np.lcm.reduce(grid_arrangement) 54 | gs = gridspec.GridSpec( 55 | nrows, num_small_cols, figure=plt.figure(constrained_layout=True) 56 | ) 57 | for r, row_cols in enumerate(grid_arrangement): 58 | skip = num_small_cols // row_cols 59 | for col in range(row_cols): 60 | s = col * skip 61 | e = s + skip 62 | 63 | ax_specs.append(gs[r, s:e]) 64 | return ax_specs 65 | 66 | def _ragged(self, nrows, ncols, grid_arrangement): 67 | if len(set(grid_arrangement)) > 1: 68 | col_width = 2 69 | else: 70 | col_width = 1 71 | 72 | gs = gridspec.GridSpec( 73 | nrows, ncols * col_width, figure=plt.figure(constrained_layout=True) 74 | ) 75 | 76 | ax_specs = [] 77 | for r, row_cols in enumerate(grid_arrangement): 78 | # This is the number of missing columns in this row. If some rows 79 | # are a different width than others, the column width is 2 so every 80 | # column skipped at the beginning is also a missing slot at the end. 81 | if self.alignment == "left": 82 | # This is left-justified (or possibly full justification) 83 | # so no need to skip anything 84 | skip = 0 85 | elif self.alignment == "right": 86 | # Skip two slots for every missing plot - right justified. 87 | skip = (ncols - row_cols) * 2 88 | else: 89 | # Defaults to centered, as that is the default value for the class. 90 | # Skip one for each missing column - centered 91 | skip = ncols - row_cols 92 | 93 | for col in range(row_cols): 94 | s = skip + col * col_width 95 | e = s + col_width 96 | 97 | ax_specs.append(gs[r, s:e]) 98 | 99 | return ax_specs 100 | -------------------------------------------------------------------------------- /src/grid_strategy/strategies.py: -------------------------------------------------------------------------------- 1 | """Implementations of the GridStrategy class to easily graph multiple plots.""" 2 | 3 | from ._abc import GridStrategy 4 | 5 | import numpy as np 6 | 7 | import itertools as it 8 | 9 | __all__ = ["SquareStrategy", "RectangularStrategy"] 10 | 11 | 12 | class SquareStrategy(GridStrategy): 13 | SPECIAL_CASES = {3: (2, 1), 5: (2, 3)} 14 | 15 | @classmethod 16 | def get_grid_arrangement(cls, n): 17 | """ 18 | Return an arrangement of rows containing ``n`` axes that is as close to 19 | square as looks good. 20 | 21 | :param n: 22 | The number of plots in the subplot 23 | 24 | :return: 25 | Returns a :class:`tuple` of length ``nrows``, where each element 26 | represents the number of plots in that row, so for example a 3 x 2 27 | grid would be represented as ``(3, 3)``, because there are 2 rows 28 | of length 3. 29 | 30 | 31 | **Example:** 32 | 33 | .. code:: 34 | 35 | >>> GridStrategy.get_grid(7) 36 | (2, 3, 2) 37 | >>> GridStrategy.get_grid(6) 38 | (3, 3) 39 | """ 40 | if n in cls.SPECIAL_CASES: 41 | return cls.SPECIAL_CASES[n] 42 | 43 | # May not work for very large n 44 | n_sqrtf = np.sqrt(n) 45 | n_sqrt = int(np.ceil(n_sqrtf)) 46 | 47 | if n_sqrtf == n_sqrt: 48 | # Perfect square, we're done 49 | x, y = n_sqrt, n_sqrt 50 | elif n <= n_sqrt * (n_sqrt - 1): 51 | # An n_sqrt x n_sqrt - 1 grid is close enough to look pretty 52 | # square, so if n is less than that value, will use that rather 53 | # than jumping all the way to a square grid. 54 | x, y = n_sqrt, n_sqrt - 1 55 | elif not (n_sqrt % 2) and n % 2: 56 | # If the square root is even and the number of axes is odd, in 57 | # order to keep the arrangement horizontally symmetrical, using a 58 | # grid of size (n_sqrt + 1 x n_sqrt - 1) looks best and guarantees 59 | # symmetry. 60 | x, y = (n_sqrt + 1, n_sqrt - 1) 61 | else: 62 | # It's not a perfect square, but a square grid is best 63 | x, y = n_sqrt, n_sqrt 64 | 65 | if n == x * y: 66 | # There are no deficient rows, so we can just return from here 67 | return tuple(x for i in range(y)) 68 | 69 | # If exactly one of these is odd, make it the rows 70 | if (x % 2) != (y % 2) and (x % 2): 71 | x, y = y, x 72 | 73 | return cls.arrange_rows(n, x, y) 74 | 75 | @classmethod 76 | def arrange_rows(cls, n, x, y): 77 | """ 78 | Given a grid of size (``x`` x ``y``) to be filled with ``n`` plots, 79 | this arranges them as desired. 80 | 81 | :param n: 82 | The number of plots in the subplot. 83 | 84 | :param x: 85 | The number of columns in the grid. 86 | 87 | :param y: 88 | The number of rows in the grid. 89 | 90 | :return: 91 | Returns a :class:`tuple` containing a grid arrangement, see 92 | :func:`get_grid` for details. 93 | """ 94 | part_rows = (x * y) - n 95 | full_rows = y - part_rows 96 | 97 | f = (full_rows, x) 98 | p = (part_rows, x - 1) 99 | 100 | # Determine which is the more and less frequent value 101 | if full_rows >= part_rows: 102 | size_order = f, p 103 | else: 104 | size_order = p, f 105 | 106 | # ((n_more, more_val), (n_less, less_val)) = size_order 107 | args = it.chain.from_iterable(size_order) 108 | 109 | if y % 2: 110 | return cls.stripe_odd(*args) 111 | else: 112 | return cls.stripe_even(*args) 113 | 114 | @classmethod 115 | def stripe_odd(cls, n_more, more_val, n_less, less_val): 116 | """ 117 | Prepare striping for an odd number of rows. 118 | 119 | :param n_more: 120 | The number of rows with the value that there's more of 121 | 122 | :param more_val: 123 | The value that there's more of 124 | 125 | :param n_less: 126 | The number of rows that there's less of 127 | 128 | :param less_val: 129 | The value that there's less of 130 | 131 | :return: 132 | Returns a :class:`tuple` of striped values with appropriate buffer. 133 | """ 134 | (n_m, m_v) = n_more, more_val 135 | (n_l, l_v) = n_less, less_val 136 | 137 | # Calculate how much "buffer" we need. 138 | # Example (b = buffer number, o = outer stripe, i = inner stripe) 139 | # 4, 4, 5, 4, 4 -> b, o, i, o, b (buffer = 1) 140 | # 4, 5, 4, 5, 4 -> o, i, o, i, o (buffer = 0) 141 | n_inner_stripes = n_l 142 | n_buffer = (n_m + n_l) - (2 * n_inner_stripes + 1) 143 | assert n_buffer % 2 == 0, (n_more, n_less, n_buffer) 144 | n_buffer //= 2 145 | 146 | buff_tuple = (m_v,) * n_buffer 147 | stripe_tuple = (m_v, l_v) * n_inner_stripes + (m_v,) 148 | 149 | return buff_tuple + stripe_tuple + buff_tuple 150 | 151 | @classmethod 152 | def stripe_even(cls, n_more, more_val, n_less, less_val): 153 | """ 154 | Prepare striping for an even number of rows. 155 | 156 | :param n_more: 157 | The number of rows with the value that there's more of 158 | 159 | :param more_val: 160 | The value that there's more of 161 | 162 | :param n_less: 163 | The number of rows that there's less of 164 | 165 | :param less_val: 166 | The value that there's less of 167 | 168 | :return: 169 | Returns a :class:`tuple` of striped values with appropriate buffer. 170 | """ 171 | total = n_more + n_less 172 | if total % 2: 173 | msg = "Expected an even number of values, got {} + {}".format( 174 | n_more, n_less 175 | ) 176 | raise ValueError(msg) 177 | 178 | assert n_more >= n_less, (n_more, n_less) 179 | 180 | # See what the minimum unit cell is 181 | n_l_c, n_m_c = n_less, n_more 182 | num_div = 0 183 | while True: 184 | n_l_c, lr = divmod(n_l_c, 2) 185 | n_m_c, mr = divmod(n_m_c, 2) 186 | if lr or mr: 187 | break 188 | 189 | num_div += 1 190 | 191 | # Maximum number of times we can half this to get a "unit cell" 192 | n_cells = 2 ** num_div 193 | 194 | # Make the largest possible odd unit cell 195 | cell_s = total // n_cells # Size of a unit cell 196 | 197 | cell_buff = int(cell_s % 2 == 0) # Buffer is either 1 or 0 198 | cell_s -= cell_buff 199 | cell_nl = n_less // n_cells 200 | cell_nm = cell_s - cell_nl 201 | 202 | if cell_nm == 0: 203 | stripe_cell = (less_val,) 204 | else: 205 | stripe_cell = cls.stripe_odd(cell_nm, more_val, cell_nl, less_val) 206 | 207 | unit_cell = (more_val,) * cell_buff + stripe_cell 208 | 209 | if num_div == 0: 210 | return unit_cell 211 | 212 | stripe_out = unit_cell * (n_cells // 2) 213 | return tuple(reversed(stripe_out)) + stripe_out 214 | 215 | 216 | class RectangularStrategy(GridStrategy): 217 | """Provide a nearest-to-square rectangular grid.""" 218 | 219 | @classmethod 220 | def get_grid_arrangement(cls, n): 221 | """ 222 | Retrieves the grid arrangement that is the nearest-to-square rectangular 223 | arrangement of plots. 224 | 225 | :param n: 226 | The number of subplots in the plot. 227 | 228 | :return: 229 | Returns a :class:`tuple` of length ``nrows``, where each element 230 | represents the number of plots in that row, so for example a 3 x 2 231 | grid would be represented as ``(3, 3)``, because there are 2 rows 232 | of length 3. 233 | """ 234 | # May not work for very large n because of the float sqrt 235 | # Get the two closest factors (may have problems for very large n) 236 | step = 2 if n % 2 else 1 237 | for i in range(int(np.sqrt(n)), 0, -step): 238 | if n % i == 0: 239 | x, y = n // i, i 240 | break 241 | else: 242 | x, y = n, 1 243 | 244 | # Convert this into a grid arrangement 245 | return tuple(x for i in range(y)) 246 | -------------------------------------------------------------------------------- /tests/test_grids.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest import mock 3 | 4 | from grid_strategy.strategies import SquareStrategy 5 | 6 | 7 | class SpecValue: 8 | def __init__(self, rows, cols, parent=None): 9 | self.rows = rows 10 | self.cols = cols 11 | self.parent = parent 12 | 13 | def __repr__(self): # pragma: nocover 14 | return f"{self.__class__.__name__}({self.rows}, {self.cols})" 15 | 16 | def __eq__(self, other): 17 | return self.rows == other.rows and self.cols == other.cols 18 | 19 | 20 | class GridSpecMock: 21 | def __init__(self, nrows, ncols, *args, **kwargs): 22 | self._nrows_ = nrows 23 | self._ncols_ = ncols 24 | 25 | self._args_ = args 26 | self._kwargs_ = kwargs 27 | 28 | def __getitem__(self, key_tup): 29 | return SpecValue(*key_tup, self) 30 | 31 | 32 | @pytest.fixture 33 | def gridspec_mock(): 34 | class Figure: 35 | pass 36 | 37 | def figure(*args, **kwargs): 38 | return Figure() 39 | 40 | with mock.patch(f"grid_strategy._abc.gridspec.GridSpec", new=GridSpecMock) as g: 41 | with mock.patch(f"grid_strategy._abc.plt.figure", new=figure): 42 | yield g 43 | 44 | 45 | @pytest.mark.parametrize( 46 | "align, n, exp_specs", 47 | [ 48 | ("center", 1, [(0, slice(0, 1))]), 49 | ("center", 2, [(0, slice(0, 1)), (0, slice(1, 2))]), 50 | ("center", 3, [(0, slice(0, 2)), (0, slice(2, 4)), (1, slice(1, 3))]), 51 | ("left", 3, [(0, slice(0, 2)), (0, slice(2, 4)), (1, slice(0, 2))]), 52 | ("right", 3, [(0, slice(0, 2)), (0, slice(2, 4)), (1, slice(2, 4))]), 53 | ("justified", 3, [(0, slice(0, 1)), (0, slice(1, 2)), (1, slice(0, 2))]), 54 | ( 55 | "center", 56 | 8, 57 | [ 58 | (0, slice(0, 2)), 59 | (0, slice(2, 4)), 60 | (0, slice(4, 6)), 61 | (1, slice(1, 3)), 62 | (1, slice(3, 5)), 63 | (2, slice(0, 2)), 64 | (2, slice(2, 4)), 65 | (2, slice(4, 6)), 66 | ], 67 | ), 68 | ("left", 2, [(0, slice(0, 1)), (0, slice(1, 2))]), 69 | ], 70 | ) 71 | def test_square_spec(gridspec_mock, align, n, exp_specs): 72 | ss = SquareStrategy(align) 73 | 74 | act = ss.get_grid(n) 75 | exp = [SpecValue(*spec) for spec in exp_specs] 76 | 77 | assert act == exp 78 | -------------------------------------------------------------------------------- /tests/test_strategies.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from grid_strategy import strategies 4 | 5 | 6 | @pytest.fixture 7 | def rectangular_strategy(): 8 | return strategies.RectangularStrategy() 9 | 10 | 11 | @pytest.fixture 12 | def square_strategy(): 13 | return strategies.SquareStrategy() 14 | 15 | 16 | # Test the rectangular strategy to see if the get_grid_arrangement returns the right tuple. 17 | @pytest.mark.parametrize( 18 | "num_plots, grid_arrangement", 19 | [ 20 | (1, (1,)), 21 | (2, (2,)), 22 | (3, (3,)), 23 | (4, (2, 2)), 24 | (5, (5,)), 25 | (6, (3, 3)), 26 | (10, (5, 5)), 27 | (12, (4, 4, 4)), 28 | (20, (5, 5, 5, 5)), 29 | ], 30 | ) 31 | def test_rectangular_strategy(rectangular_strategy, num_plots, grid_arrangement): 32 | assert rectangular_strategy.get_grid_arrangement(num_plots) == grid_arrangement 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "num_plots, grid_arrangement", 37 | [ 38 | (1, (1,)), 39 | (2, (2,)), 40 | (3, (2, 1)), 41 | (4, (2, 2)), 42 | (5, (2, 3)), 43 | (6, (3, 3)), 44 | (7, (2, 3, 2)), 45 | (8, (3, 2, 3)), 46 | (9, (3, 3, 3)), 47 | (10, (3, 4, 3)), 48 | (12, (4, 4, 4)), 49 | (14, (3, 4, 4, 3)), 50 | (17, (3, 4, 3, 4, 3)), 51 | (20, (5, 5, 5, 5)), 52 | (31, (6, 6, 7, 6, 6)), 53 | (34, (6, 5, 6, 6, 5, 6)), 54 | (58, (7, 8, 7, 7, 7, 7, 8, 7)), 55 | (94, (9, 10, 9, 10, 9, 9, 10, 9, 10, 9)), 56 | ], 57 | ) 58 | def test_square_strategy(square_strategy, num_plots, grid_arrangement): 59 | assert square_strategy.get_grid_arrangement(num_plots) == grid_arrangement 60 | 61 | 62 | # Test for bad input 63 | @pytest.mark.parametrize("n", [-1, -1000]) 64 | def test_rectangular_strategy_with_bad_input(rectangular_strategy, n): 65 | with pytest.raises(ValueError): 66 | rectangular_strategy.get_grid(n) 67 | 68 | 69 | @pytest.mark.parametrize("n", [-1, -1000]) 70 | def test_square_strategy_with_bad_input(square_strategy, n): 71 | with pytest.raises(ValueError): 72 | square_strategy.get_grid(n) 73 | 74 | 75 | # Test for the `stripe_even` functions - it is not entirely clear that these 76 | # will remain public, so do not take the fact that it is tested as an 77 | # indication that this is a critical part of the public interface 78 | @pytest.mark.parametrize( 79 | "args, exp", [((4, 3, 2, 4), (3, 4, 3, 3, 4, 3)), ((3, 2, 1, 1), (2, 2, 1, 2))] 80 | ) 81 | def test_stripe_even(args, exp): 82 | act = strategies.SquareStrategy.stripe_even(*args) 83 | 84 | assert act == exp 85 | 86 | 87 | def test_stripe_even_value_error(): 88 | # This fails when the total number (n_more + n_less) is not even 89 | with pytest.raises(ValueError): 90 | strategies.SquareStrategy.stripe_even(3, 1, 4, 1) 91 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36, py37, py38, black-check 3 | skip_missing_interpreters = true 4 | isolated_build = true 5 | 6 | [testenv] 7 | setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} 8 | passenv = TOXENV CI CODECOV_* SYSTEM_* AGENT_* BUILD_* TF_BUILD 9 | deps = 10 | pytest 11 | pytest-cov >= 2.0.0 12 | coverage[toml] >=5.0.2 13 | commands = pytest {toxinidir} \ 14 | --cov="grid_strategy" \ 15 | --cov="tests" \ 16 | {posargs} 17 | 18 | [testenv:coverage] 19 | description = combine coverage data and create reports 20 | deps = coverage[toml] >= 5.0.2 21 | skip_install = True 22 | changedir = {toxworkdir} 23 | setenv = COVERAGE_FILE=.coverage 24 | commands = coverage erase 25 | coverage combine 26 | coverage report 27 | coverage xml 28 | 29 | [testenv:codecov] 30 | description = [only run on CI]: upload coverage data to codecov (depends on coverage running first) 31 | deps = codecov 32 | skip_install = True 33 | commands = codecov --file {toxworkdir}/coverage.xml 34 | 35 | [testenv:black-check] 36 | description = test if black works 37 | deps = 38 | pytest-black 39 | commands = pytest --black 40 | 41 | [testenv:docs] 42 | description = invoke sphinx-build to build the HTML docs, check that URIs are valid 43 | basepython = python3.6 44 | deps = -r docs/requirements-docs.txt 45 | commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs \ 46 | "{toxworkdir}/docs_out" {posargs:-W --color} 47 | sphinx-build -d "{toxworkdir}/docs_doctree" docs \ 48 | "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck} 49 | 50 | [testenv:build] 51 | description = Build the documentation 52 | deps = twine 53 | pep517 54 | readme_renderer[md] >= 24.0 55 | commands = python -m pep517.build --source --binary {toxinidir} \ 56 | --out-dir {toxinidir}/dists 57 | twine check {toxinidir}/dists/* 58 | 59 | [testenv:release] 60 | description = Make a pypi release 61 | basepython = python3.7 62 | passenv = * 63 | deps = twine 64 | commands = python scripts/release.py {posargs} 65 | --------------------------------------------------------------------------------