├── .flake8 ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.txt ├── README.md ├── development_notes.md ├── docs ├── Makefile ├── conf.py ├── images │ ├── advanced.png │ ├── basic.png │ ├── braille.png │ ├── cameraman.png │ ├── cameraman_blocks.png │ ├── categorical.png │ ├── misalignment.png │ ├── multivariate_gaussian_ascii.png │ ├── multivariate_gaussian_block.png │ ├── spamspamspamspam.png │ └── twinkle_twinkle_little_star.png ├── index.rst ├── make.bat └── requirements.txt ├── pyproject.toml ├── readthedocs.yml ├── tests ├── __init__.py ├── cameraman.png ├── reference_figures │ ├── axis_labels.txt │ ├── bar_anscombe.txt │ ├── bar_cheese_or_chocolate.txt │ ├── bar_iris.txt │ ├── braille_bar_anscombe.txt │ ├── braille_bar_cheese_or_chocolate.txt │ ├── braille_bar_iris.txt │ ├── braille_hbar_anscombe.txt │ ├── braille_hbar_cheese_or_chocolate.txt │ ├── braille_hbar_iris.txt │ ├── braille_line_anscombe.txt │ ├── braille_line_cheese_or_chocolate.txt │ ├── braille_line_iris.txt │ ├── braille_scatter_anscombe.txt │ ├── braille_scatter_cheese_or_chocolate.txt │ ├── braille_scatter_iris.txt │ ├── colors.txt │ ├── hbar_anscombe.txt │ ├── hbar_cheese_or_chocolate.txt │ ├── hbar_iris.txt │ ├── image.txt │ ├── image_ascii.txt │ ├── image_big_values.txt │ ├── image_cameraman.txt │ ├── image_small_values.txt │ ├── image_vmin_vmax.txt │ ├── legendloc_bottomleft.txt │ ├── legendloc_bottomright.txt │ ├── legendloc_topleft.txt │ ├── legendloc_topright.txt │ ├── line_anscombe.txt │ ├── line_cheese_or_chocolate.txt │ ├── line_iris.txt │ ├── scatter_anscombe.txt │ ├── scatter_cheese_or_chocolate.txt │ ├── scatter_iris.txt │ ├── text.txt │ ├── y_axis_down_anscombe.txt │ ├── y_axis_down_cheese_or_chocolate.txt │ ├── y_axis_down_iris.txt │ ├── y_axis_up.txt │ └── y_only.txt ├── test_braille.py ├── test_img2ascii.py ├── test_reference_figures.py ├── test_scales.py └── test_xticklabels.py └── tplot ├── __init__.py ├── braille.py ├── figure.py ├── img2ascii.py ├── scales.py └── utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503, F403, F401 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4,B9 6 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: true 8 | matrix: 9 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 10 | os: [ubuntu-latest, macos-latest] 11 | runs-on: ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: pip install .[dev] 20 | - name: Run tests 21 | run: pytest --cov=tplot tests/ --cov-report=xml 22 | - name: Upload coverage to Codecov 23 | uses: codecov/codecov-action@v3 24 | with: 25 | fail_ci_if_error: false 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | *.mp4 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: "tests/reference_figures" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v2.3.0 5 | hooks: 6 | - id: check-yaml 7 | - id: check-toml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/psf/black 11 | rev: 23.3.0 12 | hooks: 13 | - id: black 14 | - repo: https://github.com/pycqa/isort 15 | rev: 5.12.0 16 | hooks: 17 | - id: isort 18 | name: isort (python) 19 | - repo: https://github.com/pycqa/flake8 20 | rev: 6.0.0 21 | hooks: 22 | - id: flake8 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeroen Emile Delcour 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tplot 2 | 3 | [![Documentation status](https://readthedocs.org/projects/tplot/badge/)](https://tplot.readthedocs.io/en/latest/) 4 | [![Tests status](https://github.com/JeroenDelcour/tplot/actions/workflows/tests.yml/badge.svg)](https://github.com/JeroenDelcour/tplot/actions/workflows/tests.yml) 5 | [![codecov](https://codecov.io/gh/JeroenDelcour/tplot/branch/master/graph/badge.svg?token=WXH7I3BGEO)](https://codecov.io/gh/JeroenDelcour/tplot) 6 | 7 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/tplot)](https://pypi.org/project/tplot/) 8 | [![PyPI version](https://img.shields.io/pypi/v/tplot)](https://pypi.org/project/tplot/) 9 | [![License](https://img.shields.io/github/license/jeroendelcour/tplot)](https://github.com/JeroenDelcour/tplot/blob/master/LICENSE) 10 | 11 | `tplot` is a Python package for creating text-based graphs. Useful for visualizing data to the terminal or log files. 12 | 13 | ## Features 14 | 15 | - Scatter plots, line plots, horizontal/vertical bar plots, and image plots 16 | - Supports numerical and categorical data 17 | - Legend 18 | - Unicode characters (with automatic ascii fallback) 19 | - Colors 20 | - Few dependencies 21 | - Fast and lightweight 22 | - Doesn't take over your terminal (only prints strings) 23 | 24 | ## Installation 25 | 26 | `tplot` is available on [PyPi](https://pypi.org/project/tplot/): 27 | 28 | ```bash 29 | pip install tplot 30 | ``` 31 | 32 | ## Documentation 33 | 34 | Documentation is available on [readthedocs](https://tplot.readthedocs.io/en/latest/). 35 | 36 | ## Examples 37 | 38 | ### Basic usage 39 | 40 | ```python 41 | import tplot 42 | 43 | fig = tplot.Figure() 44 | fig.scatter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 45 | fig.show() 46 | ``` 47 | 48 | ![Basic example](docs/images/basic.png) 49 | 50 | ### A more advanced example 51 | 52 | ```python 53 | import tplot 54 | import numpy as np 55 | 56 | x = np.linspace(start=0, stop=np.pi*3, num=80) 57 | 58 | fig = tplot.Figure( 59 | xlabel="Phase", 60 | ylabel="Amplitude", 61 | title="Trigonometric functions", 62 | legendloc="bottomleft", 63 | width=60, 64 | height=15, 65 | ) 66 | fig.line(x, y=np.sin(x), color="red", label="sin(x)") 67 | fig.line(x, y=np.cos(x), color="blue", label="cos(x)") 68 | fig.show() 69 | ``` 70 | 71 | ![Advanced example](docs/images/advanced.png) 72 | 73 | See more examples in the [documentation](https://tplot.readthedocs.io/en/latest/). 74 | 75 | ## Contributing 76 | 77 | Contributions are welcome. Bug fixes, feature suggestions, documentation improvements etc. can be contributed via [issues](https://github.com/JeroenDelcour/tplot/issues) and/or [pull requests](https://github.com/JeroenDelcour/tplot/pulls). 78 | -------------------------------------------------------------------------------- /development_notes.md: -------------------------------------------------------------------------------- 1 | Development notes 2 | ----------------- 3 | 4 | Because I forget. 5 | 6 | ## Setup 7 | 8 | Install dev dependencies: `pip install ".[dev]"` 9 | 10 | Install pre-commit hooks: `pre-commit install` 11 | 12 | For code formatting, `black` and `isort` are used with default settings. 13 | 14 | ## Unit tests 15 | 16 | `pytest` is used to run unit tests. 17 | 18 | Generated test figures are checked if they match with a set of reference figures in the `tests/reference_figures` directory. For some code changes, they may no longer match character-for-character, but still look good. If this is acceptable, you can generate new reference figures. 19 | 20 | ### Generating reference figures 21 | 22 | Set the `GENERATE` variable at the top of `tests/test_reference_figures.py` to `True` and run the tests again. You use `git status` to tell you which reference plots have changed, since they're just text files. Use your own eyes to look at any changes. If they look good, set the `GENERATE` flag back to `False` and commit the new refernce figures to git. 23 | 24 | ## Creating a new release 25 | 26 | - Bump the version in `pyproject.toml` 27 | - [Create an annotated git tag with the version string](https://stackoverflow.com/questions/11514075/what-is-the-difference-between-an-annotated-and-unannotated-tag): `git tag -a v0.2.0 -m ""` 28 | - [Git push with tags](https://stackoverflow.com/questions/5195859/how-do-you-push-a-tag-to-a-remote-repository-using-git): `git push --follow-tags` 29 | - [Publish on pypi](https://flit.pypa.io/en/stable/): `flit publish` 30 | - [Create a release on GitHub](https://github.com/JeroenDelcour/tplot/releases/new) 31 | 32 | Documentation is built automatically by readthedocs. To build locally, run: `cd docs && make html` 33 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = "tplot" 21 | copyright = "2022, Jeroen Delcour" 22 | author = "Jeroen Delcour" 23 | 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | master_doc = "index" 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_rtd_theme"] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ["_templates"] 36 | 37 | # List of patterns, relative to source directory, that match files and 38 | # directories to ignore when looking for source files. 39 | # This pattern also affects html_static_path and html_extra_path. 40 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 41 | 42 | autodoc_default_options = { 43 | "member-order": "bysource", 44 | } 45 | 46 | 47 | # -- Options for HTML output ------------------------------------------------- 48 | 49 | # The theme to use for HTML and HTML Help pages. See the documentation for 50 | # a list of builtin themes. 51 | # 52 | html_theme = "sphinx_rtd_theme" 53 | 54 | # Add any paths that contain custom static files (such as style sheets) here, 55 | # relative to this directory. They are copied after the builtin static files, 56 | # so a file named "default.css" will overwrite the builtin "default.css". 57 | html_static_path = ["_static"] 58 | -------------------------------------------------------------------------------- /docs/images/advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/advanced.png -------------------------------------------------------------------------------- /docs/images/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/basic.png -------------------------------------------------------------------------------- /docs/images/braille.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/braille.png -------------------------------------------------------------------------------- /docs/images/cameraman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/cameraman.png -------------------------------------------------------------------------------- /docs/images/cameraman_blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/cameraman_blocks.png -------------------------------------------------------------------------------- /docs/images/categorical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/categorical.png -------------------------------------------------------------------------------- /docs/images/misalignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/misalignment.png -------------------------------------------------------------------------------- /docs/images/multivariate_gaussian_ascii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/multivariate_gaussian_ascii.png -------------------------------------------------------------------------------- /docs/images/multivariate_gaussian_block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/multivariate_gaussian_block.png -------------------------------------------------------------------------------- /docs/images/spamspamspamspam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/spamspamspamspam.png -------------------------------------------------------------------------------- /docs/images/twinkle_twinkle_little_star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/docs/images/twinkle_twinkle_little_star.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | tplot 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | ``tplot`` is a Python package for creating text-based graphs. Useful for visualizing data to the terminal or log files. 9 | 10 | Features 11 | -------- 12 | 13 | * Scatter plots, line plots, horizontal/vertical bar plots, and image plots 14 | * Supports numerical and categorical data 15 | * Legend 16 | * Unicode characters (with automatic ascii fallback) 17 | * Colors 18 | * Few dependencies 19 | * Fast and lightweight 20 | * Doesn't take over your terminal (only prints strings) 21 | 22 | Installation 23 | ------------ 24 | 25 | ``tplot`` is available on `PyPI `_: 26 | 27 | .. code-block:: bash 28 | 29 | pip install tplot 30 | 31 | Examples 32 | ======== 33 | 34 | Basic usage 35 | ----------- 36 | :: 37 | 38 | import tplot 39 | 40 | fig = tplot.Figure() 41 | fig.scatter([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) 42 | fig.show() 43 | 44 | .. image:: images/basic.png 45 | 46 | A more advanced example 47 | ----------------------- 48 | 49 | :: 50 | 51 | import tplot 52 | import numpy as np 53 | 54 | x = np.linspace(start=0, stop=np.pi*3, num=80) 55 | 56 | fig = tplot.Figure( 57 | xlabel="Phase", 58 | ylabel="Amplitude", 59 | title="Trigonometric functions", 60 | legendloc="bottomleft", 61 | width=60, 62 | height=15, 63 | ) 64 | fig.line(x, y=np.sin(x), color="red", label="sin(x)") 65 | fig.line(x, y=np.cos(x), color="blue", label="cos(x)") 66 | fig.show() 67 | 68 | .. image:: images/advanced.png 69 | 70 | Categorical data 71 | ---------------- 72 | 73 | ``tplot`` supports categorical data in the form of strings:: 74 | 75 | import tplot 76 | 77 | dish = ["Pasta", "Ice cream", "Rice", "Waffles", "Pancakes"] 78 | topping = ["Cheese", "Chocolate", "Cheese", "Chocolate", "Chocolate"] 79 | 80 | fig = tplot.Figure(height=7, title="Chocolate or cheese?") 81 | fig.scatter(x=dish, y=topping, marker="X") 82 | fig.show() 83 | 84 | .. image:: images/categorical.png 85 | 86 | Numerical and categorical data can be combined in the same plot:: 87 | 88 | from collections import Counter 89 | import tplot 90 | 91 | counter = Counter( 92 | ["Spam", "sausage", "Spam", "Spam", "Spam", "bacon", "Spam", "tomato", "Spam"] 93 | ) 94 | 95 | fig = tplot.Figure(ylabel="Count") 96 | fig.bar(x=counter.keys(), y=counter.values()) 97 | fig.show() 98 | 99 | .. image:: images/spamspamspamspam.png 100 | 101 | Markers 102 | ------- 103 | 104 | ``tplot`` allows you to use any single character as a marker:: 105 | 106 | import tplot 107 | 108 | fig = tplot.Figure(title="Twinkle twinkle little ★") 109 | notes = ["C", "C", "G", "G", "a", "a", "G", "F", "F", "E", "E", "D", "D", "C"] 110 | fig.scatter(notes, marker="♩") 111 | fig.show() 112 | 113 | .. image:: images/twinkle_twinkle_little_star.png 114 | 115 | .. warning:: 116 | Be wary of using `fullwidth characters `_, because these will mess up alignment (see :ref:`character-alignment-issues`). 117 | 118 | Images (2D arrays) 119 | ------------------ 120 | 121 | ``tplot`` can visualize 2D arrays using a few different "colormaps":: 122 | 123 | import tplot 124 | import numpy as np 125 | 126 | mean = np.array([[0, 0]]).T 127 | covariance = np.array([[0.7, 0.4], [0.4, 0.7]]) 128 | cov_inv = np.linalg.inv(covariance) 129 | cov_det = np.linalg.det(covariance) 130 | 131 | x = np.linspace(-2, 2) 132 | y = np.linspace(-2, 2) 133 | X, Y = np.meshgrid(x, y) 134 | coe = 1.0 / ((2 * np.pi) ** 2 * cov_det) ** 0.5 135 | z = coe * np.e ** ( 136 | -0.5 137 | * ( 138 | cov_inv[0, 0] * (X - mean[0]) ** 2 139 | + (cov_inv[0, 1] + cov_inv[1, 0]) * (X - mean[0]) * (Y - mean[1]) 140 | + cov_inv[1, 1] * (Y - mean[1]) ** 2 141 | ) 142 | ) 143 | 144 | fig = tplot.Figure(title="Multivariate gaussian") 145 | fig.image(z, cmap="block") 146 | fig.show() 147 | 148 | .. image:: images/multivariate_gaussian_block.png 149 | 150 | :: 151 | 152 | fig = tplot.Figure(title="Multivariate gaussian") 153 | fig.image(z, cmap="ascii") 154 | fig.show() 155 | 156 | .. image:: images/multivariate_gaussian_ascii.png 157 | 158 | 159 | Images can also be shown, if first converted to a Numpy array of type ``uint8``: 160 | 161 | .. image:: images/cameraman.png 162 | 163 | :: 164 | 165 | import tplot 166 | from PIL import Image 167 | import numpy as np 168 | 169 | cameraman = Image.open("cameraman.png") 170 | cameraman = np.array(cameraman, dtype=np.uint8) 171 | fig = tplot.Figure() 172 | fig.image(cameraman) 173 | fig.show() 174 | 175 | .. image:: images/cameraman_blocks.png 176 | 177 | Formatting issues 178 | ================= 179 | 180 | Writing to a file and color support 181 | ----------------------------------- 182 | 183 | You can get the figure as a string simply by converting to to the ``str`` type: ``str(fig)`` 184 | 185 | However, if your figure has colors in it and you try to write it to a file (or copy and paste it from the terminal), it will look wrong: 186 | 187 | .. code-block:: text 188 | 189 | Trigonometric functions \n \nA 1┤\x1b[34m⠐\x1b[0m\x1b[34m⠢\x1b[0m\x1b[34m⠤\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⡠\x1b[0m\x1b[31m⠤\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠢\x1b[0m\x1b[31m⡀\x1b[0m \x1b[34m⡠\x1b[0m\x1b[34m⠒\x1b[0m\x1b[34m⠒\x1b[0m\x1b[34m⠢\x1b[0m\x1b[34m⠤\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⡠\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠢\x1b[0m\x1b[31m⠤\x1b[0m\x1b[31m⡀\x1b[0m \nm │ \x1b[34m⠈\x1b[0m\x1b[34m⣢\x1b[0m\x1b[34m⡜\x1b[0m \x1b[31m⠈\x1b[0m\x1b[31m⠱\x1b[0m\x1b[31m⡀\x1b[0m \x1b[34m⡔\x1b[0m\x1b[34m⠊\x1b[0m \x1b[34m⠑\x1b[0m\x1b[34m⣴\x1b[0m\x1b[31m 190 | ⠊\x1b[0m \x1b[31m⠘\x1b[0m\x1b[31m⠤\x1b[0m\x1b[31m⡀\x1b[0m \np 0.5┤ \x1b[31m⡰\x1b[0m\x1b[31m⠁\x1b[0m\x1b[34m⠑ 191 | \x1b[0m\x1b[34m⡄\x1b[0m \x1b[31m⠘\x1b[0m\x1b[31m⢄\x1b[0m \x1b[34m⢀\x1b[0m\x1b[34m⠜\x1b[0m \x1b[31m⢀ 192 | \x1b[0m\x1b[31m⠜\x1b[0m \x1b[34m⠱\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⠱\x1b[0m\x1b[31m⡀\x1b[0m \nl │ \x1b[31m⢀\x1b[0m\x1b[31m⠔\x1b[0m\x1b[31m⠁\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⢢\x1b[0m \x1b[31m⢣\x1b[0m \x1b[34m⢠\x1b[0m\x1b[34m⠃\x1b[0m \x1b[31m⢠\x1b[0m\x1b[31m⠃\x1b[0m \x1b[34m⠱\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⠑\x1b[0m\x1b[31m⢄ 193 | \x1b[0m \ni │\x1b[31m⢀\x1b[0m\x1b[31m⠎\x1b[0m \x1b[34m⢇\x1b[0m \x1b[31m⢣\x1b[0m \x1b[34m⡠\x1b[0m\x1b[34m⠃\x1b[0m \x1b[31m⢠\x1b[0m\x1b[31m⠃\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⢆\x1b[0m \x1b[31m⠈\x1b[0m\x1b[31m⢆\x1b[0m \nt 0┤ \x1b[34m⠈\x1b[0m\x1b[34m⠢\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⠱\x1b[0m\x1b[31m⡀\x1b[0m \x1b[34m⡜\x1b[0m \x1b[31m⡰\x1b[0m\x1b[31m⠁\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⢆\x1b[0m \nu │┌─Legend─┐\x1b[34m⠱\x1b[0m\x1b[34m⡀\x1b[0m \x1b[31m⠱\x1b[0m\x1b[31m⡀\x1b[0m \x1b[34m⢀\x1b[0m\x1b[34m⠜\x1b[0m \x1b[31m⡰\x1b[0m\x1b[31m⠁\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⢢\x1b[0m \nd -0.5┤│\x1b[31m⠄\x1b[0m sin(x)│ \x1b[34m 194 | ⠑\x1b[0m\x1b[34m⢄\x1b[0m \x1b[31m⠑\x1b[0m\x1b[31m⢢\x1b[0m\x1b[34m⢠\x1b[0m\x1b[34m⠃\x1b[0m \x1b[31m⢠\x1b[0m\x1b[31m⠔\x1b[0m\x1b[31m⠁\x1b[0m \x1b[34m⠱\x1b[0m\x1b[34m⡀\x1b[0m \ne ││\x1b[34m⠄\x1b[0m cos(x)│ \x1b[34m⠱\x1b[0m\x1b[34m⣀\x1b[0m \x1b[34m⡠\x1b[0m\x1b[34m⠔\x1b[0m\x1b[34m⠑\x1b[0m\x1b[31m⠤\x1b[0m\x1b[31m⡀\x1b[0m \x1b[31m⣀\x1b[0m\x1b[31m⠔\x1b[0m\x1b[31m⠁\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⠢\x1b[0m\x1b[34m⡀\x1b[0m \n -1┤ 195 | └────────┘ \x1b[34m⠉\x1b[0m\x1b[34m⠒\x1b[0m\x1b[34m⠒\x1b[0m\x1b[34m⠊\x1b[0m \x1b[31m⠈\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠒\x1b[0m\x1b[31m⠊\x1b[0m \x1b[34m⠈\x1b[0m\x1b[34m⠉\x1b[0m\x1b[34m⠒\x1b[0m \n ┬─────────┬── 196 | ────────┬─────────┬──────────┬─────────┬\n 0 2 4 6 8 10\n Phase 197 | 198 | This is because ``tplot`` uses ANSI escape characters to display colors, which only work in the terminal. Regular text files do not support colored text. If you want to write figures to a text file (e.g. for logging purposes), it is best to avoid the use of color. 199 | 200 | Converting to HTML 201 | ------------------ 202 | 203 | You can use a tool such as `ansi2html `_ to format the figure using HTML:: 204 | 205 | from ansi2html import Ansi2HTMLConverter 206 | 207 | conv = Ansi2HTMLConverter(markup_lines=True) 208 | html = conv.convert(str(fig)) 209 | with open("fig.html", "w") as f: 210 | f.write(html) 211 | 212 | But if you're using unicode characters (such as braille), you will probably run into character alignment issues. 213 | 214 | .. _character-alignment-issues: 215 | 216 | Character alignment issues 217 | -------------------------- 218 | 219 | Fullwidth and halfwidth characters 220 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 221 | 222 | ``tplot`` assumes all characters have a fixed width. Unless you're crazy, your terminal uses a monospace font. But even for monospaced fonts, the wondrous world of unicode has three character widths: `zero-width `_, `halfwidth (so called because they are half as wide as they are tall) and fullwidth `_. Most characters you know are probably halfwidth. Many Asian languages such as Chinese, Korean, and Japanese use fullwidth characters. Emoji are also usually fullwidth. 223 | 224 | Though fullwidth characters are exactly twice as wide as halfwidth characters, they still count as only one character. ``tplot`` will not stop you from using fullwidth characters, but it will mess up the alignment, potentially even causing lines to wrap around:: 225 | 226 | import tplot 227 | 228 | fig = tplot.Figure(width=80, height=6) 229 | x = [0, 1, 2, 3, 4, 5] 230 | fig.scatter(x, y=[1, 1, 1, 1, 1, 1], marker="#") 231 | fig.scatter(x, y=[0, 0, 0, 0, 0, 0], marker="💩") 232 | fig.show() 233 | 234 | .. image:: images/misalignment.png 235 | 236 | Braille 237 | ^^^^^^^ 238 | 239 | ``tplot`` can use braille characters to subdivide each character into a 2x4 grid (⣿), increasing the resolution of the plot beyond what is possible with single characters. However, not all monospace fonts display braille characters the same. In Ubuntu's default terminal with the default Monospace Regular font, braille characters are rendered as halfwidth characters, so this will show a nice diagonal line:: 240 | 241 | import tplot 242 | 243 | fig = tplot.Figure() 244 | fig.line([0,1], marker="braille") 245 | fig.show() 246 | 247 | .. image:: images/braille.png 248 | 249 | But many environments treat braille as somewhere in between halfwidth and fullwidth characters, leading to close-but-not-quite aligned plots: 250 | 251 | .. code-block:: text 252 | 253 | 254 | 1┤ ⣀⣀⠤⠤⠔⠒ 255 | │ ⢀⣀⣀⠤⠤⠒⠒⠊⠉⠉ 256 | │ ⣀⣀⡠⠤⠤⠒⠒⠉⠉⠁ 257 | │ ⣀⣀⠤⠤⠔⠒⠒⠉⠉ 258 | 0.5┤ ⢀⣀⡠⠤⠤⠒⠒⠊⠉⠉ 259 | │ ⣀⣀⡠⠤⠔⠒⠒⠉⠉⠁ 260 | │ ⢀⣀⣀⠤⠤⠔⠒⠊⠉⠉ 261 | │ ⢀⣀⡠⠤⠤⠒⠒⠊⠉⠁ 262 | 0┤⠐⠒⠉⠉⠁ 263 | ┬───────┬──────┬───────┬──────┬───────┬──────┬───────┬──────┬───────┬──────┬ 264 | 0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1 265 | 266 | 267 | API reference 268 | ============= 269 | 270 | .. autoclass:: tplot.Figure 271 | :members: 272 | :undoc-members: 273 | 274 | Indices and tables 275 | ================== 276 | 277 | * :ref:`genindex` 278 | * :ref:`modindex` 279 | * :ref:`search` 280 | -------------------------------------------------------------------------------- /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.https://www.sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "tplot" 3 | version = "0.3.6" 4 | description = "Create text-based graphs" 5 | readme = "README.md" 6 | requires-python = ">=3.8,<4" 7 | license = {file = "LICENSE.txt"} 8 | authors = [{name = "Jeroen Delcour", email = "jeroendelcour@gmail.com"}] 9 | classifiers = [ 10 | "License :: OSI Approved :: MIT License", 11 | "Natural Language :: English", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python :: 3", 14 | "Programming Language :: Python :: 3.8", 15 | "Programming Language :: Python :: 3.9", 16 | "Programming Language :: Python :: 3.10", 17 | "Programming Language :: Python :: 3.11", 18 | "Programming Language :: Python :: 3.12", 19 | ] 20 | dependencies = [ 21 | "colorama >=0.4.3", 22 | "numpy >=1.11", 23 | "termcolor-whl >=1.1.0", 24 | ] 25 | 26 | [project.optional-dependencies] 27 | dev = [ 28 | "flit >=3.9.0", 29 | "pytest >=7.3.2", 30 | "pytest-cov >=4.1.0", 31 | "coverage >=6.2", 32 | "black >=23.0.0", 33 | "isort >=5.12.0", 34 | "flake8 >=4.0.1", 35 | "pillow >=8.3.2", 36 | "pre-commit >=2.16.0", 37 | "mypy >=0.931", 38 | "Sphinx >=7.1.2", 39 | "sphinx-rtd-theme >=2.0.0", 40 | ] 41 | 42 | [project.urls] 43 | Documentation = "https://tplot.readthedocs.io/en/latest/" 44 | Source = "https://github.com/JeroenDelcour/tplot" 45 | 46 | [build-system] 47 | requires = ["flit_core >=3.2,<4"] 48 | build-backend = "flit_core.buildapi" 49 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.11" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | - requirements: docs/requirements.txt 16 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/tests/__init__.py -------------------------------------------------------------------------------- /tests/cameraman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JeroenDelcour/tplot/8b12564f1993b6a6c1fa1c304a6e514c200511c1/tests/cameraman.png -------------------------------------------------------------------------------- /tests/reference_figures/axis_labels.txt: -------------------------------------------------------------------------------- 1 | Title goes here 2 | 3 | 9┤ • 4 | │ 5 | │ 6 | │ 7 | 8┤ • 8 | │ 9 | y │ 10 | │ 11 | a 7┤ • 12 | x │ 13 | i │ 14 | s 6┤ • 15 | │ 16 | l │ 17 | a │ 18 | b 5┤ • 19 | e │ 20 | l │ 21 | │ 22 | g 4┤ • 23 | o │ 24 | e │ 25 | s │ 26 | 3┤ • 27 | h │ 28 | e │ 29 | r 2┤ • 30 | e │ 31 | │ 32 | │ 33 | 1┤ • 34 | │ 35 | │ ┌─────────Legend─────────┐ 36 | │ │• Legend label goes here│ 37 | 0┤• └────────────────────────┘ 38 | ┬───────┬────────┬───────┬───────┬────────┬───────┬───────┬────────┬───────┬ 39 | 0 1 2 3 4 5 6 7 8 9 40 | x axis label goes here -------------------------------------------------------------------------------- /tests/reference_figures/bar_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ █ 3 | │ █ 4 | │ █ 5 | 10┤ █ █ 6 | │ █ █ 7 | │ █ █ 8 | 9┤ █ █ █ 9 | │ █ █ █ 10 | │ █ █ █ █ █ 11 | 8┤ █ █ █ █ █ 12 | │ █ █ █ █ █ █ 13 | 7┤ █ █ █ █ █ █ █ 14 | │ █ █ █ █ █ █ █ █ 15 | │ █ █ █ █ █ █ █ █ 16 | 6┤ █ █ █ █ █ █ █ █ 17 | │ █ █ █ █ █ █ █ █ █ 18 | │ █ █ █ █ █ █ █ █ █ 19 | 5┤ █ █ █ █ █ █ █ █ █ 20 | │ █ █ █ █ █ █ █ █ █ █ 21 | │█ █ █ █ █ █ █ █ █ █ █ 22 | 4┤█ █ █ █ █ █ █ █ █ █ █ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/bar_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ █ 3 | │ █ 4 | │ █ 5 | │ █ 6 | │ █ 7 | rice┤█ █ 8 | │█ █ 9 | │█ █ 10 | │█ █ 11 | │█ █ 12 | pasta┤█ █ 13 | │█ █ 14 | │█ █ 15 | │█ █ 16 | │█ █ 17 | pancakes┤█ █ 18 | │█ █ 19 | │█ █ 20 | │█ █ 21 | │█ █ 22 | ice cream┤█ █ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/bar_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ █ █ █ 3 | │ █ █ █ 4 | │ █ █ █ 5 | │ █ █ █ 6 | │ █ █ █ 7 | │ █ █ █ 8 | │ █ █ █ 9 | │ █ █ █ 10 | │ █ █ █ 11 | │ █ █ █ 12 | I. versicolor┤ █ █ █ █ █ █ 13 | │ █ █ █ █ █ █ 14 | │ █ █ █ █ █ █ 15 | │ █ █ █ █ █ █ 16 | │ █ █ █ █ █ █ 17 | │ █ █ █ █ █ █ 18 | │ █ █ █ █ █ █ 19 | │ █ █ █ █ █ █ 20 | │ █ █ █ █ █ █ 21 | │ █ █ █ █ █ █ 22 | I. setosa┤ █ █ █ █ █ █ █ █ █ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/braille_bar_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ ⡇ 3 | │ ⡇ 4 | │ ⡇ 5 | 10┤ ⡇ ⡇ 6 | │ ⡇ ⡇ 7 | │ ⡇ ⡇ 8 | 9┤ ⡇ ⡇ ⡇ 9 | │ ⡇ ⡇ ⡇ 10 | │ ⡇ ⡇ ⡇ ⡇ ⡇ 11 | 8┤ ⡇ ⡇ ⡇ ⡇ ⡇ 12 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 13 | 7┤ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 14 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 15 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 16 | 6┤ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 17 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 18 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 19 | 5┤ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 20 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 21 | │⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 22 | 4┤⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/braille_bar_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ ⡇ 3 | │ ⡇ 4 | │ ⡇ 5 | │ ⡇ 6 | │ ⡇ 7 | rice┤⡇ ⡇ 8 | │⡇ ⡇ 9 | │⡇ ⡇ 10 | │⡇ ⡇ 11 | │⡇ ⡇ 12 | pasta┤⡇ ⡇ 13 | │⡇ ⡇ 14 | │⡇ ⡇ 15 | │⡇ ⡇ 16 | │⡇ ⡇ 17 | pancakes┤⡇ ⡇ 18 | │⡇ ⡇ 19 | │⡇ ⡇ 20 | │⡇ ⡇ 21 | │⡇ ⡇ 22 | ice cream┤⡇ ⡇ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/braille_bar_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ ⡇ ⡇ ⡇ 3 | │ ⡇ ⡇ ⡇ 4 | │ ⡇ ⡇ ⡇ 5 | │ ⡇ ⡇ ⡇ 6 | │ ⡇ ⡇ ⡇ 7 | │ ⡇ ⡇ ⡇ 8 | │ ⡇ ⡇ ⡇ 9 | │ ⡇ ⡇ ⡇ 10 | │ ⡇ ⡇ ⡇ 11 | │ ⡇ ⡇ ⡇ 12 | I. versicolor┤ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 13 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 14 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 15 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 16 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 17 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 18 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 19 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 20 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 21 | │ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 22 | I. setosa┤ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ ⡇ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/braille_hbar_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 3 | │ 4 | │ 5 | 10┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 6 | │ 7 | │ 8 | 9┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 9 | │ 10 | │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 11 | 8┤ 12 | │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 13 | 7┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 14 | │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 15 | │ 16 | 6┤ 17 | │⠒⠒⠒⠒⠒⠒⠒⠒⠒ 18 | │ 19 | 5┤ 20 | │⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 21 | │⠒ 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/braille_hbar_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 3 | │ 4 | │ 5 | │ 6 | │ 7 | rice┤⠒ 8 | │ 9 | │ 10 | │ 11 | │ 12 | pasta┤⠒ 13 | │ 14 | │ 15 | │ 16 | │ 17 | pancakes┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 18 | │ 19 | │ 20 | │ 21 | │ 22 | ice cream┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/braille_hbar_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 3 | │ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | I. versicolor┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | I. setosa┤⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/braille_line_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ ⢀ 3 | │ ⢠⠋⢆ 4 | │ ⢠⠃ ⠘⡄ 5 | 10┤ ⡰⠁ ⠸⡀ ⡔ 6 | │ ⡰⠁ ⢱ ⡜ 7 | │ ⡰⠁ ⢣ ⢀⠜ 8 | 9┤ ⡠⣀ ⡜ ⢇ ⢀⠎ 9 | │ ⢀⠔⠁ ⠉⠢⢄⡀ ⡜ ⠈⡆ ⢀⠎ 10 | │ ⢠⠊ ⠈⠒⠤⣀⡠⠤⠤⠒⠒⠊⠉ ⠘⡄ ⢠⠃ 11 | 8┤ ⡔⠁ ⠱⣠⠃ 12 | │ ⢀⠎ ⠁ 13 | 7┤ ⢠⠊⢆ ⡰⠁ 14 | │ ⢀⠔⠁ ⠈⢆ ⡜ 15 | │ ⡠⠃ ⢣ ⢀⠎ 16 | 6┤ ⢠⠊ ⠣⡀ ⢠⠊ 17 | │ ⢀⠔⠁ ⠱⡀ ⡠⠃ 18 | │ ⢀⠔⠁ ⠑⡄ ⡰⠁ 19 | 5┤ ⡰⠁ ⠘⡄⡜ 20 | │ ⡠⠊ ⠈ 21 | │⠠⠊ 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/braille_line_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ ⣀⣀⣀⡠⠤⠤⠤⠒⢲ 3 | │ ⢀⣀⣀⣀⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉ ⢸ 4 | │ ⢀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠁ ⢸ 5 | │ ⢀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠁ ⢸ 6 | │ ⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠊⠉⠉⠁ ⢸ 7 | rice┤⠐⠲⢎⡉⠉⠉ ⢸ 8 | │ ⠈⠉⠒⠤⣀ ⢸ 9 | │ ⠉⠑⠢⢄⣀ ⢸ 10 | │ ⠉⠒⠤⢄⡀ ⢸ 11 | │ ⠈⠑⠢⠤⣀ ⢸ 12 | pasta┤⠐⠢⠤⢄⣀ ⠉⠒⠢⢄⡀ ⢸ 13 | │ ⠉⠉⠒⠢⠤⣀⣀ ⠈⠉⠒⠤⣀ ⢸ 14 | │ ⠉⠑⠒⠢⠤⣀⣀ ⠉⠑⠢⢄⣀ ⢸ 15 | │ ⠉⠑⠒⠢⠤⣀⣀ ⠉⠒⠤⢄⡀ ⢸ 16 | │ ⠉⠑⠒⠤⠤⣀⡀ ⠈⠑⠢⠤⣀ ⢸ 17 | pancakes┤ ⠈⠉⠑⠒⠤⠤⣀⡀ ⠉⠒⠢⢄⡀ ⠘ 18 | │ ⠈⠉⠑⠒⠤⢄⣀⡀ ⠈⠉⠒⠤⣀ 19 | │ ⠈⠉⠒⠒⠤⢄⣀⡀⠉⠑⠢⢄⣀ 20 | │ ⠈⠉⠒⠒⠤⢄⣉⡒⠤⢄⡀ 21 | │ ⠈⠉⠒⠪⠵⢦⣤⣀ 22 | ice cream┤ ⠉⠉⠒ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/braille_line_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ ⠐⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⢲⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠂ 3 | │ ⠑⡄ 4 | │ ⠈⢢ 5 | │ ⠑⡄ 6 | │ ⠈⢢ 7 | │ ⠑⡄ 8 | │ ⠈⢢ 9 | │ ⠑⡄ 10 | │ ⠈⢢ 11 | │ ⠑⡄ 12 | I. versicolor┤ ⠐⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⣚⡲⠶⠒ 13 | │ ⢀⣀⠤⠔⠊⠉ 14 | │ ⢀⣀⠤⠔⠒⠉⠁ 15 | │ ⣀⡠⠤⠒⠉⠁ 16 | │ ⣀⡠⠤⠒⠊⠉ 17 | │ ⢀⣀⠤⠔⠊⠉ 18 | │ ⢀⣀⠤⠔⠒⠉⠁ 19 | │ ⣀⡠⠤⠒⠉⠁ 20 | │ ⣀⡠⠤⠒⠊⠉ 21 | │ ⢀⣀⠤⠔⠊⠉ 22 | I. setosa┤ ⠒⠛⠓⠒⠒⠒⠒⠒⠒⠒⠂ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/braille_scatter_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ ⠐ 3 | │ 4 | │ 5 | 10┤ ⠐ 6 | │ 7 | │ 8 | 9┤ ⠐ 9 | │ 10 | │ ⠐ ⠐ 11 | 8┤ 12 | │ ⠐ 13 | 7┤ ⠐ 14 | │ ⠐ 15 | │ 16 | 6┤ 17 | │ ⠐ 18 | │ 19 | 5┤ 20 | │ ⠐ 21 | │⠐ 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/braille_scatter_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ ⠐ 3 | │ 4 | │ 5 | │ 6 | │ 7 | rice┤⠐ 8 | │ 9 | │ 10 | │ 11 | │ 12 | pasta┤⠐ 13 | │ 14 | │ 15 | │ 16 | │ 17 | pancakes┤ ⠐ 18 | │ 19 | │ 20 | │ 21 | │ 22 | ice cream┤ ⠐ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/braille_scatter_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ ⠐ ⠐ ⠐ 3 | │ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | I. versicolor┤ ⠐ ⠐ ⠐ 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | I. setosa┤ ⠐ ⠐ ⠐ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/colors.txt: -------------------------------------------------------------------------------- 1 | 2 | 7┤ • 3 | │ 4 | │ 5 | 6┤ • 6 | │ 7 | │ 8 | 5┤ • 9 | │ 10 | │ 11 | 4┤ • 12 | │ 13 | 3┤ • ┌──Legend─┐ 14 | │ │• red │ 15 | │ │• green │ 16 | 2┤ • │• blue │ 17 | │ │• yellow │ 18 | │ │• magenta│ 19 | 1┤ • │• cyan │ 20 | │ │• grey │ 21 | │ │• white │ 22 | 0┤• └─────────┘ 23 | ┬─────┬────┬────┬─────┬─────┬────┬────┬─────┬─────┬────┬────┬─────┬─────┬────┬ 24 | 0 0.5 1 1.5 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 -------------------------------------------------------------------------------- /tests/reference_figures/hbar_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤██████████████████████████████████████████████████████████████ 3 | │ 4 | │ 5 | 10┤█████████████████████████████████████████████████████████████████████████████ 6 | │ 7 | │ 8 | 9┤███████████████████████████████████████ 9 | │ 10 | │██████████████████████████████████████████████████████ 11 | 8┤ 12 | │█████████████████████████████████████████████████████████████████████ 13 | 7┤████████████████ 14 | │███████████████████████████████ 15 | │ 16 | 6┤ 17 | │█████████ 18 | │ 19 | 5┤ 20 | │████████████████████████ 21 | │█ 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/hbar_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤██████████████████████████████████████████████████████████████████████ 3 | │ 4 | │ 5 | │ 6 | │ 7 | rice┤█ 8 | │ 9 | │ 10 | │ 11 | │ 12 | pasta┤█ 13 | │ 14 | │ 15 | │ 16 | │ 17 | pancakes┤██████████████████████████████████████████████████████████████████████ 18 | │ 19 | │ 20 | │ 21 | │ 22 | ice cream┤██████████████████████████████████████████████████████████████████████ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/hbar_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤███████████████████████████████████████████████████████████████ 3 | │ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | I. versicolor┤█████████████████████████████████████████████████████████████ 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | I. setosa┤█████████████ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/image.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 3 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 4 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 5 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 6 | 5┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 7 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓ 8 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓ 9 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓ 10 | 10┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓ 11 | │░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 12 | │░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 13 | │░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 14 | 15┤░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 15 | │░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 16 | │░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 17 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███ 18 | 20┤▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████ 19 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████████ 20 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████ 21 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████ 22 | 25┤ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/image_ascii.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤ ................::::::::::::::::----------------=================== 3 | │ ................::::::::::::::::----------------===================+++ 4 | │ ................::::::::::::::::----------------===================++++++ 5 | │.................::::::::::::::::----------------===================+++++++++ 6 | 5┤.............::::::::::::::::----------------====================++++++++++++ 7 | │.......::::::::::::::::----------------===================++++++++++++++++*** 8 | │....::::::::::::::::----------------===================++++++++++++++++****** 9 | │:::::::::::::::::----------------===================++++++++++++++++********* 10 | 10┤:::::::::::::----------------====================++++++++++++++++************ 11 | │::::::::::----------------===================++++++++++++++++**************** 12 | │::::----------------===================++++++++++++++++****************###### 13 | │-----------------===================++++++++++++++++****************######### 14 | 15┤-------------====================++++++++++++++++****************############ 15 | │----------===================++++++++++++++++****************################ 16 | │-------===================++++++++++++++++****************################%%% 17 | │====================++++++++++++++++****************################%%%%%%%%% 18 | 20┤=================++++++++++++++++****************################%%%%%%%%%%%% 19 | │=============++++++++++++++++****************################%%%%%%%%%%%%%%%% 20 | │==========++++++++++++++++****************################%%%%%%%%%%%%%%%%@@@ 21 | │=======++++++++++++++++****************################%%%%%%%%%%%%%%%%@@@@@@ 22 | 25┤ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/image_big_values.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 3 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 4 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 5 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 6 | 5┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 7 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 8 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 9 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 10 | 10┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 11 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 12 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒ 13 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒ 14 | 15┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒ 15 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒ 16 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 17 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 18 | 20┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 19 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 20 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 21 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 22 | 25┤ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/image_cameraman.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▒▒▒▒▒▒▒ 3 | │▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒ 4 | │▓▒▒▓▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▓▒▒▒▒▒▒▒▒▒ 5 | 50┤▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒ 6 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ▒ ▒▓ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒ 7 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ░▒░ ▒▒▒▒▓░░░▒░░▒░▒▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▒▒▒▒▒▒▒▒▒▒▒ 8 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░ ▓▓ ░ ▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒ 9 | 100┤▓▒▓▓▓▓▓▓▓▓▓ ▓ ▓▓▒█▓ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒ 10 | │▒▓▓▓▓▓▓▓▓▓▓ ▒ ▒ █▒ ▓▓▓▓▓▓▓▓▓▒▓▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒ 11 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░ ▓▒░▓ ▓▓▓▓▓▓▓▓▓▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒ 12 | 150┤▓▓▓▒▓▓▓▓▓▓▓▓▓ ▒▓▓▓ ▓▓▒▓▓▒▒ ░▓▓▓▓▓▒▒▒▒▒▓▓▓▓▒▒▒▓▒▒▒▒▒▒▒▒▒▒ 13 | │▒▒░▓▒▒▓▒░▒▒ ▓▓▓▓ ░▓▓▓▒▓▓▓▒▒░ ░▒▒▒▒▒▒▒▒░▒▒▒▒▓▓▓▓▓▓▓▒▒▒▒▒▒ 14 | │▒▒▒▒▒▒▒▒▒ ▒▒▒▒ ▓▒░▓░░▒█▒▒▒░░░░▒░░▒░░░░░░░░ ▒▒░░░░▒░ 15 | 200┤▒▒▒▒▒▒▒▒ ▒▒▒▒ ▓▒░▒▒▓▒▒▒▒▒▒▓▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 16 | │▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 17 | │▒▒▒▒▒▒▒▒▒▒▒ ░░ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒░▒▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░ 18 | │▒▒▒▒▒▒▒▒▒▒░░ ░░░▒ ░░▒▒▒▒▒░ ▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 19 | 250┤▒▒▒▒▒▒▒▒▒▒ ░ ░ ░▒▒ ░░ ▒▒▒▒▒▒█▒░▒▒▒▒▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒▒▒▒▒▒▒ 20 | │ 21 | │ 22 | 300┤ 23 | ┬─────┬─────┬────┬─────┬─────┬─────┬────┬─────┬─────┬─────┬────┬─────┬─────┬ 24 | 0 20 40 60 80 100 120 140 160 180 200 220 240 260 -------------------------------------------------------------------------------- /tests/reference_figures/image_small_values.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤████████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 3 | │█████████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 4 | │█████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 5 | │██████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 6 | 5┤███████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 7 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░ 8 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░ 9 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░ 10 | 10┤▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░ 11 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░ 12 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░ 13 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░ 14 | 15┤▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 15 | │▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 16 | │▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 17 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 18 | 20┤▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 19 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 20 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 21 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 22 | 25┤ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/image_vmin_vmax.txt: -------------------------------------------------------------------------------- 1 | 2 | 0┤▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 3 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 4 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 5 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 6 | 5┤▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 7 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 8 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 9 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 10 | 10┤▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 11 | │▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 12 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███ 13 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████ 14 | 15┤▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████████ 15 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████ 16 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████ 17 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████████████████████ 18 | 20┤▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████████████████████████ 19 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████████████████ 20 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████████████████████ 21 | │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███████████████████████████████████ 22 | 25┤ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/legendloc_bottomleft.txt: -------------------------------------------------------------------------------- 1 | 2 | 10┤ 3 | │ 3 4 | │ 5 | 8┤ 3 6 | │ 3 7 | 6┤ 3 8 | │3 9 | 4┤ • 10 | │ 11 | │ • 12 | 2┤ • 13 | │ • 14 | │ 15 | 0┤• 16 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠒⠉⠉ 17 | -2┤ ⢀⣀⣀⣀⠤⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉⠁ 18 | │┌─Legend─┐ ⣀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉⠁ 19 | -4┤│• First │ ⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉ 20 | ││⠄ Second│⠔⠒⠒⠒⠉⠉⠉⠉ 21 | ││3 Third │ 22 | -6┤└────────┘ 23 | ┬────────┬─────────┬─────────┬────────┬────────┬─────────┬─────────┬────────┬ 24 | 0 0.5 1 1.5 2 2.5 3 3.5 4 -------------------------------------------------------------------------------- /tests/reference_figures/legendloc_bottomright.txt: -------------------------------------------------------------------------------- 1 | 2 | 10┤ 3 | │ 3 4 | │ 5 | 8┤ 3 6 | │ 3 7 | 6┤ 3 8 | │3 9 | 4┤ • 10 | │ 11 | │ • 12 | 2┤ • 13 | │ • 14 | │ 15 | 0┤• 16 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠒⠉⠉ 17 | -2┤ ⢀⣀⣀⣀⠤⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉⠁ 18 | │ ⣀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉⠁ ┌─Legend─┐ 19 | -4┤ ⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉ │• First │ 20 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠉ │⠄ Second│ 21 | │⠈⠉⠁ │3 Third │ 22 | -6┤ └────────┘ 23 | ┬────────┬─────────┬─────────┬────────┬────────┬─────────┬─────────┬────────┬ 24 | 0 0.5 1 1.5 2 2.5 3 3.5 4 -------------------------------------------------------------------------------- /tests/reference_figures/legendloc_topleft.txt: -------------------------------------------------------------------------------- 1 | 2 | 10┤┌─Legend─┐ 3 | ││• First │ 3 4 | ││⠄ Second│ 5 | 8┤│3 Third │ 3 6 | │└────────┘ 3 7 | 6┤ 3 8 | │3 9 | 4┤ • 10 | │ 11 | │ • 12 | 2┤ • 13 | │ • 14 | │ 15 | 0┤• 16 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠒⠉⠉ 17 | -2┤ ⢀⣀⣀⣀⠤⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉⠁ 18 | │ ⣀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉⠁ 19 | -4┤ ⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉ 20 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠉ 21 | │⠈⠉⠁ 22 | -6┤ 23 | ┬────────┬─────────┬─────────┬────────┬────────┬─────────┬─────────┬────────┬ 24 | 0 0.5 1 1.5 2 2.5 3 3.5 4 -------------------------------------------------------------------------------- /tests/reference_figures/legendloc_topright.txt: -------------------------------------------------------------------------------- 1 | 2 | 10┤ ┌─Legend─┐ 3 | │ │• First │ 4 | │ │⠄ Second│ 5 | 8┤ 3 │3 Third │ 6 | │ 3 └────────┘ 7 | 6┤ 3 8 | │3 9 | 4┤ • 10 | │ 11 | │ • 12 | 2┤ • 13 | │ • 14 | │ 15 | 0┤• 16 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠒⠉⠉ 17 | -2┤ ⢀⣀⣀⣀⠤⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉⠁ 18 | │ ⣀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉⠁ 19 | -4┤ ⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠊⠉⠉⠉ 20 | │ ⢀⣀⣀⣀⡠⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠉ 21 | │⠈⠉⠁ 22 | -6┤ 23 | ┬────────┬─────────┬─────────┬────────┬────────┬─────────┬─────────┬────────┬ 24 | 0 0.5 1 1.5 2 2.5 3 3.5 4 -------------------------------------------------------------------------------- /tests/reference_figures/line_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ ⢀ 3 | │ ⢠⠋⢆ 4 | │ ⢠⠃ ⠘⡄ 5 | 10┤ ⡰⠁ ⠸⡀ ⡔ 6 | │ ⡰⠁ ⢱ ⡜ 7 | │ ⡰⠁ ⢣ ⢀⠜ 8 | 9┤ ⡠⣀ ⡜ ⢇ ⢀⠎ 9 | │ ⢀⠔⠁ ⠉⠢⢄⡀ ⡜ ⠈⡆ ⢀⠎ 10 | │ ⢠⠊ ⠈⠒⠤⣀⡠⠤⠤⠒⠒⠊⠉ ⠘⡄ ⢠⠃ 11 | 8┤ ⡔⠁ ⠱⣠⠃ 12 | │ ⢀⠎ ⠁ 13 | 7┤ ⢠⠊⢆ ⡰⠁ 14 | │ ⢀⠔⠁ ⠈⢆ ⡜ 15 | │ ⡠⠃ ⢣ ⢀⠎ 16 | 6┤ ⢠⠊ ⠣⡀ ⢠⠊ 17 | │ ⢀⠔⠁ ⠱⡀ ⡠⠃ 18 | │ ⢀⠔⠁ ⠑⡄ ⡰⠁ 19 | 5┤ ⡰⠁ ⠘⡄⡜ 20 | │ ⡠⠊ ⠈ 21 | │⠠⠊ 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/line_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ ⣀⣀⣀⡠⠤⠤⠤⠒⢲ 3 | │ ⢀⣀⣀⣀⠤⠤⠤⠒⠒⠒⠊⠉⠉⠉ ⢸ 4 | │ ⢀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠁ ⢸ 5 | │ ⢀⣀⣀⣀⠤⠤⠤⠔⠒⠒⠒⠉⠉⠉⠁ ⢸ 6 | │ ⣀⣀⣀⡠⠤⠤⠤⠒⠒⠒⠊⠉⠉⠁ ⢸ 7 | rice┤⠐⠲⢎⡉⠉⠉ ⢸ 8 | │ ⠈⠉⠒⠤⣀ ⢸ 9 | │ ⠉⠑⠢⢄⣀ ⢸ 10 | │ ⠉⠒⠤⢄⡀ ⢸ 11 | │ ⠈⠑⠢⠤⣀ ⢸ 12 | pasta┤⠐⠢⠤⢄⣀ ⠉⠒⠢⢄⡀ ⢸ 13 | │ ⠉⠉⠒⠢⠤⣀⣀ ⠈⠉⠒⠤⣀ ⢸ 14 | │ ⠉⠑⠒⠢⠤⣀⣀ ⠉⠑⠢⢄⣀ ⢸ 15 | │ ⠉⠑⠒⠢⠤⣀⣀ ⠉⠒⠤⢄⡀ ⢸ 16 | │ ⠉⠑⠒⠤⠤⣀⡀ ⠈⠑⠢⠤⣀ ⢸ 17 | pancakes┤ ⠈⠉⠑⠒⠤⠤⣀⡀ ⠉⠒⠢⢄⡀ ⠘ 18 | │ ⠈⠉⠑⠒⠤⢄⣀⡀ ⠈⠉⠒⠤⣀ 19 | │ ⠈⠉⠒⠒⠤⢄⣀⡀⠉⠑⠢⢄⣀ 20 | │ ⠈⠉⠒⠒⠤⢄⣉⡒⠤⢄⡀ 21 | │ ⠈⠉⠒⠪⠵⢦⣤⣀ 22 | ice cream┤ ⠉⠉⠒ 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/line_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ ⠐⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⢲⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠂ 3 | │ ⠑⡄ 4 | │ ⠈⢢ 5 | │ ⠑⡄ 6 | │ ⠈⢢ 7 | │ ⠑⡄ 8 | │ ⠈⢢ 9 | │ ⠑⡄ 10 | │ ⠈⢢ 11 | │ ⠑⡄ 12 | I. versicolor┤ ⠐⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⣚⡲⠶⠒ 13 | │ ⢀⣀⠤⠔⠊⠉ 14 | │ ⢀⣀⠤⠔⠒⠉⠁ 15 | │ ⣀⡠⠤⠒⠉⠁ 16 | │ ⣀⡠⠤⠒⠊⠉ 17 | │ ⢀⣀⠤⠔⠊⠉ 18 | │ ⢀⣀⠤⠔⠒⠉⠁ 19 | │ ⣀⡠⠤⠒⠉⠁ 20 | │ ⣀⡠⠤⠒⠊⠉ 21 | │ ⢀⣀⠤⠔⠊⠉ 22 | I. setosa┤ ⠒⠛⠓⠒⠒⠒⠒⠒⠒⠒⠂ 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/scatter_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 11┤ • 3 | │ 4 | │ 5 | 10┤ • 6 | │ 7 | │ 8 | 9┤ • 9 | │ 10 | │ • • 11 | 8┤ 12 | │ • 13 | 7┤ • 14 | │ • 15 | │ 16 | 6┤ 17 | │ • 18 | │ 19 | 5┤ 20 | │ • 21 | │• 22 | 4┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/scatter_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | waffles┤ • 3 | │ 4 | │ 5 | │ 6 | │ 7 | rice┤• 8 | │ 9 | │ 10 | │ 11 | │ 12 | pasta┤• 13 | │ 14 | │ 15 | │ 16 | │ 17 | pancakes┤ • 18 | │ 19 | │ 20 | │ 21 | │ 22 | ice cream┤ • 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/scatter_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. virginica┤ • • • 3 | │ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | I. versicolor┤ • • • 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | I. setosa┤ • • • 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/text.txt: -------------------------------------------------------------------------------- 1 | 2 | 8┤ t 3 | │ 4 | │ 5 | │ 6 | 6┤ 7 | │ 8 | │ 9 | │ 10 | 4┤ 11 | │ 12 | │ 13 | │ 14 | 2┤ 15 | │ 16 | │ 17 | │ 18 | 0┤testing text 19 | │ 20 | │testing colored text 21 | │ 22 | -2┤ 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 4.5 5 5.5 6 6.5 7 7.5 8 8.5 9 -------------------------------------------------------------------------------- /tests/reference_figures/y_axis_down_anscombe.txt: -------------------------------------------------------------------------------- 1 | 2 | 4┤ 3 | │• 4 | │ • 5 | 5┤ 6 | │ 7 | │ • 8 | 6┤ 9 | │ 10 | │ • 11 | 7┤ • 12 | │ • 13 | 8┤ 14 | │ • • 15 | │ 16 | 9┤ • 17 | │ 18 | │ 19 | 10┤ • 20 | │ 21 | │ 22 | 11┤ • 23 | ┬───────┬──────┬───────┬──────┬───────┬───────┬──────┬───────┬──────┬───────┬ 24 | 4 5 6 7 8 9 10 11 12 13 14 -------------------------------------------------------------------------------- /tests/reference_figures/y_axis_down_cheese_or_chocolate.txt: -------------------------------------------------------------------------------- 1 | 2 | ice cream┤ • 3 | │ 4 | │ 5 | │ 6 | │ 7 | pancakes┤ • 8 | │ 9 | │ 10 | │ 11 | │ 12 | pasta┤• 13 | │ 14 | │ 15 | │ 16 | │ 17 | rice┤• 18 | │ 19 | │ 20 | │ 21 | │ 22 | waffles┤ • 23 | ┬────────────────────────────────────────────────────────────────────┬ 24 | cheese chocolate -------------------------------------------------------------------------------- /tests/reference_figures/y_axis_down_iris.txt: -------------------------------------------------------------------------------- 1 | 2 | I. setosa┤ • • • 3 | │ 4 | │ 5 | │ 6 | │ 7 | │ 8 | │ 9 | │ 10 | │ 11 | │ 12 | I. versicolor┤ • • • 13 | │ 14 | │ 15 | │ 16 | │ 17 | │ 18 | │ 19 | │ 20 | │ 21 | │ 22 | I. virginica┤ • • • 23 | ┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬ 24 | 4.6 4.8 5 5.2 5.4 5.6 5.8 6 6.2 6.4 6.6 6.8 7 7.2 -------------------------------------------------------------------------------- /tests/reference_figures/y_axis_up.txt: -------------------------------------------------------------------------------- 1 | 2 | 25┤ 3 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████████ 4 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████████ 5 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓█████████ 6 | 20┤▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████ 7 | │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓███ 8 | │░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 9 | │░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 10 | 15┤░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 11 | │░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 12 | │░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 13 | │░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 14 | 10┤░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓ 15 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓ 16 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓ 17 | │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓ 18 | 5┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 19 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 20 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 21 | │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 22 | 0┤ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 23 | ┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬─────┬──────┬─────┬ 24 | 0 2 4 6 8 10 12 14 16 18 20 22 24 -------------------------------------------------------------------------------- /tests/reference_figures/y_only.txt: -------------------------------------------------------------------------------- 1 | 2 | 10┤ 3 | │ 4 | │ • 5 | │ 6 | 8┤ • 7 | │ 8 | │ • 9 | │ 10 | 6┤ • 11 | │ 12 | │ • 13 | │ 14 | 4┤ • 15 | │ 16 | │ • 17 | │ 18 | 2┤ • 19 | │ 20 | │ • 21 | │ 22 | 0┤• 23 | ┬───────┬────────┬───────┬────────┬───────┬────────┬───────┬────────┬───────┬ 24 | 0 1 2 3 4 5 6 7 8 9 -------------------------------------------------------------------------------- /tests/test_braille.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tplot.braille import ( 4 | braille_bin, 5 | braille_from_xy, 6 | combine_braille, 7 | draw_braille, 8 | get_braille, 9 | is_braille, 10 | ) 11 | 12 | 13 | def test_single_characters(): 14 | assert get_braille("00000000") == "⠀" 15 | assert get_braille("10100111") == "⢵" 16 | assert get_braille("01011000") == "⡊" 17 | assert get_braille("11001111") == "⢻" 18 | assert get_braille("11111111") == "⣿" 19 | 20 | 21 | def test_braille_bin(): 22 | assert braille_bin("⠀") == "00000000" 23 | assert braille_bin("⢵") == "10100111" 24 | assert braille_bin("⡊") == "01011000" 25 | assert braille_bin("⢻") == "11001111" 26 | assert braille_bin("⣿") == "11111111" 27 | 28 | 29 | def test_is_braille(): 30 | assert is_braille("⠀") is True 31 | assert is_braille(" ") is False 32 | assert is_braille("⟿") is False 33 | assert is_braille("⤀") is False 34 | assert is_braille("⡷") is True 35 | 36 | 37 | def test_braile_from_xy(): 38 | assert braille_from_xy(x=1, y=0) == "⠈" 39 | assert braille_from_xy(x=1, y=3) == "⢀" 40 | with pytest.raises(ValueError): 41 | braille_from_xy(x=2, y=0) 42 | with pytest.raises(ValueError): 43 | braille_from_xy(x=0, y=4) 44 | 45 | 46 | def test_combine_braille(): 47 | assert combine_braille("⠁⠂") == "⠃" 48 | assert combine_braille(["⢵", "⡊"]) == "⣿" 49 | 50 | 51 | def test_draw_braille(): 52 | assert draw_braille(x=0.3, y=0.8, canvas_str=" ") == "⠐" 53 | assert draw_braille(x=0.3, y=0.8, canvas_str="#") == "⠐" 54 | assert draw_braille(x=0.5, y=0.5, canvas_str=" ") == "⡀" 55 | assert draw_braille(x=0, y=0, canvas_str=" ") == "⠐" 56 | assert draw_braille(x=-0.1, y=-0.2, canvas_str="⠁") == "⠃" 57 | -------------------------------------------------------------------------------- /tests/test_img2ascii.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from tplot.img2ascii import resize 4 | 5 | 6 | def test_nearest_neighbor_downscaling(): 7 | image = np.array( 8 | [[0, 0, 1, 1], [0, 0, 1, 1], [1, 1, 0, 0], [1, 1, 0, 0]], dtype=np.uint8 9 | ) 10 | out = resize(image, shape=(2, 2)) 11 | assert out.shape == (2, 2) 12 | np.testing.assert_array_equal(out, np.array([[0, 1], [1, 0]], dtype=np.uint8)) 13 | 14 | 15 | def test_nearest_neighbor_upscaling(): 16 | image = np.array([[0, 1], [1, 0]], dtype=np.uint8) 17 | out = resize(image, shape=(4, 4)) 18 | assert out.shape == (4, 4) 19 | np.testing.assert_array_equal( 20 | out, 21 | np.array( 22 | [[0, 0, 1, 1], [0, 0, 1, 1], [1, 1, 0, 0], [1, 1, 0, 0]], dtype=np.uint8 23 | ), 24 | ) 25 | -------------------------------------------------------------------------------- /tests/test_reference_figures.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pytest 5 | from PIL import Image 6 | 7 | import tplot 8 | 9 | GENERATE = False 10 | 11 | 12 | def equal_to_file(output, filename): 13 | with open( 14 | Path("tests") / "reference_figures" / filename, "w" if GENERATE else "r" 15 | ) as f: 16 | if GENERATE: 17 | f.write(output) 18 | return True 19 | else: 20 | return output == f.read() 21 | 22 | 23 | def ascii_only(s): 24 | try: 25 | s.encode("ascii") # this fails if the string contains non-ascii characters 26 | return True 27 | except UnicodeEncodeError: 28 | return False 29 | 30 | 31 | datasets = { 32 | "anscombe": [ 33 | (4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14), 34 | (4.26, 5.68, 7.24, 4.82, 6.95, 8.81, 8.04, 8.33, 10.84, 7.58, 9.96), 35 | ], 36 | "iris": [ 37 | (5.1, 4.9, 4.7, 7, 6.4, 6.9, 6.3, 5.8, 7.1), 38 | (["I. setosa"] * 3 + ["I. versicolor"] * 3 + ["I. virginica"] * 3), 39 | ], 40 | "cheese_or_chocolate": [ 41 | ("cheese", "chocolate", "cheese", "chocolate", "chocolate"), 42 | ("pasta", "ice cream", "rice", "waffles", "pancakes"), 43 | ], 44 | } 45 | 46 | gradient = np.linspace(np.zeros(24), np.ones(24), num=24) 47 | gradient = (gradient + gradient.T) / 2 48 | 49 | reference_figures_dir = Path("tests/reference_figures") 50 | 51 | 52 | def test_ascii_fallback(): 53 | fig = tplot.Figure(width=80, height=24, ascii=True) 54 | for dataset_name, data in datasets.items(): 55 | fig.clear() 56 | fig.scatter(data[0], data[1]) 57 | assert ascii_only(str(fig)) 58 | 59 | fig.clear() 60 | fig.image(gradient) 61 | assert ascii_only(str(fig)) 62 | 63 | 64 | def test_figure_too_small_error(): 65 | fig = tplot.Figure(width=1, height=1) 66 | for dataset_name, data in datasets.items(): 67 | fig.clear() 68 | fig.scatter(data[0], data[1]) 69 | with pytest.raises(IndexError): 70 | str(fig) 71 | 72 | fig.clear() 73 | fig.image(gradient) 74 | with pytest.raises(IndexError): 75 | str(fig) 76 | 77 | 78 | def test_data_validation(): 79 | fig = tplot.Figure(width=80, height=24) 80 | with pytest.raises(ValueError): 81 | fig.scatter(x=range(3), y=range(5)) 82 | with pytest.raises(ValueError): 83 | fig.scatter(x=[], y=range(3)) 84 | 85 | 86 | def test_y_only(): 87 | # as positional argument 88 | pos_arg_fig = tplot.Figure(width=80, height=24) 89 | pos_arg_fig.scatter(range(10)) 90 | 91 | # as keyword argument 92 | y_kwarg_fig = tplot.Figure(width=80, height=24) 93 | y_kwarg_fig.scatter(y=range(10)) 94 | 95 | assert str(y_kwarg_fig) == str(pos_arg_fig) 96 | 97 | for s in (str(pos_arg_fig), str(y_kwarg_fig)): 98 | with open(reference_figures_dir / "y_only.txt", "w" if GENERATE else "r") as f: 99 | if GENERATE: 100 | f.write(str(f)) 101 | else: 102 | assert s == f.read() 103 | 104 | 105 | def test_scatter(): 106 | fig = tplot.Figure(width=80, height=24) 107 | for dataset_name, data in datasets.items(): 108 | fig.clear() 109 | fig.scatter(data[0], data[1]) 110 | assert equal_to_file(str(fig), f"scatter_{dataset_name}.txt") 111 | 112 | 113 | def test_line(): 114 | fig = tplot.Figure(width=80, height=24) 115 | for dataset_name, data in datasets.items(): 116 | fig.clear() 117 | fig.line(data[0], data[1]) 118 | assert equal_to_file(str(fig), f"line_{dataset_name}.txt") 119 | 120 | 121 | def test_bar(): 122 | fig = tplot.Figure(width=80, height=24) 123 | for dataset_name, data in datasets.items(): 124 | fig.clear() 125 | fig.bar(data[0], data[1]) 126 | assert equal_to_file(str(fig), f"bar_{dataset_name}.txt") 127 | 128 | 129 | def test_hbar(): 130 | fig = tplot.Figure(width=80, height=24) 131 | for dataset_name, data in datasets.items(): 132 | fig.clear() 133 | fig.hbar(data[0], data[1]) 134 | assert equal_to_file(str(fig), f"hbar_{dataset_name}.txt") 135 | 136 | 137 | def test_image(): 138 | fig = tplot.Figure(width=80, height=24) 139 | fig.image(gradient) 140 | assert equal_to_file(str(fig), "image.txt") 141 | 142 | fig.clear() 143 | fig.image((gradient * 128).astype(np.uint8)) 144 | assert equal_to_file(str(fig), "image_big_values.txt") 145 | 146 | fig.clear() 147 | fig.image(gradient * -1e-3) 148 | assert equal_to_file(str(fig), "image_small_values.txt") 149 | 150 | fig.clear() 151 | fig.image(gradient, vmin=-1, vmax=1) 152 | assert equal_to_file(str(fig), "image_vmin_vmax.txt") 153 | 154 | fig.clear() 155 | fig.image(gradient, cmap="ascii") 156 | assert equal_to_file(str(fig), "image_ascii.txt") 157 | 158 | fig.clear() 159 | cameraman = np.array(Image.open("tests/cameraman.png")) 160 | fig.image(cameraman) 161 | assert equal_to_file(str(fig), "image_cameraman.txt") 162 | 163 | 164 | def test_legend(): 165 | for legendloc in ("topleft", "topright", "bottomright", "bottomleft"): 166 | fig = tplot.Figure(width=80, height=24, legendloc=legendloc) 167 | fig.scatter(range(5), label="First") 168 | fig.line(range(-5, 0), label="Second") 169 | fig.scatter(range(5, 10), marker="3", label="Third") 170 | assert equal_to_file(str(fig), f"legendloc_{legendloc}.txt") 171 | 172 | 173 | def test_axis_labels(): 174 | fig = tplot.Figure( 175 | xlabel="x axis label goes here", 176 | ylabel="y axis label goes here", 177 | title="Title goes here", 178 | width=80, 179 | height=40, 180 | legendloc="bottomright", 181 | ) 182 | fig.scatter(range(10), label="Legend label goes here") 183 | assert equal_to_file(str(fig), "axis_labels.txt") 184 | 185 | 186 | def test_colors(): 187 | fig = tplot.Figure(width=80, height=24, legendloc="bottomright") 188 | for i, color in enumerate( 189 | ["red", "green", "blue", "yellow", "magenta", "cyan", "grey", "white"] 190 | ): 191 | fig.scatter([i], [i], color=color, label=color) 192 | assert equal_to_file(str(fig), "colors.txt") 193 | 194 | 195 | def test_text(): 196 | fig = tplot.Figure(width=80, height=24) 197 | fig.text(x=4, y=0, text="testing text") 198 | fig.text(x=4, y=-1, text="testing colored text", color="red") 199 | fig.text(x=9, y=8, text="testing text at right boundary") 200 | assert equal_to_file(str(fig), "text.txt") 201 | 202 | 203 | def test_braille(): 204 | fig = tplot.Figure(width=80, height=24) 205 | for dataset_name, data in datasets.items(): 206 | fig.clear() 207 | fig.scatter(data[0], data[1], marker="braille", color="red") 208 | assert equal_to_file(str(fig), f"braille_scatter_{dataset_name}.txt") 209 | 210 | fig.clear() 211 | fig.line(data[0], data[1], marker="braille", color="green") 212 | assert equal_to_file(str(fig), f"braille_line_{dataset_name}.txt") 213 | 214 | fig.clear() 215 | fig.bar(data[0], data[1], marker="braille", color="blue") 216 | assert equal_to_file(str(fig), f"braille_bar_{dataset_name}.txt") 217 | 218 | fig.clear() 219 | fig.hbar(data[0], data[1], marker="braille") 220 | assert equal_to_file(str(fig), f"braille_hbar_{dataset_name}.txt") 221 | 222 | 223 | def test_y_axis_direction(): 224 | fig = tplot.Figure(width=80, height=24, y_axis_direction="down") 225 | for dataset_name, data in datasets.items(): 226 | fig.clear() 227 | fig.scatter(data[0], data[1]) 228 | assert equal_to_file(str(fig), f"y_axis_down_{dataset_name}.txt") 229 | 230 | fig = tplot.Figure(width=80, height=24, y_axis_direction="up") 231 | fig.image(gradient) 232 | assert equal_to_file(str(fig), "y_axis_up.txt") 233 | -------------------------------------------------------------------------------- /tests/test_scales.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import tplot 4 | 5 | 6 | def test_linear_scale(): 7 | data = [-1, 3, -0.5, 4] 8 | scale = tplot.scales.LinearScale() 9 | scale.fit(data, target_min=-1, target_max=1) 10 | assert scale.transform(4) == 1 11 | assert scale.transform(-1) == -1 12 | assert scale.transform(1.5) == 0 13 | assert scale.transform([4, -1, 1.5]) == [1, -1, 0] 14 | np.testing.assert_array_equal( 15 | scale.transform(np.array([4, -1, 1.5])), np.array([1, -1, 0]) 16 | ) 17 | 18 | 19 | def test_linear_inverted_scale(): 20 | data = [-1, 3, -0.5, 4] 21 | scale = tplot.scales.LinearScale() 22 | scale.fit(data, target_min=1, target_max=-1) 23 | assert scale.transform(4) == -1 24 | assert scale.transform(-1) == 1 25 | 26 | 27 | def test_linear_single_value(): 28 | scale = tplot.scales.LinearScale() 29 | scale.fit([1], target_min=0, target_max=1) 30 | assert scale.transform(1) == 0.5 31 | 32 | 33 | def test_categorical_scale(): 34 | data = ["eggs", 42, "bacon", "spam", "spam", "spam", "bacon", "spam", "eggs"] 35 | scale = tplot.scales.CategoricalScale() 36 | scale.fit(data) 37 | # should return index of lexically sorted string representation of unique values 38 | scale.transform(42) == 0 39 | scale.transform("42") == 0 40 | scale.transform("bacon") == 1 41 | scale.transform("eggs") == 2 42 | scale.transform("spam") == 3 43 | scale.transform(["42", "bacon", "eggs", "spam"]) == [0, 1, 2, 3] 44 | np.testing.assert_array_equal( 45 | scale.transform(np.array(["42", "bacon", "eggs", "spam"])), 46 | np.array(([0, 1, 2, 3])), 47 | ) 48 | -------------------------------------------------------------------------------- /tests/test_xticklabels.py: -------------------------------------------------------------------------------- 1 | import tplot 2 | from tplot.utils import _optimize_xticklabel_anchors 3 | 4 | 5 | def test_simple(): 6 | anchors = _optimize_xticklabel_anchors( 7 | tick_positions=[0, 10, 20], labels=["0", "1", "2"], width=80 8 | ) 9 | assert anchors == [[0, 1], [10, 11], [20, 21]] 10 | 11 | 12 | def test_boundaries(): 13 | anchors = _optimize_xticklabel_anchors( 14 | tick_positions=[0, 20], labels=["0.0", "2.0"], width=21 15 | ) 16 | assert anchors == [[0, 2], [18, 21]] 17 | 18 | 19 | def test_margin(): 20 | anchors = _optimize_xticklabel_anchors( 21 | tick_positions=[5, 10], labels=["lorem", "ipsum"], width=80 22 | ) 23 | assert anchors == [[2, 7], [9, 14]] 24 | 25 | 26 | def test_pruning(): 27 | """ 28 | Tests that labels are shortened if they don't fit and they don't extend beyond the previous or next tick. 29 | """ 30 | anchors = _optimize_xticklabel_anchors( 31 | tick_positions=[3, 5, 7], 32 | labels=[ 33 | "your mother was a hamster", 34 | "and", 35 | "your father smelled of elderberries", 36 | ], 37 | width=10, 38 | ) 39 | assert anchors == [[0, 5], [4, 6], [6, 10]] 40 | 41 | 42 | def test_complex(): 43 | anchors = _optimize_xticklabel_anchors( 44 | tick_positions=[10, 22, 34, 47, 59], 45 | labels=[ 46 | "Delicious ice cream", 47 | "Pancakes with syrup", 48 | "Pasta", 49 | "Rice bowl", 50 | "Voluptuous waffles", 51 | ], 52 | width=60, 53 | ) 54 | assert anchors == [[0, 16], [16, 34], [34, 39], [39, 48], [48, 60]] 55 | -------------------------------------------------------------------------------- /tplot/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | from .figure import Figure 4 | 5 | __version__ = version(__name__) 6 | -------------------------------------------------------------------------------- /tplot/braille.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | 4 | def get_braille(s: str) -> str: 5 | """ 6 | `s` specifies which dots in the desired braille character must be on ('1') and which must be off ('0'). 7 | Dots in the 2x8 braille matrix are ordered top-down, left-to-right. 8 | The order of the '1's and '0's in `s` correspond to this. 9 | Schematic example: 10 | ▪ 10 11 | ▪ 01 12 | ▪▪ = 11 13 | ▪ 01 14 | ⢵ = '10100111' 15 | 16 | More examples: 17 | '10000000' = ⠁ (only top left dot) 18 | '11001111' = ⢻ 19 | '11111111' = ⣿ 20 | '00000000' = ⠀ (empty braille character) 21 | """ 22 | s = ( 23 | s[:3] + s[4:7] + s[3] + s[7] 24 | ) # rearrange ISO/TR 11548-1 dot order to something more suitable 25 | return chr(0x2800 + int(s[::-1], 2)) 26 | 27 | 28 | def braille_bin(char: str) -> str: 29 | """Inverse of get_braille()""" 30 | o = ord(char) - 0x2800 31 | s = format(o, "b").rjust(8, "0") 32 | s = s[::-1] 33 | s = ( 34 | s[:3] + s[6] + s[3:6] + s[7] 35 | ) # rearrange ISO/TR 11548-1 dot order to something more suitable 36 | return s 37 | 38 | 39 | def is_braille(char: str) -> bool: 40 | """Return True if provided unicode character is a braille character.""" 41 | return isinstance(char, str) and 0x2800 <= ord(char[0]) <= 0x28FF 42 | 43 | 44 | def braille_from_xy(x: int, y: int) -> str: 45 | """ 46 | Returns braille character with dot at x, y position filled in. 47 | Example: braille_from_xy(x=1, y=0) returns "⠈" (top right dot filled in) 48 | """ 49 | if not 0 <= x <= 1 or not 0 <= y <= 3: 50 | raise ValueError("Invalid braille dot position.") 51 | s = ["0"] * 8 52 | s[x * 4 + y] = "1" 53 | return get_braille("".join(s)) 54 | 55 | 56 | def combine_braille(braille: Iterable[str]) -> str: 57 | """ 58 | Returns braille character that combines dots of input braille characters. 59 | Example: combine_braille("⠁⠂") returns "⠃" 60 | """ 61 | out_bin = 0b00000000 62 | for char in braille: 63 | braille_b = braille_bin(char) 64 | out_bin |= int(braille_b, 2) 65 | s = format(out_bin, "b").rjust(8, "0") 66 | return get_braille(s) 67 | 68 | 69 | def draw_braille(x: float, y: float, canvas_str=None) -> str: 70 | """ 71 | Returns braille character for given x, y position. 72 | If canvas_str is already a braille character, the new braille dot will be added to it. 73 | """ 74 | x = round((x + 0.500000001) % 1) # 0 or 1. 0.500000001 so it rounds half up. 75 | y = 3 - round((-y + 0.375000001) % 1 * 4) % 4 # 0, 1, 2, or 3 76 | out = braille_from_xy(x, y) 77 | for character in canvas_str: 78 | if is_braille(character): 79 | out = combine_braille([out, character]) 80 | break 81 | return out 82 | -------------------------------------------------------------------------------- /tplot/figure.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property, partial 2 | from numbers import Number 3 | from shutil import get_terminal_size 4 | from typing import Callable, Iterable, List, Optional, Tuple 5 | 6 | import numpy as np 7 | from colorama import init 8 | from termcolor import colored 9 | 10 | from . import utils 11 | from .braille import draw_braille, is_braille 12 | from .img2ascii import img2ascii 13 | from .scales import CategoricalScale, LinearScale 14 | 15 | init() 16 | 17 | 18 | ASCII_FALLBACK = { 19 | "─": "-", 20 | "│": "|", 21 | "┤": "+", 22 | "┬": "+", 23 | "┌": "+", 24 | "┐": "+", 25 | "└": "+", 26 | "┘": "+", 27 | "█": "#", 28 | "•": "*", 29 | "·": ".", 30 | } 31 | 32 | 33 | class Figure: 34 | """ 35 | Figure to draw plots onto. 36 | 37 | Args: 38 | xlabel: Label for the x axis. 39 | ylabel: Label for the y axis. 40 | title: Title of the figure. 41 | width: Width of the figure in number of characters. Defaults to the terminal window width, or falls back to 80. 42 | height: Height of the figure in number of characters. Defaults to the terminal window height, or falls back to 24. 43 | legendloc: Legend location. Supported values are `"topleft"`, `"topright"`, `"bottomleft"`, and `"bottomright"`. 44 | ascii: Set to `True` to only use ascii characters. Defaults to trying to detect if unicode is supported in the terminal. 45 | y_axis_direction: Set to `"up"` to have Y axis point up (conventional for graphs), `"down"` to have Y axis point down 46 | (conventional for images). By default, this is automatically determined based on the drawn plots. 47 | """ 48 | 49 | def __init__( 50 | self, 51 | xlabel: Optional[str] = None, 52 | ylabel: Optional[str] = None, 53 | title: Optional[str] = None, 54 | width: Optional[int] = None, 55 | height: Optional[int] = None, 56 | legendloc: str = "topright", 57 | ascii: bool = False, 58 | y_axis_direction: str = "auto", 59 | ) -> None: 60 | if legendloc not in {"topleft", "topright", "bottomleft", "bottomright"}: 61 | raise ValueError("Unsupported legend location") 62 | if width is not None: 63 | assert isinstance(width, int) and width > 0 64 | if height is not None: 65 | assert isinstance(height, int) and height > 0 66 | 67 | self._xlabel = xlabel 68 | self._ylabel = ylabel 69 | self.title = title 70 | self.legendloc = legendloc 71 | 72 | self._y_axis_direction = y_axis_direction 73 | 74 | self.ascii_only = ascii 75 | if not self.ascii_only: 76 | self.ascii_only = not utils.unicode_supported() 77 | 78 | term_width, term_height = get_terminal_size(fallback=(80, 24)) 79 | term_height -= 1 # room for prompt 80 | self.width = width if width else term_width 81 | self.height = height if height else term_height 82 | 83 | # gather stuff to plot before actually drawing it 84 | self._plots: List[Callable] = [] 85 | self._labels: List[Tuple[str, str]] = [] 86 | 87 | @property 88 | def _x(self): 89 | return tuple([x for plot in self._plots for x in plot.keywords["x"]]) 90 | 91 | @property 92 | def _y(self): 93 | return tuple([y for plot in self._plots for y in plot.keywords["y"]]) 94 | 95 | @cached_property 96 | def _yscale(self): 97 | if utils._is_numerical(self._y): 98 | scale = LinearScale() 99 | else: 100 | scale = CategoricalScale() 101 | target_min = -self._xax_height() - 1 102 | target_max = -self.height + 1 + bool(self.title) 103 | if self._y_axis_direction == "down": 104 | target_min, target_max = target_max, target_min 105 | scale.fit(self._y, target_min, target_max) 106 | if utils._is_numerical(self._y): 107 | # refit scale to tick values, since those lay just outside the input data range 108 | scale.fit(self._ytick_values, target_min, target_max) 109 | return scale 110 | 111 | @cached_property 112 | def _xscale(self): 113 | if utils._is_numerical(self._x): 114 | scale = LinearScale() 115 | else: 116 | scale = CategoricalScale() 117 | target_min = self._yax_width 118 | target_max = self.width - 1 119 | scale.fit(self._x, target_min, target_max) 120 | if utils._is_numerical(self._x): 121 | # refit scale to tick values, since those lay just outside the input data range 122 | scale.fit(self._xtick_values, target_min, target_max) 123 | return scale 124 | 125 | def _xax_height(self) -> int: 126 | return 2 + bool(self._xlabel) 127 | 128 | def _fmt(self, value) -> str: 129 | if isinstance(value, Number): 130 | return f"{value:.3g}" 131 | else: 132 | return str(value) 133 | 134 | @cached_property 135 | def _yax_width(self) -> int: 136 | """ 137 | Since y-axis tick labels are drawn horizontally, the width of the y axis 138 | depends on the length of the labels, which themselves depend on the data. 139 | """ 140 | labels = (self._fmt(value) for value in self._ytick_values) 141 | width = max([len(label) for label in labels]) 142 | width += 1 # for axis ticks 143 | width += bool(self._ylabel) * 2 # for y label 144 | return width 145 | 146 | def _center_draw(self, string, array, fillchar=" "): 147 | array[:] = list(string.center(len(array), fillchar)) 148 | 149 | def _ljust_draw(self, string, array, fillchar=" "): 150 | array[:] = list(string.ljust(len(array), fillchar)) 151 | 152 | def _rjust_draw(self, string, array, fillchar=" "): 153 | array[:] = list(string.rjust(len(array), fillchar)) 154 | 155 | @cached_property 156 | def _ytick_values(self): 157 | if utils._is_numerical(self._y): 158 | return utils._best_ticks(min(self._y), max(self._y), most=self.height // 3) 159 | else: # nominal 160 | values = tuple(sorted([str(v) for v in set(self._y)])) 161 | y_axis_height = self.height - bool(self.title) - self._xax_height() 162 | if len(values) > y_axis_height: 163 | raise IndexError( 164 | f"Too many ({len(values)}) unique y values to fit into y axis. Try making the figure taller." 165 | ) 166 | return values 167 | 168 | @cached_property 169 | def _xtick_values(self): 170 | if utils._is_numerical(self._x): 171 | return utils._best_ticks(min(self._x), max(self._x), most=self.width // 5) 172 | else: # categorical 173 | # note this may not fit depending on the width of the figure 174 | values = tuple(sorted([str(v) for v in set(self._x)])) 175 | return values 176 | 177 | def _draw_y_axis(self) -> None: 178 | start = round(self._yscale.transform(self._ytick_values[-1])) 179 | end = round(self._yscale.transform(self._ytick_values[0])) 180 | start, end = min(start, end), max(start, end) 181 | self._canvas[start:end, self._yax_width - 1] = "│" 182 | for value, pos in zip( 183 | self._ytick_values, self._yscale.transform(self._ytick_values) 184 | ): 185 | pos = round(pos) 186 | label = self._fmt(value) 187 | self._canvas[pos, self._yax_width - 1] = "┤" 188 | self._rjust_draw( 189 | label, self._canvas[pos, bool(self._ylabel) * 2 : self._yax_width - 1] 190 | ) 191 | 192 | if self._ylabel: 193 | ylabel = self._ylabel[: end - start] # make sure it fits 194 | self._center_draw(ylabel, self._canvas[start:end, 0]) 195 | 196 | def _draw_x_axis(self) -> None: 197 | tick_positions = [round(v) for v in self._xscale.transform(self._xtick_values)] 198 | labels = [self._fmt(v) for v in self._xtick_values] 199 | # draw axis 200 | axis_start = round(self._xscale.transform(self._xtick_values[0])) 201 | axis_end = round(self._xscale.transform(self._xtick_values[-1])) 202 | self._canvas[-self._xax_height(), axis_start:axis_end] = "─" 203 | # draw ticks 204 | for tick_pos in tick_positions: 205 | self._canvas[-self._xax_height(), tick_pos] = "┬" 206 | # draw labels 207 | anchors = utils._optimize_xticklabel_anchors( 208 | tick_positions=tick_positions, labels=labels, width=self.width 209 | ) 210 | for (start, end), label in zip(anchors, labels): 211 | label = label[: end - start] # shorten label if needed 212 | self._canvas[-self._xax_height() + 1, start:end] = list(label) 213 | # draw axis label 214 | if self._xlabel: 215 | xlabel = self._xlabel[: axis_end - axis_start] # make sure it fits 216 | self._center_draw(xlabel, self._canvas[-1, axis_start:axis_end]) 217 | 218 | def _draw_legend(self) -> None: 219 | width = max([len(label) for marker, label in self._labels]) + 4 220 | width = max(width, len("Legend") + 2) 221 | height = len(self._labels) + 2 222 | 223 | if self.legendloc.startswith("top"): 224 | top = int(self._yscale.transform(self._ytick_values[-1])) 225 | elif self.legendloc.startswith("bottom"): 226 | top = int(self._yscale.transform(self._ytick_values[0])) - height + 1 227 | if self.legendloc.endswith("right"): 228 | left = int(self._xscale.transform(self._xtick_values[-1])) - width + 1 229 | elif self.legendloc.endswith("left"): 230 | left = int(self._xscale.transform(self._xtick_values[0])) 231 | 232 | self._canvas[top, left : left + width] = list( 233 | "┌" + "Legend".center(width - 2, "─") + "┐" 234 | ) 235 | for i, (marker, label) in enumerate(self._labels): 236 | self._canvas[top + i + 1, left : left + width] = list( 237 | "│" + " " + label.ljust(width - 4) + "│" 238 | ) 239 | # the marker must be inserted separately in case of ANSI escape characters messing with the string length 240 | self._canvas[top + i + 1, left + 1] = marker 241 | self._canvas[top + len(self._labels) + 1, left : left + width] = list( 242 | "└" + "─" * (width - 2) + "┘" 243 | ) 244 | 245 | def _prep(self, x, y, marker, color, label) -> tuple: 246 | """Data preparation stuff common to all plots.""" 247 | x_is_valid = x is not None and len(x) > 0 248 | y_is_valid = y is not None and len(y) > 0 249 | if not x_is_valid and not y_is_valid: 250 | raise ValueError("`x` and/or `y` must be provided and not be empty") 251 | 252 | if x_is_valid and y is None: 253 | # only `x` is provided, assume `x` is `y` 254 | x, y = range(len(x)), x 255 | elif x is None and y_is_valid: 256 | # only `y` keyword argument is provided 257 | x = range(len(y)) 258 | 259 | if not len(x) == len(y): 260 | raise ValueError("`x` and `y` must have the same length") 261 | 262 | if marker == "braille": 263 | marker = "⠄" if not self.ascii_only else "." 264 | else: 265 | marker = marker[0] 266 | if color and not self.ascii_only: 267 | marker = colored(text=marker, color=color) 268 | if label: 269 | self._labels.append((marker, label)) 270 | self._clear_scale_cache() 271 | return x, y, marker, color, label 272 | 273 | def scatter( 274 | self, 275 | x: Optional[Iterable] = None, 276 | y: Optional[Iterable] = None, 277 | marker: str = "•", 278 | color: Optional[str] = None, 279 | label: Optional[str] = None, 280 | ) -> None: 281 | """ 282 | Adds scatter plot. 283 | 284 | Args: 285 | x: x data. If `y` is not provided, `x` is assumed to be y data. 286 | y: y data. 287 | marker: Marker used to draw points. Set to `"braille"` to use braille characters. 288 | color: Color of marker. Supported values are `"grey"`, `"red"`, `"green"`, `"yellow"`, `"blue"`, `"magenta"`, `"cyan"`, and `"white"`. 289 | label: Label to use for legend. 290 | """ 291 | x, y, marker, color, label = self._prep(x, y, marker, color, label) 292 | 293 | def draw_scatter(x, y, marker): 294 | for xi, yi in zip(self._xscale.transform(x), self._yscale.transform(y)): 295 | if not self.ascii_only and any((is_braille(char) for char in marker)): 296 | xi = utils._round_half_away_from_zero(xi) 297 | yi = utils._round_half_away_from_zero(yi) 298 | marker = draw_braille(xi, yi, self._canvas[yi, xi]) 299 | if color: 300 | marker = colored(marker, color) 301 | self._canvas[yi, xi] = marker 302 | else: 303 | self._canvas[round(yi), round(xi)] = marker 304 | 305 | self._plots.append(partial(draw_scatter, x=x, y=y, marker=marker)) 306 | 307 | def line( 308 | self, 309 | x: Optional[Iterable] = None, 310 | y: Optional[Iterable] = None, 311 | marker: str = "braille", 312 | color: Optional[str] = None, 313 | label: Optional[str] = None, 314 | ) -> None: 315 | """ 316 | Adds line plot. 317 | 318 | Args: 319 | x: x data. If `y` is not provided, `x` is assumed to be y data. 320 | y: y data. 321 | marker: Marker used to draw lines. Set to `"braille"` to use braille characters. 322 | color: Color of marker. Supported values are `"grey"`, `"red"`, `"green"`, `"yellow"`, `"blue"`, `"magenta"`, `"cyan"`, and `"white"`. 323 | label: Label to use for legend. 324 | """ 325 | x, y, marker, color, label = self._prep(x, y, marker, color, label) 326 | 327 | def draw_line(x, y, marker): 328 | xs = self._xscale.transform(x) 329 | ys = self._yscale.transform(y) 330 | for (x0, x1), (y0, y1) in zip(zip(xs[:-1], xs[1:]), zip(ys[:-1], ys[1:])): 331 | if not self.ascii_only and any((is_braille(char) for char in marker)): 332 | for x, y in utils._plot_line_segment( 333 | round(x0 * 2), round(y0 * 4), round(x1 * 2), round(y1 * 4) 334 | ): 335 | x = x / 2 336 | y = y / 4 337 | x_canvas = utils._round_half_away_from_zero(x) 338 | y_canvas = utils._round_half_away_from_zero(y) 339 | marker = draw_braille(x, y, self._canvas[y_canvas, x_canvas]) 340 | if color: 341 | marker = colored(marker, color) 342 | self._canvas[y_canvas, x_canvas] = marker 343 | else: 344 | for x, y in utils._plot_line_segment( 345 | round(x0), round(y0), round(x1), round(y1) 346 | ): 347 | self._canvas[y, x] = marker 348 | 349 | self._plots.append(partial(draw_line, x=x, y=y, marker=marker)) 350 | 351 | def bar( 352 | self, 353 | x: Optional[Iterable] = None, 354 | y: Optional[Iterable] = None, 355 | marker: str = "█", 356 | color: Optional[str] = None, 357 | label: Optional[str] = None, 358 | ) -> None: 359 | """ 360 | Adds vertical bar plot. 361 | 362 | Args: 363 | x: x data. If `y` is not provided, `x` is assumed to be y data. 364 | y: y data. 365 | marker: Marker used to draw bars. Set to `"braille"` to use braille characters. 366 | color: Color of marker. Supported values are `"grey"`, `"red"`, `"green"`, `"yellow"`, `"blue"`, `"magenta"`, `"cyan"`, and `"white"`. 367 | label: Label to use for legend. 368 | """ 369 | x, y, marker, color, label = self._prep(x, y, marker, color, label) 370 | 371 | def draw_bar(x, y, marker): 372 | marker = marker.replace("⠄", "⡇") # in case of braille 373 | if utils._is_numerical(self._y): 374 | origin = self._yscale.transform(min(self._ytick_values, key=abs)) 375 | else: 376 | origin = self._yscale.transform(self._ytick_values[0]) 377 | for xi, yi in zip(self._xscale.transform(x), self._yscale.transform(y)): 378 | start, end = sorted([origin, yi]) 379 | self._canvas[round(start) : round(end) + 1, round(xi)] = marker 380 | 381 | self._plots.append(partial(draw_bar, x=x, y=y, marker=marker)) 382 | 383 | def hbar( 384 | self, 385 | x: Optional[Iterable] = None, 386 | y: Optional[Iterable] = None, 387 | marker: str = "█", 388 | color: Optional[str] = None, 389 | label: Optional[str] = None, 390 | ) -> None: 391 | """ 392 | Adds horizontal bar plot. 393 | 394 | Args: 395 | x: x data. If `y` is not provided, `x` is assumed to be y data. 396 | y: y data. 397 | marker: Marker used to draw bars. Set to `"braille"` to use braille characters. 398 | color: Color of marker. Supported values are `"grey"`, `"red"`, `"green"`, `"yellow"`, `"blue"`, `"magenta"`, `"cyan"`, and `"white"`. 399 | label: Label to use for legend. 400 | """ 401 | x, y, marker, color, label = self._prep(x, y, marker, color, label) 402 | 403 | def draw_hbar(x, y, marker): 404 | marker = marker.replace("⠄", "⠒") # in case of braille 405 | if utils._is_numerical(self._x): 406 | origin = self._xscale.transform(min(self._xtick_values, key=abs)) 407 | else: 408 | origin = self._xscale.transform(self._xtick_values[0]) 409 | for xi, yi in zip(self._xscale.transform(x), self._yscale.transform(y)): 410 | start, end = sorted([origin, xi]) 411 | self._canvas[round(yi), round(start) : round(end) + 1] = marker 412 | 413 | self._plots.append(partial(draw_hbar, x=x, y=y, marker=marker)) 414 | 415 | def text(self, x, y, text: str, color: Optional[str] = None) -> None: 416 | """ 417 | Adds text. 418 | 419 | Args: 420 | x: x location (text is left-aligned). 421 | y: y location. 422 | text: Text to draw. 423 | color: Color of text. Supported values are `"grey"`, `"red"`, `"green"`, `"yellow"`, `"blue"`, `"magenta"`, `"cyan"`, and `"white"`. 424 | """ 425 | if color and not self.ascii_only: 426 | text = colored(text, color) 427 | 428 | def draw_text(x, y, text): 429 | x0 = round(self._xscale.transform(x[0])) 430 | y0 = round(self._yscale.transform(y[0])) 431 | for i, char in enumerate(text): 432 | if x0 + i >= self.width: 433 | break 434 | self._canvas[y0, x0 + i] = char 435 | 436 | self._plots.append(partial(draw_text, x=[x], y=[y], text=text)) 437 | 438 | def image( 439 | self, 440 | image: np.ndarray, 441 | vmin: Optional[float] = None, 442 | vmax: Optional[float] = None, 443 | cmap: str = "block", 444 | ) -> None: 445 | """ 446 | Adds image. 447 | 448 | Note that this sets the Y axis direction to point down, unless `y_axis_direction` is set otherwise in Figure init. 449 | 450 | Args: 451 | image: 2D array. 452 | vmin: Minimum value covered by the colormap. Lower values are clipped. 453 | If set to `None`, uses 0 if the `dtype` of image is `numpy.uint8` (usual for pictures), `min(image)` otherwise. 454 | vmax: Maximum value covered by the colormap. Higher values are clipped. 455 | If set to `None`, uses 255 if the `dtype` of image is `numpy.uint8` (usual for pictures), `max(image)` otherwise. 456 | cmap: Colormap used to map image values to characters. Currently supported cmaps are `"ascii"` and `"block"`. 457 | """ 458 | cmap = "ascii" if self.ascii_only else cmap 459 | # guess correct value range 460 | # if (image >= 0).all() and (image <= 1).all(): # between 0 and 1 inclusive 461 | # vmin = 0 if vmin is None else vmin 462 | # vmax = 1 if vmax is None else vmax 463 | if image.dtype == np.uint8: # probably a picture 464 | vmin = 0 if vmin is None else vmin 465 | vmax = 255 if vmax is None else vmax 466 | else: 467 | vmin = image.flatten().min() if vmin is None else vmin 468 | vmax = image.flatten().max() if vmax is None else vmax 469 | 470 | if self._y_axis_direction == "auto": 471 | self._y_axis_direction = "down" 472 | 473 | def draw_image(x, y): 474 | xmin = round(self._xscale.transform(0)) 475 | ymin = round(self._yscale.transform(0)) 476 | xmax = round(self._xscale.transform(image.shape[1])) 477 | ymax = round(self._yscale.transform(image.shape[0])) 478 | ymin, ymax = min(ymin, ymax), max(ymin, ymax) 479 | drawn = img2ascii( 480 | image, 481 | width=xmax - xmin + 1, 482 | height=ymax - ymin + 1, 483 | vmin=vmin, 484 | vmax=vmax, 485 | cmap=cmap, 486 | ) 487 | if self._y_axis_direction != "down": 488 | drawn = np.flip(drawn, axis=0) 489 | self._canvas[ymin : ymax + 1, xmin : xmax + 1] = drawn 490 | 491 | self._plots.append( 492 | partial( 493 | draw_image, 494 | x=tuple(range(image.shape[1] + 1)), 495 | y=tuple(range(image.shape[0] + 1)), 496 | ) 497 | ) 498 | self._clear_scale_cache() 499 | 500 | def _draw(self) -> None: 501 | if not self._plots: 502 | raise ValueError("No plots to draw.") 503 | 504 | # 8 (ANSI escape char) + 1 (marker) + 8 (ANSI escape char) = 17 505 | self._canvas = np.empty((self.height, self.width), dtype="U17") 506 | self._canvas[:] = " " 507 | 508 | try: 509 | if self.title: 510 | title = self.title[: self.width] # make sure it fits 511 | self._center_draw(title, self._canvas[0, :]) 512 | 513 | self._draw_x_axis() 514 | self._draw_y_axis() 515 | 516 | for plot in self._plots: 517 | plot() 518 | if self._labels: 519 | self._draw_legend() 520 | except IndexError: 521 | raise IndexError("Drawing out of bounds. Try increasing the figure size.") 522 | 523 | if self.ascii_only: 524 | for old, new in ASCII_FALLBACK.items(): 525 | self._canvas = np.char.replace(self._canvas, old, new) 526 | 527 | def clear(self) -> None: 528 | """Clears previously added plots.""" 529 | self._plots = [] 530 | self._labels = [] 531 | self._clear_scale_cache() 532 | 533 | def _clear_scale_cache(self) -> None: 534 | # clear cached values if cached, otherwise do nothing 535 | self.__dict__.pop("_xscale", None) 536 | self.__dict__.pop("_yscale", None) 537 | self.__dict__.pop("_xtick_values", None) 538 | self.__dict__.pop("_ytick_values", None) 539 | self.__dict__.pop("_yax_width", None) 540 | 541 | def __str__(self) -> str: 542 | self._draw() 543 | return "\n".join(["".join(row) for row in self._canvas.tolist()]) 544 | 545 | def show(self) -> None: 546 | """ 547 | Prints the figure. 548 | 549 | Note that to get the figure as a string (to write to a file, for example), you can simply convert it to str type: `str(fig)` 550 | """ 551 | print(str(self)) 552 | -------------------------------------------------------------------------------- /tplot/img2ascii.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | 3 | import numpy as np 4 | 5 | from .scales import LinearScale 6 | 7 | COLORMAPS = { 8 | # "ascii": "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\|()1{}[]?-_+~<>i!lI;:,\"^`'. "[::-1], 9 | "ascii": np.array(tuple(" .:-=+*#%@")), 10 | "block": np.array(tuple(" ░▒▓█")), 11 | } 12 | 13 | 14 | @lru_cache(maxsize=1) 15 | def _regular_meshgrid(xmin, ymin, xmax, ymax, **kwargs): 16 | return np.meshgrid(np.arange(xmin, xmax), np.arange(ymin, ymax), **kwargs) 17 | 18 | 19 | def resize(image: np.ndarray, shape: tuple) -> np.ndarray: 20 | """Nearest neighbor image resizing""" 21 | x, y = _regular_meshgrid(0, 0, shape[0], shape[1]) 22 | x = image.shape[0] * x / shape[0] 23 | y = image.shape[1] * y / shape[1] 24 | x = x.astype(int) 25 | y = y.astype(int) 26 | return image[x, y].T 27 | 28 | 29 | def img2ascii( 30 | image: np.ndarray, 31 | width: int, 32 | height: int, 33 | vmin: float, 34 | vmax: float, 35 | cmap: str = "block", 36 | ) -> np.ndarray: 37 | if len(image.shape) != 2: 38 | raise ValueError("Invalid shape for grayscale image") 39 | image = resize(image, (height, width)) 40 | scale = LinearScale() 41 | scale.fit([vmin, vmax], target_min=0, target_max=len(COLORMAPS[cmap]) - 1) 42 | cmap_idx = scale.transform(image.astype(float).clip(vmin, vmax)).round().astype(int) 43 | return COLORMAPS[cmap][cmap_idx] 44 | -------------------------------------------------------------------------------- /tplot/scales.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | import numpy as np 4 | 5 | 6 | class Scale: 7 | """Base `Scale` class.""" 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def transform(self, values): 13 | if isinstance(values, np.ndarray): 14 | if isinstance(self, CategoricalScale): 15 | return np.vectorize(self._transform)(values) 16 | else: 17 | return self._transform(values) 18 | elif isinstance(values, str): 19 | return self._transform(values) 20 | elif isinstance(values, Iterable): 21 | return [self._transform(v) for v in values] 22 | else: 23 | return self._transform(values) 24 | 25 | def _transform(self, value): 26 | raise NotImplementedError 27 | 28 | 29 | class LinearScale(Scale): 30 | """Transform numerical values linearly.""" 31 | 32 | def __init__(self): 33 | super().__init__() 34 | 35 | def fit(self, values, target_min, target_max): 36 | """Fit transform to linearly scale `values` to `target_min` and `target_max`.""" 37 | original_min = min(tuple(values)) 38 | original_max = max(tuple(values)) 39 | if original_min == original_max: 40 | original_min -= 1 41 | original_max += 1 42 | original_range = original_max - original_min 43 | target_range = target_max - target_min 44 | 45 | def _transform(value): 46 | return target_range * (value - original_min) / original_range + target_min 47 | 48 | self._transform = _transform 49 | 50 | 51 | class CategoricalScale(Scale): 52 | """Transform arbitrary values (e.g. strings) to numerical values.""" 53 | 54 | def __init__(self): 55 | super().__init__() 56 | 57 | def fit(self, values, target_min=0, target_max=None): 58 | """Fit transform to map `values` to numbers evenly spaced from `target_min` to `target_max`.""" 59 | values = [str(v) for v in values] 60 | idxmap = {value: i for i, value in enumerate(sorted(set(values)))} 61 | if target_min == 0 and target_max is None: 62 | target_max = len(idxmap) - 1 63 | scale = LinearScale() 64 | scale.fit(list(idxmap.values()), target_min, target_max) 65 | idxmap = {value: scale.transform([i])[0] for value, i in idxmap.items()} 66 | 67 | def _transform(value): 68 | return idxmap[str(value)] 69 | 70 | self._transform = _transform 71 | -------------------------------------------------------------------------------- /tplot/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | import sys 3 | from bisect import bisect 4 | from numbers import Number 5 | from typing import Generator, Iterable, List 6 | from warnings import warn 7 | 8 | 9 | def unicode_supported(test_str: str = "─│┤┬┌┐└┘█•·⣿") -> bool: 10 | """Tries to determine if unicode is supported by encoding a test string containing unicode characters.""" 11 | try: 12 | test_str.encode(sys.stdout.encoding) 13 | return True 14 | except UnicodeEncodeError: 15 | return False 16 | 17 | 18 | def _is_numerical(data: Iterable[Number]) -> bool: 19 | """Returns True if all values in given iterable are numbers.""" 20 | return all([isinstance(value, Number) for value in data]) 21 | 22 | 23 | def _plot_line_segment( 24 | x0: int, y0: int, x1: int, y1: int 25 | ) -> Generator[Iterable[int], None, None]: 26 | """Plot line segment using Bresenham algorithm. Yields (x, y).""" 27 | dx = x1 - x0 28 | dy = y1 - y0 29 | axes_swapped = False 30 | if abs(dy) > abs(dx): # ensure slope is not >1 31 | axes_swapped = True 32 | x0, y0, x1, y1 = y0, x0, y1, x1 33 | if x0 > x1: # always draw left to right 34 | x0, x1 = x1, x0 35 | y0, y1 = y1, y0 36 | dx = x1 - x0 37 | dy = y1 - y0 38 | yi = 1 39 | if dy < 0: # switch sign of slope 40 | yi = -1 41 | dy = -dy 42 | D = 2 * dy - dx 43 | y = y0 44 | 45 | for x in range(x0, x1 + 1): 46 | yield (y, x) if axes_swapped else (x, y) 47 | if D > 0: 48 | y += yi 49 | D -= 2 * dx 50 | D += 2 * dy 51 | 52 | 53 | def _round_away_from_zero(value: float) -> int: 54 | return math.ceil(value) if value >= 0 else math.floor(value) 55 | 56 | 57 | def _round_half_away_from_zero(num: float) -> int: 58 | return ((num > 0) - (num < 0)) * int(abs(num) + 0.5) 59 | 60 | 61 | def _best_ticks(min_: float, max_: float, most: int) -> list: 62 | """Returns a list of suitable tick values.""" 63 | most = max(most, 1) 64 | # find step size 65 | range_ = max_ - min_ 66 | if range_ == 0: 67 | return [min_] 68 | min_step = range_ / most 69 | magnitude = 10 ** math.floor(math.log(min_step, 10)) 70 | residual = min_step / magnitude 71 | possible_steps = [1, 2, 5, 10] 72 | step = possible_steps[bisect(possible_steps, residual)] if residual < 10 else 10 73 | step *= magnitude 74 | # generate ticks 75 | sign = math.copysign(1, min_) 76 | start = step * round(abs(min_) / step) * sign 77 | if start > min_: 78 | start -= step 79 | return [ 80 | start + i * step 81 | for i in range(_round_away_from_zero((max_ - start) / step) + 1) 82 | ] 83 | 84 | 85 | def _optimize_xticklabel_anchors( 86 | tick_positions: List[int], 87 | labels: List[str], 88 | width: int, 89 | margin: int = 2, 90 | stepsize: float = 0.1, 91 | tolerance: float = 0.3, 92 | max_iterations: int = 1000, 93 | ) -> List[List[int]]: 94 | """ 95 | Models the placement of tick labels as a 1-dimensional case of a force-directed graph. 96 | Spring forces between the labels are simulated iteratively until they stabilize. 97 | 98 | Args: 99 | tick_positions: Ordered positions of the ticks. 100 | labels: Tick labels. 101 | width: Width of plot. 102 | margin: Margin between labels. 103 | stepsize: Spring force simulation step size. 104 | tolerance: Tolerance for termination. 105 | max_iterations: Maximum number of iterations. 106 | Returns: 107 | List of [start, end] positions of labels. 108 | """ 109 | anchors = [] 110 | for tick_pos, label in zip(tick_positions, labels): 111 | left = tick_pos - len(label) // 2 112 | right = left + len(label) 113 | # if anchor is out of bounds, move it inside bounds 114 | d = right - width 115 | if d > 0: 116 | left -= d 117 | right -= d 118 | anchors.append([left, right]) 119 | 120 | def calc_forces(anchors): 121 | forces = [0] * len(anchors) 122 | # forces between labels 123 | for i in range(len(anchors) - 1): 124 | f = max(0, anchors[i][1] + margin - anchors[i + 1][0]) 125 | forces[i] -= f 126 | forces[i + 1] += f 127 | # figure boundary forces 128 | forces[0] -= min(0, anchors[0][0]) 129 | forces[-1] -= max(0, anchors[-1][1] - width) 130 | return forces 131 | 132 | prev_total_forces = float("inf") 133 | forces = calc_forces(anchors) 134 | total_forces = sum([abs(f) for f in forces]) 135 | 136 | if total_forces == 0: 137 | return anchors # early return 138 | 139 | iterations = 0 140 | while abs(total_forces - prev_total_forces) > tolerance: 141 | for anchor, force, tick_pos in zip(anchors, forces, tick_positions): 142 | anchor[0] += force * stepsize 143 | anchor[1] += force * stepsize 144 | # don't move beyond tick position 145 | if round(anchor[0]) > tick_pos: 146 | d = round(anchor[0]) - tick_pos 147 | anchor[0] -= d 148 | anchor[1] -= d 149 | elif round(anchor[1]) - 1 < tick_pos: 150 | d = tick_pos - round(anchor[1]) + 1 151 | anchor[0] += d 152 | anchor[1] += d 153 | 154 | # recalculate forces 155 | prev_total_forces = sum([abs(f) for f in forces]) 156 | forces = calc_forces(anchors) 157 | total_forces = sum([abs(f) for f in forces]) 158 | 159 | iterations += 1 160 | # if iterations == 3: 161 | # break 162 | if iterations >= max_iterations: 163 | warn( 164 | f"Max iterations (={max_iterations}) during X axis label placement reached." 165 | ) 166 | break 167 | 168 | # round anchors 169 | anchors = [ 170 | [round(anchor[0]), round(anchor[0]) + len(label)] 171 | for anchor, label in zip(anchors, labels) 172 | ] 173 | # limit to figure boundaries 174 | for anchor in anchors: 175 | anchor[0] = max(0, anchor[0]) 176 | anchor[1] = min(width, anchor[1]) 177 | # don't overwrite other labels 178 | for i in range(len(anchors)): 179 | if i > 0: 180 | anchors[i][0] = max(tick_positions[i - 1] + 1, anchors[i][0]) 181 | if i < len(anchors) - 1: 182 | anchors[i][1] = min(tick_positions[i + 1], anchors[i][1]) 183 | return anchors 184 | --------------------------------------------------------------------------------