├── .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 | [](https://tplot.readthedocs.io/en/latest/)
4 | [](https://github.com/JeroenDelcour/tplot/actions/workflows/tests.yml)
5 | [](https://codecov.io/gh/JeroenDelcour/tplot)
6 |
7 | [](https://pypi.org/project/tplot/)
8 | [](https://pypi.org/project/tplot/)
9 | [](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 | 
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 | 
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┤ [34m⡇[0m
3 | │ [34m⡇[0m
4 | │ [34m⡇[0m
5 | 10┤ [34m⡇[0m [34m⡇[0m
6 | │ [34m⡇[0m [34m⡇[0m
7 | │ [34m⡇[0m [34m⡇[0m
8 | 9┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m
9 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
10 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
11 | 8┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
12 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
13 | 7┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
14 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
15 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
16 | 6┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
17 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
18 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
19 | 5┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
20 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
21 | │[34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
22 | 4┤[34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
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┤ [34m⡇[0m
3 | │ [34m⡇[0m
4 | │ [34m⡇[0m
5 | │ [34m⡇[0m
6 | │ [34m⡇[0m
7 | rice┤[34m⡇[0m [34m⡇[0m
8 | │[34m⡇[0m [34m⡇[0m
9 | │[34m⡇[0m [34m⡇[0m
10 | │[34m⡇[0m [34m⡇[0m
11 | │[34m⡇[0m [34m⡇[0m
12 | pasta┤[34m⡇[0m [34m⡇[0m
13 | │[34m⡇[0m [34m⡇[0m
14 | │[34m⡇[0m [34m⡇[0m
15 | │[34m⡇[0m [34m⡇[0m
16 | │[34m⡇[0m [34m⡇[0m
17 | pancakes┤[34m⡇[0m [34m⡇[0m
18 | │[34m⡇[0m [34m⡇[0m
19 | │[34m⡇[0m [34m⡇[0m
20 | │[34m⡇[0m [34m⡇[0m
21 | │[34m⡇[0m [34m⡇[0m
22 | ice cream┤[34m⡇[0m [34m⡇[0m
23 | ┬────────────────────────────────────────────────────────────────────┬
24 | cheese chocolate
--------------------------------------------------------------------------------
/tests/reference_figures/braille_bar_iris.txt:
--------------------------------------------------------------------------------
1 |
2 | I. virginica┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m
3 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
4 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
5 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
6 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
7 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
8 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
9 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
10 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
11 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m
12 | I. versicolor┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
13 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
14 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
15 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
16 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
17 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
18 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
19 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
20 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
21 | │ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
22 | I. setosa┤ [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m [34m⡇[0m
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┤ [32m⢀[0m
3 | │ [32m⢠[0m[32m⠋[0m[32m⢆[0m
4 | │ [32m⢠[0m[32m⠃[0m [32m⠘[0m[32m⡄[0m
5 | 10┤ [32m⡰[0m[32m⠁[0m [32m⠸[0m[32m⡀[0m [32m⡔[0m
6 | │ [32m⡰[0m[32m⠁[0m [32m⢱[0m [32m⡜[0m
7 | │ [32m⡰[0m[32m⠁[0m [32m⢣[0m [32m⢀[0m[32m⠜[0m
8 | 9┤ [32m⡠[0m[32m⣀[0m [32m⡜[0m [32m⢇[0m [32m⢀[0m[32m⠎[0m
9 | │ [32m⢀[0m[32m⠔[0m[32m⠁[0m [32m⠉[0m[32m⠢[0m[32m⢄[0m[32m⡀[0m [32m⡜[0m [32m⠈[0m[32m⡆[0m [32m⢀[0m[32m⠎[0m
10 | │ [32m⢠[0m[32m⠊[0m [32m⠈[0m[32m⠒[0m[32m⠤[0m[32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠤[0m[32m⠒[0m[32m⠒[0m[32m⠊[0m[32m⠉[0m [32m⠘[0m[32m⡄[0m [32m⢠[0m[32m⠃[0m
11 | 8┤ [32m⡔[0m[32m⠁[0m [32m⠱[0m[32m⣠[0m[32m⠃[0m
12 | │ [32m⢀[0m[32m⠎[0m [32m⠁[0m
13 | 7┤ [32m⢠[0m[32m⠊[0m[32m⢆[0m [32m⡰[0m[32m⠁[0m
14 | │ [32m⢀[0m[32m⠔[0m[32m⠁[0m [32m⠈[0m[32m⢆[0m [32m⡜[0m
15 | │ [32m⡠[0m[32m⠃[0m [32m⢣[0m [32m⢀[0m[32m⠎[0m
16 | 6┤ [32m⢠[0m[32m⠊[0m [32m⠣[0m[32m⡀[0m [32m⢠[0m[32m⠊[0m
17 | │ [32m⢀[0m[32m⠔[0m[32m⠁[0m [32m⠱[0m[32m⡀[0m [32m⡠[0m[32m⠃[0m
18 | │ [32m⢀[0m[32m⠔[0m[32m⠁[0m [32m⠑[0m[32m⡄[0m [32m⡰[0m[32m⠁[0m
19 | 5┤ [32m⡰[0m[32m⠁[0m [32m⠘[0m[32m⡄[0m[32m⡜[0m
20 | │ [32m⡠[0m[32m⠊[0m [32m⠈[0m
21 | │[32m⠠[0m[32m⠊[0m
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┤ [32m⣀[0m[32m⣀[0m[32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠤[0m[32m⠤[0m[32m⠒[0m[32m⢲[0m
3 | │ [32m⢀[0m[32m⣀[0m[32m⣀[0m[32m⣀[0m[32m⠤[0m[32m⠤[0m[32m⠤[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠊[0m[32m⠉[0m[32m⠉[0m[32m⠉[0m [32m⢸[0m
4 | │ [32m⢀[0m[32m⣀[0m[32m⣀[0m[32m⣀[0m[32m⠤[0m[32m⠤[0m[32m⠤[0m[32m⠔[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠉[0m[32m⠉[0m[32m⠉[0m[32m⠁[0m [32m⢸[0m
5 | │ [32m⢀[0m[32m⣀[0m[32m⣀[0m[32m⣀[0m[32m⠤[0m[32m⠤[0m[32m⠤[0m[32m⠔[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠉[0m[32m⠉[0m[32m⠉[0m[32m⠁[0m [32m⢸[0m
6 | │ [32m⣀[0m[32m⣀[0m[32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠤[0m[32m⠤[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠊[0m[32m⠉[0m[32m⠉[0m[32m⠁[0m [32m⢸[0m
7 | rice┤[32m⠐[0m[32m⠲[0m[32m⢎[0m[32m⡉[0m[32m⠉[0m[32m⠉[0m [32m⢸[0m
8 | │ [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠤[0m[32m⣀[0m [32m⢸[0m
9 | │ [32m⠉[0m[32m⠑[0m[32m⠢[0m[32m⢄[0m[32m⣀[0m [32m⢸[0m
10 | │ [32m⠉[0m[32m⠒[0m[32m⠤[0m[32m⢄[0m[32m⡀[0m [32m⢸[0m
11 | │ [32m⠈[0m[32m⠑[0m[32m⠢[0m[32m⠤[0m[32m⣀[0m [32m⢸[0m
12 | pasta┤[32m⠐[0m[32m⠢[0m[32m⠤[0m[32m⢄[0m[32m⣀[0m [32m⠉[0m[32m⠒[0m[32m⠢[0m[32m⢄[0m[32m⡀[0m [32m⢸[0m
13 | │ [32m⠉[0m[32m⠉[0m[32m⠒[0m[32m⠢[0m[32m⠤[0m[32m⣀[0m[32m⣀[0m [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠤[0m[32m⣀[0m [32m⢸[0m
14 | │ [32m⠉[0m[32m⠑[0m[32m⠒[0m[32m⠢[0m[32m⠤[0m[32m⣀[0m[32m⣀[0m [32m⠉[0m[32m⠑[0m[32m⠢[0m[32m⢄[0m[32m⣀[0m [32m⢸[0m
15 | │ [32m⠉[0m[32m⠑[0m[32m⠒[0m[32m⠢[0m[32m⠤[0m[32m⣀[0m[32m⣀[0m [32m⠉[0m[32m⠒[0m[32m⠤[0m[32m⢄[0m[32m⡀[0m [32m⢸[0m
16 | │ [32m⠉[0m[32m⠑[0m[32m⠒[0m[32m⠤[0m[32m⠤[0m[32m⣀[0m[32m⡀[0m [32m⠈[0m[32m⠑[0m[32m⠢[0m[32m⠤[0m[32m⣀[0m [32m⢸[0m
17 | pancakes┤ [32m⠈[0m[32m⠉[0m[32m⠑[0m[32m⠒[0m[32m⠤[0m[32m⠤[0m[32m⣀[0m[32m⡀[0m [32m⠉[0m[32m⠒[0m[32m⠢[0m[32m⢄[0m[32m⡀[0m [32m⠘[0m
18 | │ [32m⠈[0m[32m⠉[0m[32m⠑[0m[32m⠒[0m[32m⠤[0m[32m⢄[0m[32m⣀[0m[32m⡀[0m [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠤[0m[32m⣀[0m
19 | │ [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠒[0m[32m⠤[0m[32m⢄[0m[32m⣀[0m[32m⡀[0m[32m⠉[0m[32m⠑[0m[32m⠢[0m[32m⢄[0m[32m⣀[0m
20 | │ [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠒[0m[32m⠤[0m[32m⢄[0m[32m⣉[0m[32m⡒[0m[32m⠤[0m[32m⢄[0m[32m⡀[0m
21 | │ [32m⠈[0m[32m⠉[0m[32m⠒[0m[32m⠪[0m[32m⠵[0m[32m⢦[0m[32m⣤[0m[32m⣀[0m
22 | ice cream┤ [32m⠉[0m[32m⠉[0m[32m⠒[0m
23 | ┬────────────────────────────────────────────────────────────────────┬
24 | cheese chocolate
--------------------------------------------------------------------------------
/tests/reference_figures/braille_line_iris.txt:
--------------------------------------------------------------------------------
1 |
2 | I. virginica┤ [32m⠐[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⢲[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠂[0m
3 | │ [32m⠑[0m[32m⡄[0m
4 | │ [32m⠈[0m[32m⢢[0m
5 | │ [32m⠑[0m[32m⡄[0m
6 | │ [32m⠈[0m[32m⢢[0m
7 | │ [32m⠑[0m[32m⡄[0m
8 | │ [32m⠈[0m[32m⢢[0m
9 | │ [32m⠑[0m[32m⡄[0m
10 | │ [32m⠈[0m[32m⢢[0m
11 | │ [32m⠑[0m[32m⡄[0m
12 | I. versicolor┤ [32m⠐[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⣚[0m[32m⡲[0m[32m⠶[0m[32m⠒[0m
13 | │ [32m⢀[0m[32m⣀[0m[32m⠤[0m[32m⠔[0m[32m⠊[0m[32m⠉[0m
14 | │ [32m⢀[0m[32m⣀[0m[32m⠤[0m[32m⠔[0m[32m⠒[0m[32m⠉[0m[32m⠁[0m
15 | │ [32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠒[0m[32m⠉[0m[32m⠁[0m
16 | │ [32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠒[0m[32m⠊[0m[32m⠉[0m
17 | │ [32m⢀[0m[32m⣀[0m[32m⠤[0m[32m⠔[0m[32m⠊[0m[32m⠉[0m
18 | │ [32m⢀[0m[32m⣀[0m[32m⠤[0m[32m⠔[0m[32m⠒[0m[32m⠉[0m[32m⠁[0m
19 | │ [32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠒[0m[32m⠉[0m[32m⠁[0m
20 | │ [32m⣀[0m[32m⡠[0m[32m⠤[0m[32m⠒[0m[32m⠊[0m[32m⠉[0m
21 | │ [32m⢀[0m[32m⣀[0m[32m⠤[0m[32m⠔[0m[32m⠊[0m[32m⠉[0m
22 | I. setosa┤ [32m⠒[0m[32m⠛[0m[32m⠓[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠒[0m[32m⠂[0m
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┤ [31m⠐[0m
3 | │
4 | │
5 | 10┤ [31m⠐[0m
6 | │
7 | │
8 | 9┤ [31m⠐[0m
9 | │
10 | │ [31m⠐[0m [31m⠐[0m
11 | 8┤
12 | │ [31m⠐[0m
13 | 7┤ [31m⠐[0m
14 | │ [31m⠐[0m
15 | │
16 | 6┤
17 | │ [31m⠐[0m
18 | │
19 | 5┤
20 | │ [31m⠐[0m
21 | │[31m⠐[0m
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┤ [31m⠐[0m
3 | │
4 | │
5 | │
6 | │
7 | rice┤[31m⠐[0m
8 | │
9 | │
10 | │
11 | │
12 | pasta┤[31m⠐[0m
13 | │
14 | │
15 | │
16 | │
17 | pancakes┤ [31m⠐[0m
18 | │
19 | │
20 | │
21 | │
22 | ice cream┤ [31m⠐[0m
23 | ┬────────────────────────────────────────────────────────────────────┬
24 | cheese chocolate
--------------------------------------------------------------------------------
/tests/reference_figures/braille_scatter_iris.txt:
--------------------------------------------------------------------------------
1 |
2 | I. virginica┤ [31m⠐[0m [31m⠐[0m [31m⠐[0m
3 | │
4 | │
5 | │
6 | │
7 | │
8 | │
9 | │
10 | │
11 | │
12 | I. versicolor┤ [31m⠐[0m [31m⠐[0m [31m⠐[0m
13 | │
14 | │
15 | │
16 | │
17 | │
18 | │
19 | │
20 | │
21 | │
22 | I. setosa┤ [31m⠐[0m [31m⠐[0m [31m⠐[0m
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┤ [37m•[0m
3 | │
4 | │
5 | 6┤ [30m•[0m
6 | │
7 | │
8 | 5┤ [36m•[0m
9 | │
10 | │
11 | 4┤ [35m•[0m
12 | │
13 | 3┤ [33m•[0m ┌──Legend─┐
14 | │ │[31m•[0m red │
15 | │ │[32m•[0m green │
16 | 2┤ [34m•[0m │[34m•[0m blue │
17 | │ │[33m•[0m yellow │
18 | │ │[35m•[0m magenta│
19 | 1┤ [32m•[0m │[36m•[0m cyan │
20 | │ │[30m•[0m grey │
21 | │ │[37m•[0m white │
22 | 0┤[31m•[0m └─────────┘
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 | │[31mtesting colored text[0m
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 |
--------------------------------------------------------------------------------