├── tests
├── __init__.py
├── test_helpers.py
├── test_column_def.py
├── test_font.py
├── test_cmap.py
├── conftest.py
├── test_formatters.py
├── test_cell.py
└── test_table.py
├── requirements.txt
├── docs
├── dev
│ ├── contributing.rst
│ └── changelog.rst
├── modules.rst
├── example_notebooks
│ ├── country_flags
│ │ ├── USA.png
│ │ ├── Chile.png
│ │ ├── China.png
│ │ ├── Italy.png
│ │ ├── Japan.png
│ │ ├── Spain.png
│ │ ├── Brazil.png
│ │ ├── Cameroon.png
│ │ ├── Canada.png
│ │ ├── England.png
│ │ ├── France.png
│ │ ├── Germany.png
│ │ ├── Jamaica.png
│ │ ├── Nigeria.png
│ │ ├── Norway.png
│ │ ├── Scotland.png
│ │ ├── Sweden.png
│ │ ├── Thailand.png
│ │ ├── Argentina.png
│ │ ├── Australia.png
│ │ ├── Netherlands.png
│ │ ├── New Zealand.png
│ │ ├── South Africa.png
│ │ └── South Korea.png
│ ├── images
│ │ ├── calender.png
│ │ ├── wwc_table.png
│ │ ├── basic_table.png
│ │ ├── alternating_row_color.png
│ │ └── bohndesliga_table_recreation.png
│ ├── bundesliga_crests_22_23
│ │ ├── Mainz.png
│ │ ├── FC Cologne.png
│ │ ├── RB Leipzig.png
│ │ ├── Schalke 04.png
│ │ ├── VfL Bochum.png
│ │ ├── Bayern Munich.png
│ │ ├── FC Augsburg.png
│ │ ├── Hertha Berlin.png
│ │ ├── SC Freiburg.png
│ │ ├── VfB Stuttgart.png
│ │ ├── VfL Wolfsburg.png
│ │ ├── Werder Bremen.png
│ │ ├── TSG Hoffenheim.png
│ │ ├── 1. FC Union Berlin.png
│ │ ├── Bayer Leverkusen.png
│ │ ├── Borussia Dortmund.png
│ │ ├── Eintracht Frankfurt.png
│ │ └── Borussia Monchengladbach.png
│ ├── data
│ │ └── wwc_forecasts.csv
│ └── heatmap.ipynb
├── Makefile
├── make.bat
├── notebooks
│ ├── cmap.ipynb
│ ├── font.ipynb
│ ├── rows_and_columns.ipynb
│ ├── table.ipynb
│ └── plots.ipynb
├── plottable.rst
├── conf.py
└── index.rst
├── docs-requirements.txt
├── plottable
├── __init__.py
├── helpers.py
├── cmap.py
├── formatters.py
├── font.py
├── column_def.py
├── plots.py
├── cell.py
└── table.py
├── .readthedocs.yaml
├── changelog.rst
├── LICENSE.txt
├── setup.py
├── contributing.rst
├── README.md
└── .gitignore
/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib
2 | numpy
3 | pandas
4 | Pillow
--------------------------------------------------------------------------------
/docs/dev/contributing.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../contributing.rst
--------------------------------------------------------------------------------
/docs/dev/changelog.rst:
--------------------------------------------------------------------------------
1 | .. include:: ../../changelog.rst
2 |
3 |
--------------------------------------------------------------------------------
/docs-requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx==4.5.0
2 | sphinx-book-theme==0.3.3
3 | myst-nb
4 | ipykernel
5 | plottable
--------------------------------------------------------------------------------
/docs/modules.rst:
--------------------------------------------------------------------------------
1 | plottable
2 | =========
3 |
4 | .. toctree::
5 | :maxdepth: 4
6 |
7 | plottable
8 |
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/USA.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/USA.png
--------------------------------------------------------------------------------
/docs/example_notebooks/images/calender.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/images/calender.png
--------------------------------------------------------------------------------
/docs/example_notebooks/images/wwc_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/images/wwc_table.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Chile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Chile.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/China.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/China.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Italy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Italy.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Japan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Japan.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Spain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Spain.png
--------------------------------------------------------------------------------
/docs/example_notebooks/images/basic_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/images/basic_table.png
--------------------------------------------------------------------------------
/plottable/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.5"
2 |
3 | from .column_def import ColDef, ColumnDefinition, ColumnType
4 | from .table import Table
5 |
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Brazil.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Brazil.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Cameroon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Cameroon.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Canada.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Canada.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/England.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/England.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/France.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/France.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Germany.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Germany.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Jamaica.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Jamaica.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Nigeria.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Nigeria.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Norway.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Norway.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Scotland.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Scotland.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Sweden.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Sweden.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Thailand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Thailand.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Argentina.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Argentina.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Australia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Australia.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/Netherlands.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/Netherlands.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/New Zealand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/New Zealand.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/South Africa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/South Africa.png
--------------------------------------------------------------------------------
/docs/example_notebooks/country_flags/South Korea.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/country_flags/South Korea.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Mainz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Mainz.png
--------------------------------------------------------------------------------
/docs/example_notebooks/images/alternating_row_color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/images/alternating_row_color.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/FC Cologne.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/FC Cologne.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/RB Leipzig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/RB Leipzig.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Schalke 04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Schalke 04.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/VfL Bochum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/VfL Bochum.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Bayern Munich.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Bayern Munich.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/FC Augsburg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/FC Augsburg.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Hertha Berlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Hertha Berlin.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/SC Freiburg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/SC Freiburg.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/VfB Stuttgart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/VfB Stuttgart.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/VfL Wolfsburg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/VfL Wolfsburg.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Werder Bremen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Werder Bremen.png
--------------------------------------------------------------------------------
/docs/example_notebooks/images/bohndesliga_table_recreation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/images/bohndesliga_table_recreation.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/TSG Hoffenheim.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/TSG Hoffenheim.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/1. FC Union Berlin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/1. FC Union Berlin.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Bayer Leverkusen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Bayer Leverkusen.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Borussia Dortmund.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Borussia Dortmund.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Eintracht Frankfurt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Eintracht Frankfurt.png
--------------------------------------------------------------------------------
/docs/example_notebooks/bundesliga_crests_22_23/Borussia Monchengladbach.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/znstrider/plottable/HEAD/docs/example_notebooks/bundesliga_crests_22_23/Borussia Monchengladbach.png
--------------------------------------------------------------------------------
/plottable/helpers.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 |
4 | def _replace_lw_key(d) -> Dict[str, Any]:
5 | """Replaces the "lw" key in a Dictionary with "linewidth"."""
6 | if "lw" in d:
7 | d["linewidth"] = d.pop("lw")
8 | return d
9 |
--------------------------------------------------------------------------------
/tests/test_helpers.py:
--------------------------------------------------------------------------------
1 | from plottable.helpers import _replace_lw_key
2 |
3 |
4 | def test_replace_lw_key():
5 | d = {"lw": 1}
6 |
7 | d = _replace_lw_key(d)
8 |
9 | assert "lw" not in d
10 | assert "linewidth" in d
11 | assert d["linewidth"] == 1
12 |
--------------------------------------------------------------------------------
/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/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 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
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 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # .readthedocs.yaml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Set the version of Python and other tools you might need
9 | build:
10 | os: ubuntu-20.04
11 | tools:
12 | python: "3.10"
13 | # You can also specify other tool versions:
14 | # nodejs: "16"
15 | # rust: "1.55"
16 | # golang: "1.17"
17 |
18 | # Build documentation in the docs/ directory with Sphinx
19 | sphinx:
20 | configuration: docs/conf.py
21 | fail_on_warning: true
22 |
23 | # If using Sphinx, optionally build your docs in additional formats such as PDF
24 | # formats:
25 | # - pdf
26 |
27 | # Optionally declare the Python requirements required to build your docs
28 | python:
29 | install:
30 | - requirements: requirements.txt
31 | - requirements: docs-requirements.txt
32 | - method: pip
33 | path: .
--------------------------------------------------------------------------------
/tests/test_column_def.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from plottable.column_def import ColumnDefinition, _filter_none_values
3 |
4 |
5 | @pytest.fixture
6 | def col_def() -> ColumnDefinition:
7 | return ColumnDefinition(name="col", title="Column")
8 |
9 |
10 | def test_filter_none_values():
11 | d = {"a": 1, "b": {}, "c": None}
12 | assert _filter_none_values(d) == {"a": 1, "b": {}}
13 |
14 |
15 | def test_column_definition_as_dict(col_def):
16 | assert col_def._asdict() == {
17 | "name": "col",
18 | "title": "Column",
19 | "width": 1,
20 | "textprops": {},
21 | "formatter": None,
22 | "cmap": None,
23 | "text_cmap": None,
24 | "group": None,
25 | "plot_fn": None,
26 | "plot_kw": {},
27 | "border": None,
28 | }
29 |
30 |
31 | def test_column_definition_as_non_none_dict(col_def):
32 | assert col_def._as_non_none_dict() == {
33 | "name": "col",
34 | "title": "Column",
35 | "width": 1,
36 | "textprops": {},
37 | "plot_kw": {},
38 | }
39 |
--------------------------------------------------------------------------------
/changelog.rst:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | All notable changes to this project will be documented in this file.
5 |
6 | 0.1.5
7 | =====
8 | - allow python>=3.7
9 |
10 |
11 | 0.1.4
12 | =====
13 |
14 | - add light_color and dark_color arguments to plottable.font autoset fontcolor functions
15 | - inverse the yaxis of the table axes. This aligns the indices of table.rows with the integer location (iloc) of the row in the DataFrame. Alongside that change col_label_row and col_group_labels now have negative indices (y-locations).
16 | - add an apply_formatter function to formatters. This can now be used to also apply builtin string formatter syntax within plots.
17 | - allow for custom height of col_label_row
18 | - require python>=3.10
19 | - add Bohndesliga table example
20 |
21 |
22 | 0.1.3
23 | =====
24 |
25 | - Allow for string representations of builtin string formatters to ColumnDefinitions formatters.
26 |
27 |
28 | 0.1.2
29 | =====
30 |
31 | Fixed
32 | -----
33 | - Fix bboxes not being drawn for table texts
34 |
35 |
36 | Documentation
37 | -------------
38 | - Add .readthedocs.yaml config
--------------------------------------------------------------------------------
/tests/test_font.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from plottable.font import (
4 | contrasting_font_color,
5 | contrasting_font_color_,
6 | contrasting_font_color_w3c,
7 | )
8 |
9 | font_color_fns = (
10 | contrasting_font_color,
11 | contrasting_font_color_,
12 | contrasting_font_color_w3c,
13 | )
14 |
15 |
16 | @pytest.mark.parametrize("fn", font_color_fns)
17 | def test_font_color_fns_black_on_white_bg(fn):
18 | white = (1, 1, 1)
19 | assert fn(white) == "#000000"
20 |
21 |
22 | @pytest.mark.parametrize("fn", font_color_fns)
23 | def test_font_color_fns_white_on_black_bg(fn):
24 | black = (0, 0, 0)
25 | assert fn(black) == "#ffffff"
26 |
27 |
28 | @pytest.mark.parametrize("fn", font_color_fns)
29 | def test_font_color_fns_custom_dark_on_white_bg(fn):
30 | white = (1, 1, 1)
31 | custom = "#252526"
32 | assert fn(white, dark_color=custom) == custom
33 |
34 |
35 | @pytest.mark.parametrize("fn", font_color_fns)
36 | def test_font_color_fns_custom_light_on_black_bg(fn):
37 | black = (0, 0, 0)
38 | custom = "#f0f0f0"
39 | assert fn(black, light_color=custom) == custom
40 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 | ===========
3 |
4 | Copyright (c) 2022 znstrider
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | **The software is provided "as is", without warranty of any kind, express or
17 | implied, including but not limited to the warranties of merchantability,
18 | fitness for a particular purpose and noninfringement. In no event shall the
19 | authors or copyright holders be liable for any claim, damages or other
20 | liability, whether in an action of contract, tort or otherwise, arising from,
21 | out of or in connection with the software or the use or other dealings in the
22 | software.**
--------------------------------------------------------------------------------
/docs/notebooks/cmap.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Using ColorMaps"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "You can provide a `cmap (Callable | LinearSegmentedColormap)` argument to a ColumnDefinition to color each of the columns cells backgrounds based on their values."
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "metadata": {},
20 | "source": [
21 | "```{seealso}\n",
22 | "You can find an example of using a colormap in the [Women's World Cup Example](../example_notebooks/wwc_example.ipynb)\n",
23 | "```"
24 | ]
25 | }
26 | ],
27 | "metadata": {
28 | "kernelspec": {
29 | "display_name": "Python 3.10.5 ('env': venv)",
30 | "language": "python",
31 | "name": "python3"
32 | },
33 | "language_info": {
34 | "name": "python",
35 | "version": "3.10.5"
36 | },
37 | "orig_nbformat": 4,
38 | "vscode": {
39 | "interpreter": {
40 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
41 | }
42 | }
43 | },
44 | "nbformat": 4,
45 | "nbformat_minor": 2
46 | }
47 |
--------------------------------------------------------------------------------
/tests/test_cmap.py:
--------------------------------------------------------------------------------
1 | import matplotlib
2 | import pandas as pd
3 | from plottable.cmap import normed_cmap
4 |
5 |
6 | def test_normed_cmap():
7 | s = pd.Series(list(range(0, 11)))
8 | cmap_fn = normed_cmap(s, matplotlib.cm.PiYG)
9 | assert cmap_fn(10) == (
10 | 0.49034986543637066,
11 | 0.7307958477508651,
12 | 0.24998077662437523,
13 | 1.0,
14 | )
15 | assert cmap_fn(5) == (
16 | 0.9673202614379085,
17 | 0.968473663975394,
18 | 0.9656286043829296,
19 | 1.0,
20 | )
21 | assert cmap_fn(0) == (
22 | 0.8667435601691658,
23 | 0.45251826220684355,
24 | 0.6748173779315648,
25 | 1.0,
26 | )
27 |
28 |
29 | def test_normed_cmap_three_stds():
30 | s = pd.Series(list(range(0, 11)))
31 | cmap_fn = normed_cmap(s, matplotlib.cm.PiYG, num_stds=3)
32 | assert cmap_fn(10) == (
33 | 0.6032295271049597,
34 | 0.8055363321799309,
35 | 0.3822376009227222,
36 | 1.0,
37 | )
38 | assert cmap_fn(5) == (
39 | 0.9673202614379085,
40 | 0.968473663975394,
41 | 0.9656286043829296,
42 | 1.0,
43 | )
44 | assert cmap_fn(0) == (
45 | 0.9056516724336793,
46 | 0.5829296424452133,
47 | 0.7635524798154556,
48 | 1.0,
49 | )
50 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pandas as pd
3 | import pytest
4 |
5 | from plottable.cell import Cell, SubplotCell, TableCell, TextCell
6 | from plottable.plots import percentile_bars
7 | from plottable.table import Table
8 |
9 |
10 | @pytest.fixture
11 | def default_cell() -> Cell:
12 | return Cell(xy=(0, 0))
13 |
14 |
15 | @pytest.fixture
16 | def custom_cell() -> Cell:
17 | return Cell(xy=(1, 2), width=3, height=4)
18 |
19 |
20 | @pytest.fixture
21 | def table_cell() -> TableCell:
22 | return TableCell(
23 | xy=(1, 2), content="String Content", row_idx=1, col_idx=2, width=3, height=4
24 | )
25 |
26 |
27 | @pytest.fixture
28 | def text_cell() -> TextCell:
29 | return TextCell(
30 | xy=(1, 2), content="String Content", row_idx=1, col_idx=2, width=3, height=4
31 | )
32 |
33 |
34 | @pytest.fixture
35 | def subplot_cell() -> SubplotCell:
36 | return SubplotCell(
37 | xy=(0, 0),
38 | content=60,
39 | row_idx=0,
40 | col_idx=0,
41 | plot_fn=percentile_bars,
42 | width=2,
43 | height=1,
44 | )
45 |
46 |
47 | @pytest.fixture
48 | def df() -> pd.DataFrame:
49 | return pd.DataFrame(np.random.random((5, 5)), columns=["A", "B", "C", "D", "E"])
50 |
51 |
52 | @pytest.fixture
53 | def table(df) -> Table:
54 | return Table(df)
55 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | with open("README.md", "r") as f:
4 | readme = f.read()
5 |
6 | INSTALL_REQUIRES = [
7 | "matplotlib",
8 | "numpy",
9 | "pandas",
10 | "Pillow",
11 | ]
12 |
13 | EXTRAS_REQUIRE = {
14 | "development": [
15 | "pytest",
16 | "black",
17 | ]
18 | }
19 |
20 | CLASSIFIERS = [
21 | "Development Status :: 3 - Alpha",
22 | "Intended Audience :: Science/Research",
23 | "Programming Language :: Python :: 3.7",
24 | "Programming Language :: Python :: 3.8",
25 | "Programming Language :: Python :: 3.9",
26 | "Programming Language :: Python :: 3.10",
27 | "Programming Language :: Python :: 3.11",
28 | "Framework :: Matplotlib",
29 | "Topic :: Scientific/Engineering :: Visualization",
30 | ]
31 |
32 | setuptools.setup(
33 | name="plottable",
34 | version="0.1.5",
35 | author="znstrider",
36 | author_email="mindfulstrider@gmail.com",
37 | author_twitter="@danzn1",
38 | description="Beautifully customized tables with matplotlib",
39 | license="MIT",
40 | long_description_content_type="text/markdown",
41 | long_description=readme,
42 | url="https://github.com/znstrider/plottable",
43 | packages=setuptools.find_packages(),
44 | package_data={
45 | # If any package contains *.txt or *.rst files, include them:
46 | "": ["*.txt"]
47 | },
48 | include_package_data=True,
49 | install_requires=INSTALL_REQUIRES,
50 | extras_require=EXTRAS_REQUIRE,
51 | classifiers=CLASSIFIERS,
52 | python_requires=">=3.7",
53 | )
54 |
--------------------------------------------------------------------------------
/docs/plottable.rst:
--------------------------------------------------------------------------------
1 | plottable package
2 | =================
3 |
4 | Submodules
5 | ----------
6 |
7 | plottable.cell module
8 | ---------------------
9 |
10 | .. automodule:: plottable.cell
11 | :members:
12 | :undoc-members:
13 | :show-inheritance:
14 |
15 | plottable.cmap module
16 | ---------------------
17 |
18 | .. automodule:: plottable.cmap
19 | :members:
20 | :undoc-members:
21 | :show-inheritance:
22 |
23 | plottable.column_def module
24 | ---------------------------
25 |
26 | .. automodule:: plottable.column_def
27 | :members:
28 | :undoc-members:
29 | :show-inheritance:
30 |
31 | plottable.font module
32 | ---------------------
33 |
34 | .. automodule:: plottable.font
35 | :members:
36 | :undoc-members:
37 | :show-inheritance:
38 |
39 | plottable.formatters module
40 | ---------------------------
41 |
42 | .. automodule:: plottable.formatters
43 | :members:
44 | :undoc-members:
45 | :show-inheritance:
46 |
47 | plottable.helpers module
48 | ------------------------
49 |
50 | .. automodule:: plottable.helpers
51 | :members:
52 | :undoc-members:
53 | :show-inheritance:
54 |
55 | plottable.plots module
56 | ----------------------
57 |
58 | .. automodule:: plottable.plots
59 | :members:
60 | :undoc-members:
61 | :show-inheritance:
62 |
63 | plottable.table module
64 | ----------------------
65 |
66 | .. automodule:: plottable.table
67 | :members:
68 | :undoc-members:
69 | :show-inheritance:
70 |
71 | Module contents
72 | ---------------
73 |
74 | .. automodule:: plottable
75 | :members:
76 | :undoc-members:
77 | :show-inheritance:
78 |
--------------------------------------------------------------------------------
/contributing.rst:
--------------------------------------------------------------------------------
1 |
2 | Contributor Guide
3 | =================
4 |
5 | *Contributors are very welcome to this project.*
6 |
7 | You can contribute by giving feedback:
8 |
9 | At this stage, **usability** and **clarity** are a main priority.
10 | - If there is something that you think doesn't make sense, is too hard to do, worded badly or should work differently etc., don't hesitate to open an issue or get in touch.
11 |
12 | - If there is something you would like plottable to do, but it currently lacks the functionality, open an issue.
13 |
14 | - If you'd like to review the code and have suggestions on how to structure the project better, I'm all ears!
15 |
16 | **You are also very welcome to contribute to the package by creating a Pull Request.**
17 |
18 | If you are relatively new to contributing to projects or need a refresher on the process, you can read this great `Step-by-step guide to contribute on GitHub `_
19 |
20 | If you want to contribute to the project, best use an editable installation:
21 |
22 | .. code-block::
23 |
24 | git clone https://github.com/znstrider/plottable.git
25 | cd plottable
26 |
27 | pip install -e .
28 |
29 | Any contribution to documentation and examples is also very welcome.
30 |
31 |
32 | Code Formatting
33 | ---------------
34 |
35 | This project uses the black code formatter to ensure all code conforms to a specified format. It is necessary to format the code using black prior to committing. You can do this by manually running the `black` command to run black on all .py files, or with `black ` to run it on a specific file.
36 |
37 | Alternatively, you can setup black autoformatting within your IDE.
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | import os
7 | import sys
8 |
9 | sys.path.insert(0, os.path.abspath("../plottable"))
10 |
11 | # -- Project information -----------------------------------------------------
12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
13 |
14 | project = "plottable"
15 | copyright = "2022, znstrider"
16 | author = "znstrider"
17 | release = "0.1.5"
18 |
19 | # -- General configuration ---------------------------------------------------
20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
21 |
22 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "myst_nb"]
23 |
24 | autodoc_member_order = "bysource"
25 |
26 | templates_path = ["_templates"]
27 | exclude_patterns = [
28 | "_build",
29 | "Thumbs.db",
30 | ".DS_Store",
31 | "**.ipynb_checkpoints",
32 | "jupyter_execute",
33 | ]
34 |
35 |
36 | # -- Options for HTML output -------------------------------------------------
37 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
38 |
39 | # html_favicon = "_static/"
40 | # html_logo = "_static/"
41 | # html_static_path = ["_static"]
42 | html_theme = "sphinx_book_theme"
43 | html_theme_options = {
44 | "home_page_in_toc": True,
45 | "github_url": "https://github.com/znstrider/plottable",
46 | "repository_url": "https://github.com/znstrider/plottable",
47 | "repository_branch": "master",
48 | "path_to_docs": "docs",
49 | "use_repository_button": True,
50 | "use_edit_page_button": True,
51 | }
52 | html_title = "plottable"
53 |
--------------------------------------------------------------------------------
/tests/test_formatters.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from plottable.formatters import (
4 | apply_formatter,
5 | apply_string_formatter,
6 | decimal_to_percent,
7 | signed_integer,
8 | tickcross,
9 | )
10 |
11 |
12 | class TestDecimalToPercent:
13 | def test_decimal_to_percent_is_zero(self):
14 | assert decimal_to_percent(0) == "–"
15 |
16 | def test_decimal_to_percent_is_one(self):
17 | assert decimal_to_percent(1) == "✓"
18 |
19 | def test_decimal_to_percent_smaller_than_one(self):
20 | assert decimal_to_percent(0.005) == "<1%"
21 |
22 | def test_decimal_to_percent_bigger_than_point_ninetynine(self):
23 | assert decimal_to_percent(0.995) == ">99%"
24 |
25 | def test_decimal_to_percent_other_rounds_down(self):
26 | assert decimal_to_percent(0.554) == "55%"
27 |
28 | def test_decimal_to_percent_other_rounds_up(self):
29 | assert decimal_to_percent(0.555) == "56%"
30 |
31 |
32 | @pytest.mark.parametrize(
33 | "input,out", [(0, "✖"), (1, "✔"), (0.0, "✖"), (True, "✔"), (False, "✖")]
34 | )
35 | def test_tickcross(input, out):
36 | assert tickcross(input) == out
37 |
38 |
39 | def test_signed_integer():
40 | assert signed_integer(0) == "0"
41 | assert signed_integer(1) == "+1"
42 | assert signed_integer(-1) == "-1"
43 |
44 |
45 | @pytest.mark.parametrize(
46 | "content, fmt, output",
47 | [(1.23456, "{:.2f}", "1.23"), (0.5, "{:.0%}", "50%"), ("100", "${:}", "$100")],
48 | )
49 | def test_apply_string_formatter(content, fmt, output):
50 | assert apply_string_formatter(fmt, content) == output
51 |
52 |
53 | @pytest.mark.parametrize(
54 | "content, fmt, output",
55 | [
56 | (1.23456, "{:.2f}", "1.23"),
57 | (0.5, "{:.0%}", "50%"),
58 | ("100", "${:}", "$100"),
59 | (0, decimal_to_percent, "–"),
60 | (1.0, decimal_to_percent, "✓"),
61 | ],
62 | )
63 | def test_apply_formatter(content, fmt, output):
64 | assert apply_formatter(fmt, content) == output
65 |
--------------------------------------------------------------------------------
/plottable/cmap.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from typing import Callable
4 |
5 | import matplotlib
6 | import pandas as pd
7 | from matplotlib.colors import TwoSlopeNorm
8 |
9 |
10 | def normed_cmap(
11 | s: pd.Series, cmap: matplotlib.colors.LinearSegmentedColormap, num_stds: float = 2.5
12 | ) -> Callable:
13 | """Returns a normalized colormap function that takes a float as an argument and
14 | returns an rgba value.
15 |
16 | Args:
17 | s (pd.Series):
18 | a series of numeric values
19 | cmap (matplotlib.colors.LinearSegmentedColormap):
20 | matplotlib Colormap
21 | num_stds (float, optional):
22 | vmin and vmax are set to the median ± num_stds.
23 | Defaults to 2.5.
24 |
25 | Returns:
26 | Callable: Callable that takes a float as an argument and returns an rgba value.
27 | """
28 | _median = s.median()
29 | _std = s.std()
30 |
31 | vmin = _median - num_stds * _std
32 | vmax = _median + num_stds * _std
33 |
34 | norm = matplotlib.colors.Normalize(vmin=vmin, vmax=vmax)
35 | m = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
36 |
37 | return m.to_rgba
38 |
39 |
40 | def centered_cmap(
41 | s: pd.Series,
42 | cmap: matplotlib.colors.LinearSegmentedColormap,
43 | num_stds: float = 2.5,
44 | center: float | None = 0,
45 | ) -> Callable:
46 | """Returns a centered and normalized colormap function that takes a float as an argument and
47 | returns an rgba value.
48 |
49 | Args:
50 | s (pd.Series):
51 | a series of numeric values
52 | cmap (matplotlib.colors.LinearSegmentedColormap):
53 | matplotlib Colormap
54 | num_stds (float, optional):
55 | vmin and vmax are set to the median ± num_stds.
56 | Defaults to 2.5.
57 |
58 | Returns:
59 | Callable: Callable that takes a float as an argument and returns an rgba value.
60 | """
61 |
62 | if center is None:
63 | center = s.median()
64 |
65 | _std = s.std()
66 |
67 | vmin = center - num_stds * _std
68 | vmax = center + num_stds * _std
69 |
70 | norm = TwoSlopeNorm(vcenter=center, vmin=vmin, vmax=vmax)
71 | m = matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
72 |
73 | return m.to_rgba
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Beautiful tables in matplotlib.
2 |
3 | plottable is a Python library for plotting beautifully customized, presentation ready tables in Matplotlib.
4 |
5 | To learn about its functionality, have a look at the [documentation](https://plottable.readthedocs.io/en/latest/).
6 |
7 | ### Quickstart
8 |
9 | #### Installation
10 |
11 | ```
12 | pip install plottable
13 | ```
14 |
15 | #### A Basic Example
16 | ```python
17 | import matplotlib.pyplot as plt
18 | import numpy as np
19 | import pandas as pd
20 |
21 | from plottable import Table
22 |
23 | d = pd.DataFrame(np.random.random((5, 5)), columns=["A", "B", "C", "D", "E"]).round(2)
24 | fig, ax = plt.subplots(figsize=(6, 5))
25 | tab = Table(d)
26 |
27 | plt.show()
28 | ```
29 |
30 |
31 |
32 | ### Redoing the [Reactable 2019 Women's World Cup Predictions Visualization](https://glin.github.io/reactable/articles/womens-world-cup/womens-world-cup.html)
33 |
34 | You can find the [notebook here](https://github.com/znstrider/plottable/blob/master/docs/example_notebooks/wwc_example.ipynb)
35 |
36 |
37 |
38 | ### Styling A Table
39 |
40 | #### There are three main ways to customize a table:
41 |
42 | ##### 1) [By supplying keywords to the Table](https://plottable.readthedocs.io/en/latest/notebooks/table.html)
43 |
44 | ##### 2) [Providing a ColumnDefinition for each column you want to style](https://plottable.readthedocs.io/en/latest/notebooks/column_definition.html)
45 |
46 | ##### 3) [Accessing a tables rows or columns](https://plottable.readthedocs.io/en/latest/notebooks/rows_and_columns.html)
47 |
48 | ### Contributing
49 |
50 | ##### *Contributors are very welcome to this project.*
51 |
52 | Please take a look at the [Contributor Guide](contributing.rst)
53 |
54 |
55 | ### Credits
56 |
57 | plottable is built for the lack of good table packages in the python ecosystem.
58 | It draws inspiration from R packages [gt](https://github.com/rstudio/gt) and [reactable](https://github.com/glin/reactable), from blog posts about creating tables in matplotlib [Tim Bayer: How to create custom tables](https://matplotlib.org/matplotblog/posts/how-to-create-custom-tables/) and [Son of a corner: Beautiful Tables in Matplotlib, a Tutorial](https://www.sonofacorner.com/beautiful-tables/) and from matplotlibs own table module.
--------------------------------------------------------------------------------
/docs/notebooks/font.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Using Font ColorMaps"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "Similarly to coloring each ColumnCells background based on its value, you can color each cells text.\n",
15 | "\n",
16 | "You can do this by providing a `text_cmap` to a `ColumnDefinition`."
17 | ]
18 | },
19 | {
20 | "cell_type": "markdown",
21 | "metadata": {},
22 | "source": [
23 | "## Auto-Coloring of text\n",
24 | "\n",
25 | "You can auto-color text based on the cells backgrounds (or bbox-patches) colors.\n",
26 | "\n",
27 | "You do this after instantiating the table."
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "metadata": {},
33 | "source": [
34 | "```python\n",
35 | "autoset_fontcolors(\n",
36 | " self, fn: Callable = None, colnames: List[str] = None, **kwargs\n",
37 | " ) -> Table:\n",
38 | " \"\"\"Sets the fontcolor of each table cells text based on the facecolor of its rectangle patch.\n",
39 | "\n",
40 | " Args:\n",
41 | " fn (Callable, optional):\n",
42 | " Callable that takes the rectangle patches facecolor as\n",
43 | " rgba-value as argument.\n",
44 | " Defaults to plottable.font.contrasting_font_color if fn is None.\n",
45 | " kwargs are passed to fn.\n",
46 | "\n",
47 | " Returns:\n",
48 | " plottable.table.Table\n",
49 | " \"\"\"\n",
50 | "```"
51 | ]
52 | },
53 | {
54 | "cell_type": "markdown",
55 | "metadata": {},
56 | "source": [
57 | "```{seealso}\n",
58 | "You can find an example of autosetting fontcolors for specific columns in the [Women's World Cup Example](../example_notebooks/wwc_example.ipynb)\n",
59 | "```"
60 | ]
61 | },
62 | {
63 | "cell_type": "markdown",
64 | "metadata": {},
65 | "source": []
66 | }
67 | ],
68 | "metadata": {
69 | "kernelspec": {
70 | "display_name": "Python 3.10.5 ('env': venv)",
71 | "language": "python",
72 | "name": "python3"
73 | },
74 | "language_info": {
75 | "name": "python",
76 | "version": "3.10.5"
77 | },
78 | "orig_nbformat": 4,
79 | "vscode": {
80 | "interpreter": {
81 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
82 | }
83 | }
84 | },
85 | "nbformat": 4,
86 | "nbformat_minor": 2
87 | }
88 |
--------------------------------------------------------------------------------
/plottable/formatters.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from numbers import Number
4 | from typing import Callable
5 |
6 |
7 | def apply_string_formatter(fmt: str, val: str | Number) -> str:
8 | return fmt.format(val)
9 |
10 |
11 | def apply_formatter(formatter: str | Callable, content: str | Number) -> str:
12 | """Applies a formatter to the content.
13 |
14 | Args:
15 | formatter (str | Callable):
16 | the string formatter.
17 | Can either be a string format, ie "{:2f}" for 2 decimal places.
18 | Or a Callable that is applied to the content.
19 | content (str | Number):
20 | The content to format
21 |
22 | Raises:
23 | TypeError: when formatter is not of type str or Callable.
24 |
25 | Returns:
26 | str: a formatted string
27 | """
28 |
29 | if isinstance(formatter, str):
30 | return apply_string_formatter(formatter, content)
31 | elif isinstance(formatter, Callable):
32 | return formatter(content)
33 | else:
34 | raise TypeError("formatter needs to be either a `Callable` or a string.")
35 |
36 |
37 | def decimal_to_percent(val: float) -> str:
38 | """Formats Numbers to a string, replacing
39 | 0 with "–"
40 | 1 with "✓"
41 | values < 0.01 with "<1%" and
42 | values > 0.99 with ">99%"
43 |
44 | Args:
45 | val (float): numeric value to format
46 |
47 | Returns:
48 | str: formatted numeric value as string
49 | """
50 | if val == 0:
51 | return "–"
52 | elif val == 1:
53 | return "✓" # "\u2713"
54 | elif val < 0.01:
55 | return "<1%"
56 | elif val > 0.99:
57 | return ">99%"
58 | else:
59 | return f"{str(round(val * 100))}%"
60 |
61 |
62 | def tickcross(val: Number | bool) -> str:
63 | """formats a bool or (0, 1) value to a tick "✔" or cross "✖".
64 |
65 | Args:
66 | val (Number | bool): bool or (0, 1) value to format
67 |
68 | Returns:
69 | str: formatted value as string
70 | """
71 | if val:
72 | return "✔"
73 | else:
74 | return "✖"
75 |
76 |
77 | def signed_integer(val: int) -> str:
78 | """formats an integer to a string that includes the sign, ie. 1 to "+1".
79 |
80 | Args:
81 | val (int): integer value to format
82 |
83 | Returns:
84 | str: formatted value as string
85 | """
86 | if val <= 0:
87 | return str(val)
88 | else:
89 | return f"+{val}"
90 |
--------------------------------------------------------------------------------
/docs/notebooks/rows_and_columns.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Accessing Rows and Columns"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "Columns are accessed by the column name:\n",
15 | "```python\n",
16 | " table.columns[column_name]\n",
17 | "```\n",
18 | "\n",
19 | "Rows are ordered by their integer location (iloc) within the DataFrame and can be accessed by that index:\n",
20 | "```python\n",
21 | " table.rows[idx]\n",
22 | "```\n",
23 | "\n",
24 | "the column label row can be accessed by the `col_label_row` attribute:\n",
25 | "```python\n",
26 | " table.col_label_row\n",
27 | "```\n",
28 | "\n",
29 | "\n",
30 | "After creating a table, you can set cell (rectangle patch) properties and textproperties by accessing functions of the Sequence, ie:\n",
31 | "\n",
32 | "```python\n",
33 | " table.columns[column_name].set_facecolor(\"#f0f0f0\")\n",
34 | "\n",
35 | " table.rows[0].set_facecolor(\"#f0f0f0\")\n",
36 | "```\n",
37 | "\n",
38 | "Available functions are:\n",
39 | "```\n",
40 | " # rectangle patch setters\n",
41 | " set_alpha\n",
42 | " set_color\n",
43 | " set_edgecolor\n",
44 | " set_facecolor\n",
45 | " set_fill\n",
46 | " set_hatch\n",
47 | " set_linestyle\n",
48 | " set_linewidth\n",
49 | "\n",
50 | " # fontproperty setters\n",
51 | " set_fontcolor\n",
52 | " set_fontfamily\n",
53 | " set_fontsize\n",
54 | " set_ha\n",
55 | " set_ma\n",
56 | "```"
57 | ]
58 | },
59 | {
60 | "cell_type": "markdown",
61 | "metadata": {},
62 | "source": [
63 | "```{seealso}\n",
64 | "You can find an example of accessing rows and columns in the [Heatmap Example](../example_notebooks/heatmap.ipynb)\n",
65 | "```"
66 | ]
67 | },
68 | {
69 | "cell_type": "markdown",
70 | "metadata": {},
71 | "source": []
72 | }
73 | ],
74 | "metadata": {
75 | "kernelspec": {
76 | "display_name": "Python 3.10.5 ('env': venv)",
77 | "language": "python",
78 | "name": "python3"
79 | },
80 | "language_info": {
81 | "name": "python",
82 | "version": "3.10.5"
83 | },
84 | "orig_nbformat": 4,
85 | "vscode": {
86 | "interpreter": {
87 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
88 | }
89 | }
90 | },
91 | "nbformat": 4,
92 | "nbformat_minor": 2
93 | }
94 |
--------------------------------------------------------------------------------
/plottable/font.py:
--------------------------------------------------------------------------------
1 | import colorsys
2 | import math
3 | from typing import Tuple
4 |
5 |
6 | def contrasting_font_color(
7 | rgb: Tuple[float],
8 | light_color: str = "#ffffff",
9 | dark_color: str = "#000000",
10 | thresh=186,
11 | ) -> str:
12 | """Automatically chooses a light ("#ffffff") or dark ("#000000") fontcolor based on
13 | a (background) color.
14 |
15 | Args:
16 | rgb (Tuple[float]):
17 | rgb color tuple
18 | light_color (str, optional)
19 | the light color to use for dark backgrounds. Defaults to #ffffff
20 | dark_color (str, optional):
21 | the dark color to use for light backgrounds. Defaults to #000000
22 | thresh (int, optional):
23 | threshold to use. Defaults to 186.
24 |
25 | Returns:
26 | str: color hex code
27 | """
28 | r, g, b = rgb[:3]
29 | if (r * 0.299 * 256 + g * 0.587 * 256 + b * 0.114 * 256) > thresh:
30 | return dark_color
31 | else:
32 | return light_color
33 |
34 |
35 | def contrasting_font_color_w3c(
36 | rgb: Tuple[float],
37 | light_color: str = "#ffffff",
38 | dark_color: str = "#000000",
39 | adjust: float = -0.05,
40 | ) -> str:
41 | """Automatically chooses a light ("#ffffff") or dark ("#000000") fontcolor based on
42 | a (background) color.
43 |
44 | Args:
45 | rgb (Tuple[float]): rgb color tuple
46 | light_color (str, optional)
47 | the light color to use for dark backgrounds. Defaults to #ffffff
48 | dark_color (str, optional):
49 | the dark color to use for light backgrounds. Defaults to #000000
50 | adjust (float, optional): threshold to use. Defaults to -0.05.
51 |
52 | Returns:
53 | str: color hex code
54 | """
55 | _, l, s = colorsys.rgb_to_hls(*rgb[:3])
56 |
57 | if l > math.sqrt(1.05 * 0.05) + adjust:
58 | return dark_color
59 | else:
60 | return light_color
61 |
62 |
63 | def contrasting_font_color_(
64 | rgb: Tuple[float], light_color: str = "#ffffff", dark_color: str = "#000000"
65 | ) -> str:
66 | """Automatically chooses a light ("#ffffff") or dark ("#000000") fontcolor based on
67 | a (background) color.
68 |
69 | Args:
70 | rgb (Tuple[float]): rgb color tuple
71 | light_color (str, optional)
72 | the light color to use for dark backgrounds. Defaults to #ffffff
73 | dark_color (str, optional):
74 | the dark color to use for light backgrounds. Defaults to #000000
75 |
76 | Returns:
77 | str: color hex code
78 | """
79 | _, l, s = colorsys.rgb_to_hls(*rgb[:3])
80 |
81 | if l > math.sqrt(1.05 * 0.05):
82 | return dark_color
83 | else:
84 | return light_color
85 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. plottable documentation master file, created by
2 | sphinx-quickstart on Sat Oct 29 12:13:32 2022.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to plottable's documentation!
7 | =====================================
8 |
9 | Beautiful Tables in Matplotlib
10 | ------------------------------
11 |
12 | plottable is a Python library for plotting beautiful, presentation ready tables in Matplotlib.
13 |
14 |
15 | Quick start
16 | -----------
17 |
18 | ------------
19 | Installation
20 | ------------
21 |
22 | .. code-block::
23 |
24 | pip install plottable
25 |
26 | ---------------
27 | A Basic Example
28 | ---------------
29 |
30 | .. code-block::
31 |
32 | import matplotlib.pyplot as plt
33 | import numpy as np
34 | import pandas as pd
35 |
36 | from plottable import Table
37 |
38 | d = pd.DataFrame(np.random.random((10, 5)), columns=["A", "B", "C", "D", "E"]).round(2)
39 | fig, ax = plt.subplots(figsize=(5, 8))
40 | tab = Table(d)
41 |
42 | plt.show()
43 |
44 | .. image:: https://raw.githubusercontent.com/znstrider/plottable/master/docs/example_notebooks/images/basic_table.png
45 |
46 | -------------------------
47 | Women's World Cup Example
48 | -------------------------
49 |
50 | `You can access the WWC Example Notebook here `_
51 |
52 | .. image:: https://raw.githubusercontent.com/znstrider/plottable/master/docs/example_notebooks/images/wwc_table.png
53 |
54 | .. |br| raw:: html
55 |
56 |
57 |
58 | |br|
59 |
60 | Customizing a Table
61 | ===================
62 |
63 | 1) `By supplying keywords to the Table `_
64 |
65 | 2) `Providing a ColumnDefinition for each column you want to style `_
66 |
67 | 3) `Accessing a tables rows or columns `_
68 |
69 | Contributing
70 | ============
71 |
72 | *Contributors are very welcome to this project.*
73 |
74 | Please take a look at the `Contributor Guide `_
75 |
76 |
77 | Credits
78 | =======
79 |
80 | plottable is built for the lack of good table packages in the python ecosystem.
81 | It draws inspiration from R packages `gt `_ and `reactable `_, from blog posts about creating tables in matplotlib `Tim Bayer: How to create custom tables `_ and `Son of a corner: Beautiful Tables in Matplotlib, a Tutorial `_ and from matplotlibs own table module.
82 |
83 |
84 | .. toctree::
85 | :maxdepth: 2
86 | :caption: Documentation:
87 |
88 | notebooks/table.ipynb
89 | notebooks/column_definition.ipynb
90 | notebooks/plots.ipynb
91 | notebooks/cmap.ipynb
92 | notebooks/formatters.ipynb
93 | notebooks/font.ipynb
94 | notebooks/rows_and_columns.ipynb
95 |
96 |
97 | .. toctree::
98 | :maxdepth: 2
99 | :caption: Example Notebooks:
100 |
101 | example_notebooks/basic_example.ipynb
102 | example_notebooks/wwc_example.ipynb
103 | example_notebooks/bohndesliga_table.ipynb
104 | example_notebooks/plot_example.ipynb
105 | example_notebooks/heatmap.ipynb
106 |
107 |
108 | .. toctree::
109 | :maxdepth: 2
110 | :caption: Development:
111 |
112 | dev/contributing
113 | dev/changelog
114 |
115 |
116 | .. toctree::
117 | :maxdepth: 3
118 | :caption: Contents:
119 |
120 | modules
121 |
122 | Indices and tables
123 | ==================
124 |
125 | * :ref:`genindex`
126 | * :ref:`modindex`
127 | * :ref:`search`
128 |
--------------------------------------------------------------------------------
/docs/example_notebooks/data/wwc_forecasts.csv:
--------------------------------------------------------------------------------
1 | "forecast_timestamp","team","group","spi","global_o","global_d","sim_wins","sim_ties","sim_losses","sim_goal_diff","goals_scored","goals_against","group_1","group_2","group_3","group_4","make_round_of_16","make_quarters","make_semis","make_final","win_league","points"
2 | "2019-06-16 17:54:33 UTC","USA","F",98.32748,5.52561,0.58179,2.60922,0.22034,0.17044,16.95412,17.72834,0.77422,0.82956,0.17044,0,0,1,0.78079,0.47307,0.35076,0.23618,6
3 | "2019-06-16 17:54:33 UTC","France","A",96.29671,4.31375,0.52137,2.83658,0.12907,0.03435,6.97992,8.25201,1.27209,0.99483,0.00515,2e-05,0,1,0.78367,0.42052,0.30038,0.19428,6
4 | "2019-06-16 17:54:33 UTC","Germany","B",93.76549,3.96791,0.67818,2.85072,0.12325,0.02603,4.02534,4.23404,0.2087,0.98483,0.01517,0,0,1,0.8928,0.48039,0.2771,0.12256,6
5 | "2019-06-16 17:54:33 UTC","Canada","E",93.51599,3.67537,0.5698,2.3883,0.26705,0.34465,3.08796,4.16952,1.08156,0.3883,0.6117,0,0,1,0.59192,0.3614,0.20157,0.09031,6
6 | "2019-06-16 17:54:33 UTC","England","D",91.92311,3.5156,0.63717,2.45472,0.25098,0.2943,2.3368,4.4104,2.0736,0.7057,0.2943,0,0,1,0.6851,0.43053,0.16465,0.08003,6
7 | "2019-06-16 17:54:33 UTC","Netherlands","E",92.67529,3.8588,0.73539,2.34465,0.26705,0.3883,2.91204,5.08156,2.16952,0.6117,0.3883,0,0,1,0.59166,0.36983,0.18514,0.07576,6
8 | "2019-06-16 17:54:33 UTC","Australia","C",92.82054,4.21769,0.89761,1.88791,0.07186,1.04023,2.85093,7.60702,4.75609,0.12812,0.53618,0.33529,0.00041,0.99862,0.53811,0.26179,0.10214,0.04791,3
9 | "2019-06-16 17:54:33 UTC","Sweden","F",88.44996,2.98755,0.6263,2.17044,0.22034,0.60922,5.04588,7.77422,2.72834,0.17044,0.82956,0,0,1,0.4669,0.20267,0.10316,0.04006,6
10 | "2019-06-16 17:54:33 UTC","Japan","D",90.31291,3.78342,0.90887,1.2943,1.25098,0.45472,0.6632,3.0736,2.4104,0.2943,0.62818,0.07752,0,1,0.46314,0.26616,0.09314,0.03515,4
11 | "2019-06-16 17:54:33 UTC","Brazil","C",89.47398,3.589,0.87289,1.5792,0.20453,1.21627,2.85079,6.95614,4.10535,0.25916,0.22349,0.51735,0,0.9994,0.42681,0.17353,0.06693,0.02666,3
12 | "2019-06-16 17:54:33 UTC","Spain","B",86.53061,3.12364,0.82417,1.41905,0.29009,1.29086,1.24323,4.09506,2.85183,0.01082,0.69832,0.2907,0.00016,0.99367,0.30836,0.10742,0.04509,0.01759,3
13 | "2019-06-16 17:54:33 UTC","Norway","A",83.68324,2.96979,0.90942,1.48876,0.2523,1.25894,2.47825,5.42313,2.94488,0.00515,0.94029,0.05358,0.00098,0.99766,0.44246,0.17536,0.04192,0.01617,3
14 | "2019-06-16 17:54:33 UTC","China","B",82.6652,2.69201,0.79838,1.29086,0.29009,1.41905,-0.24323,1.85183,2.09506,0.00435,0.28651,0.70479,0.00435,0.92813,0.33744,0.10723,0.03367,0.00994,3
15 | "2019-06-16 17:54:33 UTC","Italy","C",76.137,2.97985,1.34724,2.21627,0.20453,0.5792,5.14921,8.10535,2.95614,0.61272,0.24033,0.14695,0,1,0.37442,0.08802,0.01963,0.00407,6
16 | "2019-06-16 17:54:33 UTC","New Zealand","E",77.62324,2.82453,1.15653,0.48334,0.25687,2.25979,-2.5438,1.38452,3.92832,0,0,0.48334,0.51666,0.40427,0.09699,0.03725,0.00614,0.00178,0
17 | "2019-06-16 17:54:33 UTC","Nigeria","A",71.65054,2.35033,1.11681,1.03435,0.12907,1.83658,-2.97992,2.27209,5.25201,2e-05,0.05077,0.90873,0.04048,0.49519,0.13261,0.02736,0.00609,0.00111,3
18 | "2019-06-16 17:54:33 UTC","Cameroon","E",65.8396,2.13554,1.21653,0.25979,0.25687,2.48334,-3.4562,1.92832,5.38452,0,0,0.51666,0.48334,0.2266,0.03779,0.01084,0.00124,0.00028,0
19 | "2019-06-16 17:54:33 UTC","South Korea","A",76.36337,2.70504,1.14036,0.25894,0.2523,2.48876,-6.47825,0.94488,7.42313,0,0.00379,0.03767,0.95854,0.017,0.00621,0.00209,0.00051,1e-04,0
20 | "2019-06-16 17:54:33 UTC","Scotland","D",53.97942,2.0491,1.69041,0.52512,0.24249,2.23239,-1.38437,3.5153,4.89967,0,0,0.52512,0.47488,0.51934,0.02828,0.00324,0.00068,5e-05,0
21 | "2019-06-16 17:54:33 UTC","Argentina","D",39.18661,1.50978,1.85584,0.23239,1.24249,1.52512,-1.61563,0.89967,2.5153,0,0.07752,0.39736,0.52512,0.24587,0.00546,0.00043,4e-05,1e-05,1
22 | "2019-06-16 17:54:33 UTC","Chile","F",46.53391,1.82545,1.83067,0.4457,0.21692,2.33738,-4.7484,1.69129,6.43969,0,0,0.66262,0.33738,0.14801,0.00781,0.00072,1e-05,0,0
23 | "2019-06-16 17:54:33 UTC","South Africa","B",56.69038,1.71768,1.2548,0.02603,0.12325,2.85072,-5.02534,1.2087,6.23404,0,0,0.00451,0.99549,0.0038,0.00072,1e-04,1e-05,0,0
24 | "2019-06-16 17:54:33 UTC","Thailand","F",40.69282,2.04038,2.37858,0.33738,0.21692,2.4457,-17.2516,2.43969,19.69129,0,0,0.33738,0.66262,0.02218,0.00054,5e-05,0,0,0
25 | "2019-06-16 17:54:33 UTC","Jamaica","C",53.51525,2.47074,2.11961,0.04023,0.07186,2.88791,-10.85093,0.75609,11.60702,0,0,0.00041,0.99959,0.00026,1e-05,0,0,0,0
26 |
--------------------------------------------------------------------------------
/plottable/column_def.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from dataclasses import asdict, dataclass, field
4 | from enum import Enum
5 | from typing import Any, Callable, Dict, List
6 |
7 | from matplotlib.colors import LinearSegmentedColormap
8 |
9 |
10 | class ColumnType(Enum):
11 | """The Column Type.
12 |
13 | Column Types are:
14 | STRING = "string"
15 | SUBPLOT = "subplot"
16 | """
17 |
18 | STRING = "string"
19 | SUBPLOT = "subplot"
20 |
21 |
22 | def _filter_none_values(d: Dict[str, Any]) -> Dict[str, Any]:
23 | """Filters out keys with None values from a dictionary.
24 |
25 | Args:
26 | d (Dict[str, Any]): Dictionary
27 |
28 | Returns:
29 | Dict[str, Any]: Dictionary without None valued values.
30 | """
31 | return {k: v for k, v in d.items() if v is not None}
32 |
33 |
34 | @dataclass
35 | class ColumnDefinition:
36 | """A Class defining attributes for a table column.
37 |
38 | Args:
39 | name: str:
40 | the column name
41 | title: str = None:
42 | the plotted title to override the column name
43 | width: float = 1:
44 | the width of the column as a factor of the default width
45 | textprops: Dict[str, Any] = field(default_factory=dict)
46 | textprops provided to each textcell
47 | formatter: Callable | str = None:
48 | Either A Callable or a builtin format string to format
49 | the appearance of the texts
50 | cmap: Callable | LinearSegmentedColormap = None:
51 | A Callable that returns a color based on the cells value.
52 | text_cmap: Callable | LinearSegmentedColormap = None:
53 | A Callable that returns a color based on the cells value.
54 | group: str = None:
55 | Each group will get a spanner column label above the column labels.
56 | plot_fn: Callable = None
57 | A Callable that will take the cells value as input and create a subplot
58 | on top of each cell and plot onto them.
59 | To pass additional arguments to it, use plot_kw (see below).
60 | plot_kw: Dict[str, Any] = field(default_factory=dict)
61 | Additional keywords provided to plot_fn.
62 | border: str | List = None:
63 | Plots a vertical borderline.
64 | can be either "left" / "l", "right" / "r" or "both"
65 |
66 | Formatting digits reference:
67 |
68 | source: https://www.pythoncheatsheet.org/cheatsheet/string-formatting
69 |
70 | number format output description
71 | ------ ------ ------ -----------
72 | 3.1415926 {:.2f} 3.14 Format float 2 decimal places
73 | 3.1415926 {:+.2f} +3.14 Format float 2 decimal places with sign
74 | -1 {:+.2f} -1.00 Format float 2 decimal places with sign
75 | 2.71828 {:.0f} 3 Format float with no decimal places
76 | 4 {:0>2d} 04 Pad number with zeros (left padding, width 2)
77 | 4 {:x<4d} 4xxx Pad number with x’s (right padding, width 4)
78 | 10 {:x<4d} 10xx Pad number with x’s (right padding, width 4)
79 | 1000000 {:,} 1,000,000 Number format with comma separator
80 | 0.35 {:.2%} 35.00% Format percentage
81 | 1000000000 {:.2e} 1.00e+09 Exponent notation
82 | 11 {:11d} 11 Right-aligned (default, width 10)
83 | 11 {:<11d} 11 Left-aligned (width 10)
84 | 11 {:^11d} 11 Center aligned (width 10)
85 |
86 | """
87 |
88 | name: str
89 | title: str = None
90 | width: float = 1
91 | textprops: Dict[str, Any] = field(default_factory=dict)
92 | formatter: Callable | str = None
93 | cmap: Callable | LinearSegmentedColormap = None
94 | text_cmap: Callable | LinearSegmentedColormap = None
95 | group: str = None
96 | plot_fn: Callable = None
97 | plot_kw: Dict[str, Any] = field(default_factory=dict)
98 | border: str | List = None
99 |
100 | def _asdict(self) -> Dict[str, Any]:
101 | """Returns the attributes as a dictionary.
102 |
103 | Returns:
104 | Dict[str, Any]: Dictionary of Column Attributes.
105 | """
106 | return asdict(self)
107 |
108 | def _as_non_none_dict(self) -> Dict[str, Any]:
109 | """Returns the attributes as a dictionary, filtering out
110 | keys with None values.
111 |
112 | Returns:
113 | Dict[str, Any]: Dictionary of Column Attributes.
114 | """
115 | return _filter_none_values(asdict(self))
116 |
117 |
118 | # abbreviated name to reduce writing
119 | ColDef = ColumnDefinition
120 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 |
5 | # C extensions
6 | *.so
7 |
8 | ## Core latex/pdflatex auxiliary files:
9 | *.aux
10 | *.lof
11 | *.log
12 | *.lot
13 | *.fls
14 | *.out
15 | *.toc
16 | *.fmt
17 | *.fot
18 | *.cb
19 | *.cb2
20 | .*.lb
21 |
22 | ## Intermediate documents:
23 | *.dvi
24 | *.xdv
25 | *-converted-to.*
26 |
27 | ## VSCODE
28 | .vscode
29 | *.code-workspace
30 |
31 | # these rules might exclude image files for figures etc.
32 | # *.ps
33 | # *.eps
34 |
35 | ## Build tool auxiliary files:
36 | *.fdb_latexmk
37 | *.synctex
38 | *.synctex(busy)
39 | *.synctex.gz
40 | *.synctex.gz(busy)
41 | *.pdfsync
42 |
43 | # Distribution / packaging
44 | .Python
45 | env/
46 | build/
47 | develop-eggs/
48 | dist/
49 | downloads/
50 | eggs/
51 | .eggs/
52 | lib/
53 | lib64/
54 | parts/
55 | sdist/
56 | var/
57 | *.egg-info/
58 | .installed.cfg
59 | *.egg
60 | .venv/
61 | .python-version
62 |
63 | # PyInstaller
64 | # Usually these files are written by a python script from a template
65 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
66 | *.manifest
67 | *.spec
68 |
69 | # Installer logs
70 | pip-log.txt
71 | pip-delete-this-directory.txt
72 |
73 | # Unit test / coverage reports
74 | htmlcov/
75 | .tox/
76 | .mypy_cache/
77 | .nox/
78 | .coverage
79 | .coverage.*
80 | .cache
81 | nosetests.xml
82 | coverage.xml
83 | *,cover
84 |
85 | # Translations
86 | *.mo
87 | *.pot
88 |
89 | # Django stuff:
90 | *.log
91 |
92 | # Sphinx documentation
93 | docs/_build/
94 | docs/modules/generated/
95 |
96 | # PyBuilder
97 | target/
98 |
99 | # DotEnv configuration
100 | .env
101 |
102 | # Database
103 | *.db
104 | *.rdb
105 |
106 | # Pycharm
107 | .idea
108 |
109 | # Jupyter NB Checkpoints
110 | .ipynb_checkpoints/
111 |
112 | # MyST Notebook Parser
113 | jupyter_execute/
114 |
115 | # exclude data from source control by default
116 | *.hdf
117 | *.clf
118 |
119 | # reports
120 |
121 | ## Core latex/pdflatex auxiliary files:
122 | *.aux
123 | *.lof
124 | *.log
125 | *.lot
126 | *.fls
127 | *.out
128 | *.toc
129 | *.fmt
130 | *.fot
131 | *.cb
132 | *.cb2
133 | .*.lb
134 |
135 | ## Intermediate documents:
136 | *.dvi
137 | *.xdv
138 | *-converted-to.*
139 | # these rules might exclude image files for figures etc.
140 | # *.ps
141 | # *.eps
142 | # *.pdf
143 |
144 | ## Generated if empty string is given at "Please type another file name for output:"
145 | .pdf
146 |
147 | ## Bibliography auxiliary files (bibtex/biblatex/biber):
148 | *.bbl
149 | *.bcf
150 | *.blg
151 | *-blx.aux
152 | *-blx.bib
153 | *.run.xml
154 |
155 | ## Build tool auxiliary files:
156 | *.fdb_latexmk
157 | *.synctex
158 | *.synctex(busy)
159 | *.synctex.gz
160 | *.synctex.gz(busy)
161 | *.pdfsync
162 |
163 | ## Build tool directories for auxiliary files
164 | # latexrun
165 | latex.out/
166 |
167 | ## Auxiliary and intermediate files from other packages:
168 | # algorithms
169 | *.alg
170 | *.loa
171 |
172 | # achemso
173 | acs-*.bib
174 |
175 | # amsthm
176 | *.thm
177 |
178 | # beamer
179 | *.nav
180 | *.pre
181 | *.snm
182 | *.vrb
183 |
184 | # changes
185 | *.soc
186 |
187 | # comment
188 | *.cut
189 |
190 | # cprotect
191 | *.cpt
192 |
193 | # elsarticle (documentclass of Elsevier journals)
194 | *.spl
195 |
196 | # endnotes
197 | *.ent
198 |
199 | # fixme
200 | *.lox
201 |
202 | # feynmf/feynmp
203 | *.mf
204 | *.mp
205 | *.t[1-9]
206 | *.t[1-9][0-9]
207 | *.tfm
208 |
209 | #(r)(e)ledmac/(r)(e)ledpar
210 | *.end
211 | *.?end
212 | *.[1-9]
213 | *.[1-9][0-9]
214 | *.[1-9][0-9][0-9]
215 | *.[1-9]R
216 | *.[1-9][0-9]R
217 | *.[1-9][0-9][0-9]R
218 | *.eledsec[1-9]
219 | *.eledsec[1-9]R
220 | *.eledsec[1-9][0-9]
221 | *.eledsec[1-9][0-9]R
222 | *.eledsec[1-9][0-9][0-9]
223 | *.eledsec[1-9][0-9][0-9]R
224 |
225 | # glossaries
226 | *.acn
227 | *.acr
228 | *.glg
229 | *.glo
230 | *.gls
231 | *.glsdefs
232 |
233 | # gnuplottex
234 | *-gnuplottex-*
235 |
236 | # gregoriotex
237 | *.gaux
238 | *.gtex
239 |
240 | # htlatex
241 | *.4ct
242 | *.4tc
243 | *.idv
244 | *.lg
245 | *.trc
246 | *.xref
247 |
248 | # hyperref
249 | *.brf
250 |
251 | # knitr
252 | *-concordance.tex
253 | # TODO Comment the next line if you want to keep your tikz graphics files
254 | *.tikz
255 | *-tikzDictionary
256 |
257 | # listings
258 | *.lol
259 |
260 | # luatexja-ruby
261 | *.ltjruby
262 |
263 | # makeidx
264 | *.idx
265 | *.ilg
266 | *.ind
267 | *.ist
268 |
269 | # minitoc
270 | *.maf
271 | *.mlf
272 | *.mlt
273 | *.mtc[0-9]*
274 | *.slf[0-9]*
275 | *.slt[0-9]*
276 | *.stc[0-9]*
277 |
278 | # minted
279 | _minted*
280 | *.pyg
281 |
282 | # morewrites
283 | *.mw
284 |
285 | # nomencl
286 | *.nlg
287 | *.nlo
288 | *.nls
289 |
290 | # pax
291 | *.pax
292 |
293 | # pdfpcnotes
294 | *.pdfpc
295 |
296 | # sagetex
297 | *.sagetex.sage
298 | *.sagetex.py
299 | *.sagetex.scmd
300 |
301 | # scrwfile
302 | *.wrt
303 |
304 | # sympy
305 | *.sout
306 | *.sympy
307 | sympy-plots-for-*.tex/
308 |
309 | # pdfcomment
310 | *.upa
311 | *.upb
312 |
313 | # pythontex
314 | *.pytxcode
315 | pythontex-files-*/
316 |
317 | # tcolorbox
318 | *.listing
319 |
320 | # thmtools
321 | *.loe
322 |
323 | # TikZ & PGF
324 | *.dpth
325 | *.md5
326 | *.auxlock
327 |
328 | # todonotes
329 | *.tdo
330 |
331 | # vhistory
332 | *.hst
333 | *.ver
334 |
335 | # easy-todo
336 | *.lod
337 |
338 | # xcolor
339 | *.xcp
340 |
341 | # xmpincl
342 | *.xmpi
343 |
344 | # xindy
345 | *.xdy
346 |
347 | # xypic precompiled matrices
348 | *.xyc
349 |
350 | # endfloat
351 | *.ttt
352 | *.fff
353 |
354 | # Latexian
355 | TSWLatexianTemp*
356 |
357 | ## Editors:
358 | # WinEdt
359 | *.bak
360 | *.sav
361 |
362 | # Texpad
363 | .texpadtmp
364 |
365 | # LyX
366 | *.lyx~
367 |
368 | # Kile
369 | *.backup
370 |
371 | # KBibTeX
372 | *~[0-9]*
373 |
374 | # auto folder when using emacs and auctex
375 | ./auto/*
376 | *.el
377 |
378 | # expex forward references with \gathertags
379 | *-tags.tex
380 |
381 | # standalone packages
382 | *.sta
--------------------------------------------------------------------------------
/docs/notebooks/table.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Styling a Table"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "By supplying keywords to the Table you can adjust its style:\n",
15 | "\n",
16 | "```python\n",
17 | "\"\"\"\n",
18 | "Args:\n",
19 | " df (pd.DataFrame):\n",
20 | " A pandas DataFrame with your table data\n",
21 | " ax (mpl.axes.Axes, optional):\n",
22 | " matplotlib axes. Defaults to None.\n",
23 | " index_col (str, optional):\n",
24 | " column to set as the DataFrame index. Defaults to None.\n",
25 | " columns (List[str], optional):\n",
26 | " columns to use. If None defaults to all columns.\n",
27 | " column_definitions (List[plottable.column_def.ColumnDefinition], optional):\n",
28 | " ColumnDefinitions for columns that should be styled. Defaults to None.\n",
29 | " textprops (Dict[str, Any], optional):\n",
30 | " textprops are passed to each TextCells matplotlib.pyplot.text. Defaults to {}.\n",
31 | " cell_kw (Dict[str, Any], optional):\n",
32 | " cell_kw are passed to to each cells matplotlib.patches.Rectangle patch.\n",
33 | " Defaults to {}.\n",
34 | " col_label_cell_kw (Dict[str, Any], optional):\n",
35 | " col_label_cell_kw are passed to to each ColumnLabels cells\n",
36 | " matplotlib.patches.Rectangle patch. Defaults to {}.\n",
37 | " col_label_divider (bool, optional):\n",
38 | " Whether to plot a divider line below the column labels. Defaults to True.\n",
39 | " col_label_divider_kw (Dict[str, Any], optional):\n",
40 | " col_label_divider_kw are passed to plt.plot. Defaults to {}.\n",
41 | " footer_divider (bool, optional):\n",
42 | " Whether to plot a divider line below the table. Defaults to False.\n",
43 | " footer_divider_kw (Dict[str, Any], optional):\n",
44 | " footer_divider_kw are passed to plt.plot. Defaults to {}.\n",
45 | " row_dividers (bool, optional):\n",
46 | " Whether to plot divider lines between rows. Defaults to True.\n",
47 | " row_divider_kw (Dict[str, Any], optional):\n",
48 | " row_divider_kw are passed to plt.plot. Defaults to {}.\n",
49 | " column_border_kw (Dict[str, Any], optional):\n",
50 | " column_border_kw are passed to plt.plot. Defaults to {}.\n",
51 | " even_row_color (str | Tuple, optional):\n",
52 | " facecolor of the even row cell's patches\n",
53 | " odd_row_color (str | Tuple, optional):\n",
54 | " facecolor of the even row cell's patches\n",
55 | "\"\"\"\n",
56 | "```"
57 | ]
58 | },
59 | {
60 | "cell_type": "markdown",
61 | "metadata": {},
62 | "source": [
63 | "## Column Definitions\n",
64 | "\n",
65 | "You can pass a list of `ColumnDefinition`'s to the column_definitions argument. \n",
66 | "\n",
67 | "```{seealso}\n",
68 | "How to use ColumnDefinition is documented in the [Using ColumnDefinition Notebook](./column_definition.ipynb)\n",
69 | "```"
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "metadata": {},
75 | "source": [
76 | "## Textprops\n",
77 | "\n",
78 | "With `textprops` such as `fontsize`, `fontname` and `color` you can adjust the appearance of table text globally for the whole table. They are passed to all cell's texts and only overridden by a `ColumnDefinition`'s textprops."
79 | ]
80 | },
81 | {
82 | "cell_type": "markdown",
83 | "metadata": {},
84 | "source": [
85 | "## Cell Keywords\n",
86 | "\n",
87 | "With `cell_kw` such as `facecolor`, `edgecolor` and `linewidth` you can adjust the appearance of table cells globally. They are passed to all cell's rectangle patches.\n",
88 | "\n",
89 | "With `col_label_cell_kw` you can similarly adjust the appearance of the column label cells."
90 | ]
91 | },
92 | {
93 | "cell_type": "markdown",
94 | "metadata": {},
95 | "source": [
96 | "## Row Divider Lines\n",
97 | "\n",
98 | "With boolean arguments `col_label_divider`, `footer_divider` and `row_dividers` you can plot divider lines between table rows.\n",
99 | "\n",
100 | "By passing arguments to their respective keyword arguments - `col_label_divider_kw`, `footer_divider_kw` and `row_dividers_kw` - you can further style the divider lines. Keywords are passed to `plt.plot`.\n",
101 | "\n",
102 | "## Column Border Keywords\n",
103 | "\n",
104 | "Similarly you can pass a `column_border_kw` dictionary to `Table` to style the vertical divider lines between `Column`'s. Keywords are passed to `plt.plot`.\n",
105 | "\n",
106 | "```{tip}\n",
107 | "Mind that they are only applied to `Column`'s that have a `border` attribute specified in their `ColumnDefinition`. \n",
108 | "You can find an example of plotting column borders in the [Women's World Cup Example](../example_notebooks/wwc_example.ipynb)\n",
109 | "```"
110 | ]
111 | },
112 | {
113 | "cell_type": "markdown",
114 | "metadata": {},
115 | "source": [
116 | "## Row Colors\n",
117 | "\n",
118 | "With the `even_row_color` and `odd_row_color` arguments you can color each respective rows cell colors with the color you passed.\n",
119 | "\n",
120 | "```{seealso}\n",
121 | "You can find an example in the [Basic Example Notebook](../example_notebooks/basic_example.ipynb)\n",
122 | "```"
123 | ]
124 | }
125 | ],
126 | "metadata": {
127 | "kernelspec": {
128 | "display_name": "Python 3.10.5 ('env': venv)",
129 | "language": "python",
130 | "name": "python3"
131 | },
132 | "language_info": {
133 | "name": "python",
134 | "version": "3.10.5"
135 | },
136 | "orig_nbformat": 4,
137 | "vscode": {
138 | "interpreter": {
139 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
140 | }
141 | }
142 | },
143 | "nbformat": 4,
144 | "nbformat_minor": 2
145 | }
146 |
--------------------------------------------------------------------------------
/tests/test_cell.py:
--------------------------------------------------------------------------------
1 | import matplotlib
2 |
3 | from plottable import __version__
4 | from plottable.cell import Column, Row, SubplotCell, TextCell, create_cell
5 | from plottable.column_def import ColumnType
6 | from plottable.plots import percentile_bars
7 |
8 |
9 | def test_version():
10 | assert __version__ == "0.1.5"
11 |
12 |
13 | class TestDefaultCell:
14 | def test_default_cell_xy(self, default_cell):
15 | assert default_cell.xy == (0, 0)
16 |
17 | def test_default_cell_width(self, default_cell):
18 | assert default_cell.width == 1
19 |
20 | def test_default_cell_height(self, default_cell):
21 | assert default_cell.height == 1
22 |
23 | def test_default_cell_x(self, default_cell):
24 | assert default_cell.x == 0
25 |
26 | def test_default_cell_y(self, default_cell):
27 | assert default_cell.y == 0
28 |
29 |
30 | class TestCustomCell:
31 | def test_custom_cell_xy(self, custom_cell):
32 | assert custom_cell.xy == (1, 2)
33 |
34 | def test_custom_cell_width(self, custom_cell):
35 | assert custom_cell.width == 3
36 |
37 | def test_custom_cell_height(self, custom_cell):
38 | assert custom_cell.height == 4
39 |
40 | def test_custom_cell_x(self, custom_cell):
41 | assert custom_cell.x == 1
42 |
43 | def test_custom_cell_y(self, custom_cell):
44 | assert custom_cell.y == 2
45 |
46 |
47 | class TestTableCell:
48 | def test_table_cell_xy(self, table_cell):
49 | assert table_cell.xy == (1, 2)
50 |
51 | def test_table_cell_content(self, table_cell):
52 | assert table_cell.content == "String Content"
53 |
54 | def test_table_cell_row_idx(self, table_cell):
55 | assert table_cell.row_idx == 1
56 |
57 | def test_table_cell_col_idx(self, table_cell):
58 | assert table_cell.col_idx == 2
59 |
60 | def test_table_cell_width(self, table_cell):
61 | assert table_cell.width == 3
62 |
63 | def test_table_cell_height(self, table_cell):
64 | assert table_cell.height == 4
65 |
66 | def test_table_cell_rect_kw(self, table_cell):
67 | assert table_cell.rect_kw == {
68 | "linewidth": 0.0,
69 | "edgecolor": table_cell.ax.get_facecolor(),
70 | "facecolor": table_cell.ax.get_facecolor(),
71 | "width": 3,
72 | "height": 4,
73 | }
74 |
75 | def test_rectangle_patch(self, table_cell):
76 | _rect = matplotlib.patches.Rectangle(table_cell.xy, **table_cell.rect_kw)
77 | assert table_cell.rectangle_patch.xy == _rect.xy
78 | assert table_cell.rectangle_patch.get_width() == _rect.get_width()
79 | assert table_cell.rectangle_patch.get_height() == _rect.get_height()
80 |
81 |
82 | class TestTextCell:
83 | def test_table_cell_textprops(self, text_cell):
84 | assert text_cell.textprops == {"ha": "right", "va": "center"}
85 |
86 | def test_table_padding(self, text_cell):
87 | assert text_cell.padding == 0.1
88 |
89 | def test_table_default_ha(self, text_cell):
90 | assert text_cell.ha == "right"
91 |
92 | def test_set_text(self, text_cell):
93 | _text = matplotlib.text.Text(
94 | text_cell.x + text_cell.width - text_cell.padding * text_cell.width,
95 | text_cell.y + text_cell.height / 2,
96 | "String Content",
97 | )
98 |
99 | text_cell.draw()
100 | assert text_cell.text.get_text() == _text.get_text()
101 | assert text_cell.text.get_position() == _text.get_position()
102 |
103 | def test_set_text_ha_is_left(self, text_cell):
104 | text_cell.ha = "left"
105 | text_cell.draw()
106 |
107 | x, _ = text_cell.text.get_position()
108 | assert x == text_cell.x + text_cell.padding * text_cell.width
109 |
110 | def test_set_text_ha_is_center(self, text_cell):
111 | text_cell.ha = "center"
112 | text_cell.draw()
113 |
114 | x, _ = text_cell.text.get_position()
115 | assert x == text_cell.x + text_cell.width / 2
116 |
117 |
118 | class TestSubplotCell:
119 | def test_subplot_cell_plot_fn(self, subplot_cell):
120 | assert subplot_cell._plot_fn == percentile_bars
121 |
122 | def test_subplot_cell_plot_kw(self, subplot_cell):
123 | assert subplot_cell._plot_kw == {}
124 |
125 | def test_subplot_cell_make_axes_inset(self, subplot_cell):
126 | subplot_cell.make_axes_inset()
127 | assert isinstance(subplot_cell.axes_inset, matplotlib.axes.Axes)
128 |
129 | def test_get_rectangle_bounds(self, subplot_cell):
130 | # TODO
131 | assert True
132 |
133 | def test_subplot_cell_plot(self, subplot_cell):
134 | subplot_cell.make_axes_inset()
135 | subplot_cell.plot()
136 | assert len(subplot_cell.axes_inset.patches) > 1
137 |
138 |
139 | def test_create_cell_type_is_stringcell():
140 | assert (
141 | type(
142 | create_cell(
143 | column_type=ColumnType.STRING,
144 | xy=(0, 0),
145 | content="A",
146 | row_idx=0,
147 | col_idx=0,
148 | )
149 | )
150 | == TextCell
151 | )
152 |
153 |
154 | def test_create_cell_type_is_subplotcell():
155 | assert (
156 | type(
157 | create_cell(
158 | column_type=ColumnType.SUBPLOT,
159 | xy=(0, 0),
160 | content="A",
161 | row_idx=0,
162 | col_idx=0,
163 | plot_fn=percentile_bars,
164 | )
165 | )
166 | == SubplotCell
167 | )
168 |
169 |
170 | def test_row_of_subplot_cells_does_not_raise_on_set_fontproperties():
171 | cells = [
172 | create_cell(
173 | column_type=ColumnType.SUBPLOT,
174 | xy=(0, i),
175 | content=i,
176 | row_idx=i,
177 | col_idx=0,
178 | plot_fn=percentile_bars,
179 | )
180 | for i in range(5)
181 | ]
182 |
183 | row = Column(cells, index=0)
184 |
185 | row.set_fontcolor("k")
186 | row.set_fontfamily("Arial")
187 | row.set_fontsize(10)
188 | row.set_ha("right")
189 | row.set_ma("right")
190 |
191 |
192 | def test_row_height():
193 | cells = [
194 | create_cell(
195 | column_type=ColumnType.STRING,
196 | xy=(i, 0),
197 | content=i,
198 | row_idx=0,
199 | col_idx=i,
200 | height=2,
201 | )
202 | for i in range(5)
203 | ]
204 | row = Row(cells, index=0)
205 | assert row.height == 2
206 |
207 |
208 | def test_row_x():
209 | cells = [
210 | create_cell(
211 | column_type=ColumnType.STRING,
212 | xy=(i + 2, 0),
213 | content=i,
214 | row_idx=0,
215 | col_idx=i,
216 | )
217 | for i in range(5)
218 | ]
219 | row = Row(cells, index=0)
220 | assert row.x == 2
221 |
222 |
223 | def test_row_y():
224 | cells = [
225 | create_cell(
226 | column_type=ColumnType.STRING,
227 | xy=(i, 2),
228 | content=i,
229 | row_idx=0,
230 | col_idx=i,
231 | )
232 | for i in range(5)
233 | ]
234 | row = Row(cells, index=0)
235 | assert row.y == 2
236 |
237 |
238 | def test_column_width():
239 | cells = [
240 | create_cell(
241 | column_type=ColumnType.STRING,
242 | xy=(0, i),
243 | content=i,
244 | row_idx=i,
245 | col_idx=0,
246 | width=2,
247 | )
248 | for i in range(5)
249 | ]
250 | col = Column(cells, index=0)
251 | assert col.width == 2
252 |
253 |
254 | def test_column_x():
255 | cells = [
256 | create_cell(
257 | column_type=ColumnType.STRING,
258 | xy=(2, i),
259 | content=i,
260 | row_idx=i,
261 | col_idx=0,
262 | )
263 | for i in range(5)
264 | ]
265 | col = Column(cells, index=0)
266 | assert col.x == 2
267 |
268 |
269 | def test_column_y():
270 | cells = [
271 | create_cell(
272 | column_type=ColumnType.STRING,
273 | xy=(0, i + 2),
274 | content=i,
275 | row_idx=i,
276 | col_idx=0,
277 | )
278 | for i in range(5)
279 | ]
280 | col = Column(cells, index=0)
281 | assert col.y == 2
282 |
--------------------------------------------------------------------------------
/plottable/plots.py:
--------------------------------------------------------------------------------
1 | from statistics import mean
2 | from typing import Any, Callable, Dict, List, Tuple
3 |
4 | import matplotlib
5 | import matplotlib.pyplot as plt
6 | import numpy as np
7 | from matplotlib.patches import BoxStyle, Circle, FancyBboxPatch, Rectangle, Wedge
8 | from PIL import Image
9 |
10 | from .formatters import apply_formatter
11 |
12 |
13 | def image(ax: matplotlib.axes.Axes, path: str) -> matplotlib.image.AxesImage:
14 | """Plots an image on the axes.
15 |
16 | Args:
17 | ax (matplotlib.axes.Axes): Axes
18 | path (str): path to image file
19 |
20 | Returns:
21 | matplotlib.image.AxesImage
22 | """
23 | img = plt.imread(path)
24 | im = ax.imshow(img)
25 | im.set_clip_on(False)
26 | ax.axis("off")
27 | return im
28 |
29 |
30 | def monochrome_image(ax: matplotlib.axes.Axes, path: str) -> matplotlib.image.AxesImage:
31 | """Plots a monochrome image on the axes.
32 |
33 | Args:
34 | ax (matplotlib.axes.Axes): Axes
35 | path (str): path to image file
36 |
37 | Returns:
38 | matplotlib.image.AxesImage
39 | """
40 | img = Image.open(path).convert("LA")
41 | im = ax.imshow(img)
42 | im.set_clip_on(False)
43 | ax.axis("off")
44 | return im
45 |
46 |
47 | def circled_image(
48 | ax: matplotlib.axes.Axes, path: str, **circle_kwargs
49 | ) -> matplotlib.image.AxesImage:
50 | """Plots an image cropped to a circle on the axes.
51 | The cropping radius is the minimum of (width, height) of the image.
52 |
53 | Args:
54 | ax (matplotlib.axes.Axes): Axes
55 | path (str): path to image file
56 |
57 | Returns:
58 | matplotlib.image.AxesImage
59 | """
60 | circle_kw = {
61 | "visible": False,
62 | "facecolor": "None",
63 | "linewidth": 1,
64 | "clip_on": False,
65 | }
66 |
67 | circle_kw.update(circle_kwargs)
68 |
69 | img = plt.imread(path)
70 | im = ax.imshow(img)
71 | ax.axis("off")
72 |
73 | radius = min(max(ax.get_xlim()), max(ax.get_ylim())) / 2
74 | center = (mean(ax.get_xlim()), mean(ax.get_ylim()))
75 | circle = Circle(center, radius, transform=ax.transData, **circle_kw)
76 | ax.add_patch(circle)
77 |
78 | im.set_clip_path(circle)
79 |
80 | ax.set_aspect("equal")
81 |
82 | return im
83 |
84 |
85 | def bar(
86 | ax: matplotlib.axes.Axes,
87 | val: float,
88 | xlim: Tuple[float, float] = (0, 1),
89 | cmap: matplotlib.colors.Colormap = None,
90 | plot_bg_bar: bool = False,
91 | annotate: bool = False,
92 | textprops: Dict[str, Any] = {},
93 | formatter: Callable = None,
94 | **kwargs,
95 | ) -> matplotlib.container.BarContainer:
96 | """Plots a bar on the axes.
97 |
98 | Args:
99 | ax (matplotlib.axes.Axes):
100 | Axes
101 | val (float):
102 | value
103 | xlim (Tuple[float, float], optional):
104 | data limit for the x-axis. Defaults to (0, 1).
105 | cmap (matplotlib.colors.Colormap, optional):
106 | colormap. Defaults to None.
107 | plot_bg_bar (bool, optional):
108 | whether to plot a background bar. Defaults to False.
109 | annotate (bool, optional):
110 | whether to annotate the value. Defaults to False.
111 | textprops (Dict[str, Any], optional):
112 | textprops passed to ax.text. Defaults to {}.
113 | formatter (Callable, optional):
114 | a string formatter.
115 | Can either be a string format, ie "{:2f}" for 2 decimal places.
116 | Or a Callable that is applied to the value. Defaults to None.
117 |
118 | Returns:
119 | matplotlib.container.BarContainer
120 | """
121 |
122 | if "color" in kwargs:
123 | color = kwargs.pop("color")
124 | else:
125 | color = "C1"
126 |
127 | if cmap is not None:
128 | color = cmap(float(val))
129 |
130 | if plot_bg_bar:
131 | ax.barh(
132 | 0.5,
133 | xlim[1],
134 | left=xlim[0],
135 | fc="None",
136 | ec=plt.rcParams["text.color"],
137 | **kwargs,
138 | zorder=0.1,
139 | )
140 |
141 | bar = ax.barh(0.5, val, fc=color, ec="None", **kwargs, zorder=0.05)
142 |
143 | if annotate:
144 | if val < 0.5 * xlim[1]:
145 | ha = "left"
146 | x = val + 0.025 * abs(xlim[1] - xlim[0])
147 | else:
148 | ha = "right"
149 | x = val - 0.025 * abs(xlim[1] - xlim[0])
150 |
151 | if formatter is not None:
152 | text = apply_formatter(formatter, val)
153 | else:
154 | text = val
155 |
156 | ax.text(x, 0.5, text, ha=ha, va="center", **textprops, zorder=0.3)
157 |
158 | ax.axis("off")
159 | ax.set_xlim(
160 | xlim[0] - 0.025 * abs(xlim[1] - xlim[0]),
161 | xlim[1] + 0.025 * abs(xlim[1] - xlim[0]),
162 | )
163 | ax.set_ylim(0, 1)
164 |
165 | return bar
166 |
167 |
168 | def percentile_bars(
169 | ax: matplotlib.axes.Axes,
170 | val: float,
171 | color: str = None,
172 | background_color: str = None,
173 | cmap: matplotlib.colors.Colormap = None,
174 | is_pct=False,
175 | rect_kw: Dict[str, Any] = {},
176 | ) -> List[matplotlib.patches.FancyBboxPatch]:
177 | """Plots percentile bars on the axes.
178 |
179 | Args:
180 | ax (matplotlib.axes.Axes): Axes
181 | val (float): value
182 | color (str, optional):
183 | color of the bars. Defaults to None.
184 | background_color (str, optional):
185 | background_color of the bars if value is not reached. Defaults to None.
186 | cmap (matplotlib.colors.Colormap, optional):
187 | Colormap. Defaults to None.
188 | is_pct (bool, optional):
189 | whether the value is given not as a decimal, but as a value between 0 and 100.
190 | Defaults to False.
191 | rect_kw (Dict[str, Any], optional):
192 | rect keywords passed to matplotlib.patches.FancyBboxPatch. Defaults to {}.
193 |
194 | Returns:
195 | List[matplotlib.patches.FancyBboxPatch]
196 | """
197 |
198 | _rect_kw = {
199 | "linewidth": 2.5,
200 | "boxstyle": BoxStyle("Round", pad=0, rounding_size=0.05),
201 | }
202 |
203 | _rect_kw.update(rect_kw)
204 |
205 | edgecolor = ax.get_facecolor()
206 |
207 | if background_color is None:
208 | background_color = ax.get_facecolor()
209 |
210 | if is_pct is False:
211 | val = val / 100
212 |
213 | if cmap is not None:
214 | color = cmap(val)
215 | elif color is None:
216 | color = "C1"
217 |
218 | value_patch = Rectangle(
219 | xy=(0, 0), width=val, height=1, color="w", alpha=0.75, transform=ax.transData
220 | )
221 |
222 | bg_rects = []
223 | rects = []
224 |
225 | for x in np.linspace(0, 0.9, 10):
226 |
227 | bg_rect = FancyBboxPatch(
228 | xy=(x, 0),
229 | width=0.1,
230 | height=1,
231 | facecolor=background_color,
232 | edgecolor=edgecolor,
233 | alpha=1,
234 | **_rect_kw,
235 | zorder=0.1,
236 | )
237 | bg_rects.append(bg_rect)
238 | ax.add_patch(bg_rect)
239 |
240 | rect = FancyBboxPatch(
241 | xy=(x, 0),
242 | width=0.1,
243 | height=1,
244 | facecolor=color,
245 | edgecolor=edgecolor,
246 | **_rect_kw,
247 | zorder=0.2,
248 | )
249 | rects.append(rect)
250 | ax.add_patch(rect)
251 | rect.set_clip_path(value_patch)
252 |
253 | ax.axis("off")
254 |
255 | return rects
256 |
257 |
258 | def percentile_stars(
259 | ax: matplotlib.axes.Axes,
260 | val: float,
261 | n_stars: int = 5,
262 | color: str = "orange",
263 | background_color: str = None,
264 | is_pct: bool = False,
265 | padding: float = 0.1,
266 | **kwargs,
267 | ) -> matplotlib.collections.PathCollection:
268 | """Plots x out of 5 percentile stars on the axes.
269 |
270 | Args:
271 | ax (matplotlib.axes.Axes): Axes
272 | val (float): value
273 | n_stars (int, optional):
274 | number of maximum stars. Defaults to 5.
275 | color (str, optional):
276 | color of the stars. Defaults to "orange".
277 | background_color (str, optional):
278 | background_color of the stars if value is not reached. Defaults to None.
279 | is_pct (bool, optional):
280 | whether the value is given not as a decimal, but as a value between 0 and 100.
281 | Defaults to False.
282 | padding (float, optional):
283 | horizontal and vertical padding from stars to axes border. Defaults to 0.1.
284 |
285 | Returns:
286 | matplotlib.collections.PathCollection
287 | """
288 | if background_color is None:
289 | background_color = ax.get_facecolor()
290 |
291 | if is_pct is False:
292 | val = val / 100
293 |
294 | bounds = np.linspace(0, 1, n_stars + 1)
295 | xs = (bounds[:-1] + bounds[1:]) / 2
296 | ys = n_stars * [0.5]
297 |
298 | value_patch = Rectangle(
299 | xy=(0, 0), width=val, height=1, color="w", alpha=0.75, transform=ax.transData
300 | )
301 |
302 | if "s" not in kwargs:
303 | kwargs["s"] = 200
304 |
305 | ax.scatter(
306 | x=xs, y=ys, color=background_color, marker="*", zorder=0.2, alpha=1, **kwargs
307 | )
308 | sc = ax.scatter(x=xs, y=ys, color=color, marker="*", zorder=0.2, **kwargs)
309 | sc.set_clip_path(value_patch)
310 |
311 | ax.set_ylim(0 - padding, 1 + padding)
312 | ax.set_xlim(0 - padding, 1 + padding)
313 | ax.axis("off")
314 |
315 | return sc
316 |
317 |
318 | def progress_donut(
319 | ax: matplotlib.axes.Axes,
320 | val: float,
321 | radius: float = 0.45,
322 | color: str = None,
323 | background_color: str = None,
324 | width: float = 0.05,
325 | is_pct: bool = False,
326 | textprops: Dict[str, Any] = {},
327 | formatter: Callable = None,
328 | **kwargs,
329 | ) -> List[matplotlib.patches.Wedge]:
330 | """Plots a Progress Donut on the axes.
331 |
332 | Args:
333 | ax (matplotlib.axes.Axes): Axes
334 | val (float): value
335 | radius (float, optional):
336 | radius of the progress donut. Defaults to 0.45.
337 | color (str, optional):
338 | color of the progress donut. Defaults to None.
339 | background_color (str, optional):
340 | background_color of the progress donut where the value is not reached. Defaults to None.
341 | width (float, optional):
342 | width of the donut wedge. Defaults to 0.05.
343 | is_pct (bool, optional):
344 | whether the value is given not as a decimal, but as a value between 0 and 100.
345 | Defaults to False.
346 | textprops (Dict[str, Any], optional):
347 | textprops passed to ax.text. Defaults to {}.
348 | formatter (Callable, optional):
349 | a string formatter.
350 | Can either be a string format, ie "{:2f}" for 2 decimal places.
351 | Or a Callable that is applied to the value. Defaults to None.
352 |
353 | Returns:
354 | List[matplotlib.patches.Wedge]
355 | """
356 | wedges = []
357 |
358 | if color is None:
359 | color = "C1"
360 |
361 | if is_pct is False:
362 | val = val / 100
363 |
364 | if background_color is not None:
365 | bg_wedge = Wedge(
366 | (0.5, 0.5),
367 | radius,
368 | 90,
369 | 360 + 90,
370 | width=width,
371 | color=background_color,
372 | **kwargs,
373 | )
374 | ax.add_patch(bg_wedge)
375 |
376 | wedges.append(bg_wedge)
377 |
378 | wedge = Wedge(
379 | (0.5, 0.5), radius, 90, 90 + val * 360, width=width, color=color, **kwargs
380 | )
381 | wedges.append(wedge)
382 |
383 | ax.add_patch(wedge)
384 | ax.set_aspect("equal")
385 |
386 | if formatter is not None:
387 | text = apply_formatter(formatter, val)
388 | else:
389 | text = val
390 |
391 | ax.text(0.5, 0.5, text, ha="center", va="center", **textprops)
392 | ax.axis("off")
393 |
394 | return wedges
395 |
--------------------------------------------------------------------------------
/tests/test_table.py:
--------------------------------------------------------------------------------
1 | import matplotlib as mpl
2 | import matplotlib.pyplot as plt
3 | import pytest
4 |
5 | from plottable import ColDef, ColumnDefinition, Table, formatters, plots
6 | from plottable.cell import SubplotCell
7 |
8 |
9 | def test_table_df(df):
10 | tab = Table(df)
11 | assert tab.df.equals(df)
12 |
13 |
14 | def test_table_df_index_name(table):
15 | assert table.df.index.name == "index"
16 |
17 |
18 | def test_index_col(df):
19 | tab = Table(df, index_col="A")
20 | assert tab.index_col == "A"
21 | assert tab.df.shape[1] == 4
22 |
23 |
24 | def test_columns(df):
25 | tab = Table(df, columns=["A", "B"])
26 | assert tab.df.columns.to_list() == ["A", "B"]
27 |
28 |
29 | def test_cell_kw(df):
30 | tab = Table(df, cell_kw={"linewidth": 2})
31 | assert "linewidth" in tab.cell_kw
32 | assert tab.cell_kw["linewidth"] == 2
33 |
34 | for cell in tab.cells.values():
35 | assert cell.rectangle_patch.get_linewidth() == 2
36 |
37 |
38 | def test_col_label_row(table, df):
39 | for col_idx, cell in enumerate(table.col_label_row.cells):
40 | assert cell.y == cell.row_idx
41 | assert cell.col_idx == col_idx
42 |
43 |
44 | def test_table_axes(table):
45 | assert table.ax == plt.gca()
46 |
47 |
48 | def test_table_figure(table):
49 | assert table.figure == plt.gca().figure
50 |
51 |
52 | def test_table_df_n_rows(table, df):
53 | assert table.n_rows == len(df)
54 |
55 |
56 | def test_table_df_n_cols(table, df):
57 | assert table.n_cols == len(df.columns)
58 |
59 |
60 | def test_table_column_names(table, df):
61 | assert table.column_names == ["index"] + list(df.columns)
62 |
63 |
64 | def test_table_column_definitions(table):
65 | for col in table.column_names:
66 | assert (
67 | table.column_definitions[col]
68 | == ColumnDefinition(name=col)._as_non_none_dict()
69 | )
70 |
71 |
72 | def test_get_column_titles(table):
73 | assert table._get_column_titles()[0] == table.df.index.name
74 | assert table._get_column_titles()[1:] == ["A", "B", "C", "D", "E"]
75 |
76 |
77 | def test_get_col_groups_are_empty(table):
78 | assert table._get_col_groups() == set()
79 |
80 |
81 | def test_get_col_groups(df):
82 | tab = Table(
83 | df,
84 | column_definitions=[
85 | ColumnDefinition(name, group="group1") for name in ["A", "B"]
86 | ],
87 | )
88 | assert tab._get_col_groups() == set(["group1"])
89 |
90 |
91 | def test_get_col_groups_multiple_groups(df):
92 | tab = Table(
93 | df,
94 | column_definitions=[ColumnDefinition(name, group=name) for name in df.columns],
95 | )
96 | assert tab._get_col_groups() == set(df.columns)
97 |
98 |
99 | def test_get_non_group_colnames(df):
100 | tab = Table(
101 | df,
102 | column_definitions=[
103 | ColumnDefinition(name, group="group1") for name in ["A", "B"]
104 | ],
105 | )
106 | assert tab._get_non_group_colnames() == set(["index", "C", "D", "E"])
107 |
108 |
109 | def test_get_non_group_colnames_is_empty_set(df):
110 | tab = Table(
111 | df,
112 | column_definitions=[
113 | ColumnDefinition(name, group=name) for name in list(df.columns) + ["index"]
114 | ],
115 | )
116 | assert tab._get_non_group_colnames() == set()
117 |
118 |
119 | def test_table_column_name_to_idx(table):
120 | assert table.column_name_to_idx == {
121 | "index": 0,
122 | "A": 1,
123 | "B": 2,
124 | "C": 3,
125 | "D": 4,
126 | "E": 5,
127 | }
128 |
129 |
130 | def test_table_cell_kw_is_empty_dict(table):
131 | assert table.cell_kw == {}
132 |
133 |
134 | def test_table_cell_kw(df):
135 | tab = Table(df, cell_kw={"linewidth": 2})
136 | assert tab.cell_kw == {"linewidth": 2}
137 |
138 |
139 | def test_get_column_textprops_default(table):
140 | col_def_dict = ColumnDefinition("A")._as_non_none_dict()
141 | textprops = table._get_column_textprops(col_def_dict)
142 | assert textprops == {"ha": "right", "multialignment": "right"}
143 |
144 |
145 | def test_get_column_textprops_added_kw(table):
146 | col_def_dict = ColumnDefinition(
147 | "A", textprops={"size": 16, "weight": "bold"}
148 | )._as_non_none_dict()
149 | textprops = table._get_column_textprops(col_def_dict)
150 | assert textprops == {
151 | "ha": "right",
152 | "multialignment": "right",
153 | "size": 16,
154 | "weight": "bold",
155 | }
156 |
157 |
158 | def test_get_column_textprops_replace_default_kw(table):
159 | col_def_dict = ColumnDefinition("A", textprops={"ha": "center"})._as_non_none_dict()
160 | textprops = table._get_column_textprops(col_def_dict)
161 | assert textprops == {"ha": "center", "multialignment": "center"}
162 |
163 |
164 | def test_textprops_is_default(table):
165 | assert table.textprops == {"ha": "right"}
166 |
167 |
168 | def test_textprops(df):
169 | tab = Table(df, textprops={"fontsize": 14})
170 | assert tab.textprops == {"ha": "right", "fontsize": 14}
171 |
172 |
173 | def test_celltext_textprops(table):
174 | for cell in table.cells.values():
175 | assert cell.textprops == {
176 | "ha": "right",
177 | "va": "center",
178 | "multialignment": "right",
179 | }
180 |
181 |
182 | def test_celltext_bbox_textprop(df):
183 | boxprops = {"boxstyle": "circle"}
184 | tab = Table(df, textprops={"bbox": boxprops})
185 | for cell in tab.cells.values():
186 | assert cell.textprops == {
187 | "ha": "right",
188 | "va": "center",
189 | "multialignment": "right",
190 | "bbox": boxprops,
191 | }
192 |
193 |
194 | def test_init_table_columns(table):
195 | table._init_columns()
196 | assert list(table.columns.keys()) == table.column_names
197 |
198 |
199 | @pytest.mark.parametrize(
200 | "colname,idx", [("index", 0), ("A", 1), ("B", 2), ("C", 3), ("D", 4), ("E", 5)]
201 | )
202 | def test_initialized_table_columns(table, colname, idx):
203 | table._init_columns()
204 | assert colname in table.columns.keys()
205 | col = table.columns[colname]
206 | assert col.index == idx
207 | assert col.cells == []
208 | assert col.name == colname
209 |
210 |
211 | def test_init_table_rows(table):
212 | table._init_rows()
213 | assert list(table.rows.keys()) == list(range(len(table.df)))
214 |
215 |
216 | def test_table_row_indices(df):
217 | tab = Table(df)
218 |
219 | first_row = tab.rows[0]
220 | df_row_values = df.to_records()[0]
221 |
222 | for cell, value in zip(first_row.cells, df_row_values):
223 | assert cell.content == value
224 |
225 |
226 | def test_col_label_row_index(table):
227 | assert table.col_label_row.index == -1
228 |
229 |
230 | def test_group_label_row_index(table):
231 | assert table.col_label_row.index == -1
232 |
233 |
234 | def test_get_column(table):
235 | col = table.get_column("A")
236 | assert col.index == 1
237 | assert col.name == "A"
238 | assert len(col.cells) == len(table.df)
239 |
240 |
241 | def test_get_column_by_index(table):
242 | col = table.get_column_by_index(1)
243 | assert col.index == 1
244 | assert col.name == "A"
245 | assert len(col.cells) == len(table.df)
246 |
247 |
248 | def test_get_row(table):
249 | row_data = table.df.to_records()
250 | row = table._get_row(0, row_data[0])
251 | assert len(row_data[0]) == len(row.cells)
252 |
253 |
254 | def test_xlim(table):
255 | assert table.ax.get_xlim() == (-0.025, table.df.shape[1] + 1 + 0.025)
256 |
257 |
258 | def test_ylim(table):
259 | assert table.ax.get_ylim() == (len(table.df) + 0.05, -1.025)
260 |
261 |
262 | def test_get_column_widths(table):
263 | assert table._get_column_widths() == [1] * (table.df.shape[1] + 1)
264 |
265 |
266 | def test_get_custom_column_widths(df):
267 | tab = Table(df, column_definitions=[ColumnDefinition("E", width=2)])
268 | assert tab._get_column_widths() == [1] * (tab.df.shape[1]) + [2]
269 |
270 |
271 | def test_get_custom_index_column_widths(df):
272 | tab = Table(df, column_definitions=[ColumnDefinition("index", width=2)])
273 | assert tab._get_column_widths() == [2] + [1] * (tab.df.shape[1])
274 |
275 |
276 | def test_get_even_rows(table):
277 | assert table.get_even_rows() == list(table.rows.values())[::2]
278 |
279 |
280 | def test_get_odd_rows(table):
281 | assert table.get_odd_rows() == list(table.rows.values())[1::2]
282 |
283 |
284 | def test_set_alternating_row_colors_even_rows(table):
285 | color = (0.9, 0.9, 0.9, 1)
286 | table.set_alternating_row_colors(color)
287 | for row in table.get_even_rows():
288 | for cell in row.cells:
289 | assert cell.rectangle_patch.get_facecolor() == color
290 |
291 |
292 | def test_set_alternating_row_colors_odd_rows(table):
293 | color2 = (0.9, 0.9, 0.9, 1)
294 | table.set_alternating_row_colors(color2=color2)
295 | for row in table.get_odd_rows():
296 | for cell in row.cells:
297 | assert cell.rectangle_patch.get_facecolor() == color2
298 |
299 |
300 | def test_set_alternating_row_colors_odd_and_even_rows(table):
301 | color = (0.9, 0.9, 0.9, 1)
302 | color2 = (0.85, 0.85, 0.85, 1)
303 | table.set_alternating_row_colors(color=color, color2=color2)
304 |
305 | for row in table.get_even_rows():
306 | for cell in row.cells:
307 | assert cell.rectangle_patch.get_facecolor() == color
308 |
309 | for row in table.get_odd_rows():
310 | for cell in row.cells:
311 | assert cell.rectangle_patch.get_facecolor() == color2
312 |
313 |
314 | def test_get_col_label_row(table):
315 | idx = len(table.df) + 1
316 |
317 | row = table._get_col_label_row(idx, table._get_column_titles())
318 |
319 | for col_idx, (cell, content) in enumerate(
320 | zip(row.cells, ["index", "A", "B", "C", "D", "E"])
321 | ):
322 | assert cell.content == content
323 | assert cell.row_idx == idx
324 | assert cell.col_idx == col_idx
325 | assert cell.xy == (col_idx, idx)
326 |
327 |
328 | def test_get_subplot_cells_is_empty_dict(table):
329 | assert table._get_subplot_cells() == {}
330 |
331 |
332 | def test_get_subplot_cells(df):
333 | def plot_fn(ax, arg):
334 | return None
335 |
336 | tab = Table(df, column_definitions=[ColumnDefinition("A", plot_fn=plot_fn)])
337 |
338 | subplot_cells = tab._get_subplot_cells()
339 | assert list(subplot_cells.keys()) == [(idx, 1) for idx in range(len(tab.df))]
340 |
341 | for cell in subplot_cells.values():
342 | assert hasattr(cell, "make_axes_inset")
343 | assert isinstance(cell, SubplotCell)
344 |
345 |
346 | def test_table_make_subplots(df):
347 | def plot_fn(ax, arg):
348 | ax.plot([0, 0], [1, 1])
349 |
350 | tab = Table(df, column_definitions=[ColumnDefinition("A", plot_fn=plot_fn)])
351 |
352 | subplot_cells = tab._get_subplot_cells()
353 |
354 | for cell in subplot_cells.values():
355 | assert len(cell.axes_inset.get_lines()) > 0
356 |
357 |
358 | def test_cell_text_is_formatted_by_formatter(df):
359 |
360 | col_defs = [ColDef("A", formatter=formatters.decimal_to_percent)]
361 | tab = Table(df, column_definitions=col_defs)
362 |
363 | for cell in tab.columns["A"].cells:
364 | cell_text = cell.text.get_text()
365 | assert len(cell_text) <= 4
366 | assert any([s in cell_text for s in ["–", "✓", "%"]])
367 |
368 |
369 | def test_cell_text_is_formatted_by_string_formatter(df):
370 | tab = Table(df, column_definitions=[ColDef("A", formatter="{:.2f}")])
371 | for cell in tab.columns["A"].cells:
372 | cell_text = cell.text.get_text()
373 | assert len(cell_text) == 4
374 |
375 |
376 | def test_table_apply_cmaps(df):
377 | tab = Table(df, column_definitions=[ColDef("B", cmap=mpl.colormaps["RdBu"])])
378 | base_cell_color = tab.cells[1, 1].rectangle_patch.get_facecolor()
379 |
380 | for cell in tab.columns["B"].cells:
381 | cell_color = cell.rectangle_patch.get_facecolor()
382 | assert cell_color != base_cell_color
383 |
384 |
385 | def test_table_apply_text_cmaps(df):
386 | tab = Table(df, column_definitions=[ColDef("B", text_cmap=mpl.colormaps["RdBu"])])
387 | base_text_color = tab.cells[1, 1].text.get_color()
388 |
389 | for cell in tab.columns["B"].cells:
390 | cell_color = cell.text.get_color()
391 | assert cell_color != base_text_color
392 |
393 |
394 | def test_col_label_row_height_is_set(df):
395 | tab = Table(df, col_label_cell_kw={"height": 2})
396 |
397 | for cell in tab.col_label_row.cells:
398 | assert cell.height == 2
399 | assert cell.rectangle_patch.get_height() == 2
400 |
401 |
402 | def test_set_col_label_row_influences_col_group_label_y(df):
403 | tab = Table(df, col_label_cell_kw={"height": 2})
404 |
405 | for cell in tab.col_group_cells.values():
406 | assert cell.y == -2
407 |
408 |
409 | # TODO
410 | def test_table_plot_colgroup_headers(df):
411 | tab = Table(df)
412 | assert True
413 |
414 |
415 | def test_table_plot_title_divider(df):
416 | tab = Table(df)
417 | assert True
418 |
419 |
420 | def test_table_plot_row_dividers(df):
421 | tab = Table(df)
422 | assert True
423 |
424 |
425 | def test_table_plot_column_borders(df):
426 | tab = Table(df)
427 | assert True
428 |
429 |
430 | def test_plot_col_group_labels(table):
431 | pass
432 |
433 |
434 | def test_plot_col_label_divider(table):
435 | pass
436 |
437 |
438 | def test_plot_footer_divider(table):
439 | pass
440 |
441 |
442 | def test_plot_row_dividers(table):
443 | pass
444 |
445 |
446 | def test_plot_column_borders(table):
447 | pass
448 |
449 |
450 | def test_plot_fn_with_formatter_does_not_raise(df):
451 | column_definitions = [
452 | ColDef(
453 | "A", plot_fn=plots.progress_donut, formatter=formatters.decimal_to_percent
454 | )
455 | ]
456 |
457 | tab = Table(df, column_definitions=column_definitions)
458 |
--------------------------------------------------------------------------------
/plottable/cell.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from numbers import Number
4 | from typing import Any, Callable, Dict, List, Tuple
5 |
6 | import matplotlib as mpl
7 | import matplotlib.pyplot as plt
8 | from matplotlib.patches import Rectangle
9 |
10 | from .column_def import ColumnType
11 |
12 |
13 | def create_cell(column_type: ColumnType, *args, **kwargs) -> TableCell:
14 | """Factory Function to create a specific TableCell depending on `column_type`.
15 |
16 | Args:
17 | column_type (ColumnType): plottable.column_def.ColumnType
18 |
19 | Returns:
20 | TableCell: plottable.cell.TableCell
21 | """
22 | if column_type is ColumnType.SUBPLOT:
23 | return SubplotCell(*args, **kwargs)
24 | elif column_type is ColumnType.STRING:
25 | return TextCell(*args, **kwargs)
26 |
27 |
28 | class Cell:
29 | """A cell is a rectangle defined by the lower left corner xy and it's width and height."""
30 |
31 | def __init__(self, xy: Tuple[float, float], width: float = 1, height: float = 1):
32 | """
33 | Args:
34 | xy (Tuple[float, float]): lower left corner of a rectangle
35 | width (float, optional): width of the rectangle cell. Defaults to 1.
36 | height (float, optional): height of the rectangle cell. Defaults to 1.
37 | """
38 | self.xy = xy
39 | self.width = width
40 | self.height = height
41 |
42 | @property
43 | def x(self) -> float:
44 | return self.xy[0]
45 |
46 | @property
47 | def y(self) -> float:
48 | return self.xy[1]
49 |
50 | def __repr__(self) -> str:
51 | return f"Cell({self.xy}, {self.width}, {self.height})"
52 |
53 |
54 | class TableCell(Cell):
55 | """A TableCell class for a plottable.table.Table."""
56 |
57 | def __init__(
58 | self,
59 | xy: Tuple[float, float],
60 | content: Any,
61 | row_idx: int,
62 | col_idx: int,
63 | width: float = 1,
64 | height: float = 1,
65 | ax: mpl.axes.Axes = None,
66 | rect_kw: Dict[str, Any] = {},
67 | ):
68 | """
69 | Args:
70 | xy (Tuple[float, float]):
71 | lower left corner of a rectangle
72 | content (Any):
73 | the content of the cell
74 | row_idx (int):
75 | row index
76 | col_idx (int):
77 | column index
78 | width (float, optional):
79 | width of the rectangle cell. Defaults to 1.
80 | height (float, optional):
81 | height of the rectangle cell. Defaults to 1.
82 | ax (mpl.axes.Axes, optional):
83 | matplotlib Axes object. Defaults to None.
84 | rect_kw (Dict[str, Any], optional):
85 | keywords passed to matplotlib.patches.Rectangle. Defaults to {}.
86 | """
87 |
88 | super().__init__(xy, width, height)
89 | self.index = (row_idx, col_idx)
90 | self.content = content
91 | self.row_idx = row_idx
92 | self.col_idx = col_idx
93 | self.ax = ax or plt.gca()
94 | self.rect_kw = {
95 | "linewidth": 0.0,
96 | "edgecolor": self.ax.get_facecolor(),
97 | "facecolor": self.ax.get_facecolor(),
98 | "width": width,
99 | "height": height,
100 | }
101 |
102 | self.rect_kw.update(rect_kw)
103 | self.rectangle_patch = Rectangle(xy, **self.rect_kw)
104 |
105 | def draw(self):
106 | self.ax.add_patch(self.rectangle_patch)
107 |
108 | def __repr__(self) -> str:
109 | return f"TableCell(xy={self.xy}, row_idx={self.index[0]}, col_idx={self.index[1]})" # noqa
110 |
111 |
112 | class SubplotCell(TableCell):
113 | """A SubplotTableCell class for a plottable.table.Table that creates a subplot on top of
114 | it's rectangle patch.
115 | """
116 |
117 | def __init__(
118 | self,
119 | xy: Tuple[float, float],
120 | content: Any,
121 | row_idx: int,
122 | col_idx: int,
123 | plot_fn: Callable,
124 | plot_kw: Dict[str, Any] = {},
125 | width: float = 1,
126 | height: float = 1,
127 | ax: mpl.axes.Axes = None,
128 | rect_kw: Dict[str, Any] = {},
129 | ):
130 | """
131 | Args:
132 | xy (Tuple[float, float]):
133 | lower left corner of a rectangle
134 | content (Any):
135 | the content of the cell
136 | row_idx (int):
137 | row index
138 | col_idx (int):
139 | column index
140 | plot_fn (Callable):
141 | function that draws onto the created subplot.
142 | plot_kw (Dict[str, Any], optional):
143 | keywords for the plot_fn. Defaults to {}.
144 | width (float, optional):
145 | width of the rectangle cell. Defaults to 1.
146 | height (float, optional):
147 | height of the rectangle cell. Defaults to 1.
148 | ax (mpl.axes.Axes, optional):
149 | matplotlib Axes object. Defaults to None.
150 | rect_kw (Dict[str, Any], optional):
151 | keywords passed to matplotlib.patches.Rectangle. Defaults to {}.
152 | """
153 | super().__init__(
154 | xy=xy,
155 | width=width,
156 | height=height,
157 | content=content,
158 | row_idx=row_idx,
159 | col_idx=col_idx,
160 | ax=ax,
161 | rect_kw=rect_kw,
162 | )
163 |
164 | self._plot_fn = plot_fn
165 | self._plot_kw = plot_kw
166 | self.fig = self.ax.figure
167 | self.draw()
168 |
169 | def plot(self):
170 | self._plot_fn(self.axes_inset, self.content, **self._plot_kw)
171 |
172 | def make_axes_inset(self):
173 | rect_fig_coords = self._get_rectangle_bounds()
174 | self.axes_inset = self.fig.add_axes(rect_fig_coords)
175 | return self.axes_inset
176 |
177 | def _get_rectangle_bounds(self, padding: float = 0.2) -> List[float]:
178 | transformer = self.fig.transFigure.inverted()
179 | display_coords = self.rectangle_patch.get_window_extent()
180 | (xmin, ymin), (xmax, ymax) = transformer.transform(display_coords)
181 | y_range = ymax - ymin
182 | return [
183 | xmin,
184 | ymin + padding * y_range,
185 | xmax - xmin,
186 | ymax - ymin - 2 * padding * y_range,
187 | ]
188 |
189 | def draw(self):
190 | self.ax.add_patch(self.rectangle_patch)
191 |
192 | def __repr__(self) -> str:
193 | return f"SubplotCell(xy={self.xy}, row_idx={self.index[0]}, col_idx={self.index[1]})" # noqa
194 |
195 |
196 | class TextCell(TableCell):
197 | """A TextCell class for a plottable.table.Table that creates a text inside it's rectangle patch."""
198 |
199 | def __init__(
200 | self,
201 | xy: Tuple[float, float],
202 | content: str | Number,
203 | row_idx: int,
204 | col_idx: int,
205 | width: float = 1,
206 | height: float = 1,
207 | ax: mpl.axes.Axes = None,
208 | rect_kw: Dict[str, Any] = {},
209 | textprops: Dict[str, Any] = {},
210 | padding: float = 0.1,
211 | ):
212 | """
213 | Args:
214 | xy (Tuple[float, float]):
215 | lower left corner of a rectangle
216 | content (Any):
217 | the content of the cell
218 | row_idx (int):
219 | row index
220 | col_idx (int):
221 | column index
222 | width (float, optional):
223 | width of the rectangle cell. Defaults to 1.
224 | height (float, optional):
225 | height of the rectangle cell. Defaults to 1.
226 | ax (mpl.axes.Axes, optional):
227 | matplotlib Axes object. Defaults to None.
228 | rect_kw (Dict[str, Any], optional):
229 | keywords passed to matplotlib.patches.Rectangle. Defaults to {}.
230 | textprops (Dict[str, Any], optional):
231 | textprops passed to matplotlib.text.Text. Defaults to {}.
232 | padding (float, optional):
233 | Padding around the text within the rectangle patch. Defaults to 0.1.
234 |
235 | """
236 | super().__init__(
237 | xy=xy,
238 | width=width,
239 | height=height,
240 | content=content,
241 | row_idx=row_idx,
242 | col_idx=col_idx,
243 | ax=ax,
244 | rect_kw=rect_kw,
245 | )
246 |
247 | self.textprops = {"ha": "right", "va": "center"}
248 | self.textprops.update(textprops)
249 | self.ha = self.textprops["ha"]
250 | self.va = self.textprops["va"]
251 | self.padding = padding
252 |
253 | def draw(self):
254 | self.ax.add_patch(self.rectangle_patch)
255 | self.set_text()
256 |
257 | def set_text(self):
258 | x, y = self.xy
259 |
260 | if self.ha == "left":
261 | x = x + self.padding * self.width
262 | elif self.ha == "right":
263 | x = x + (1 - self.padding) * self.width
264 | elif self.ha == "center":
265 | x = x + self.width / 2
266 | else:
267 | raise ValueError(
268 | f"ha can be either 'left', 'center' or 'right'. You provided {self.ha}."
269 | )
270 |
271 | if self.va == "center":
272 | y += self.height / 2
273 | # because the yaxis is inverted we subtract a ratio of the padding
274 | # if va is "bottom" and add it if it's "top"
275 | elif self.va == "bottom":
276 | y = y - self.padding * self.height
277 | elif self.va == "top":
278 | y = y - (1 - self.padding) * self.height
279 |
280 | self.text = self.ax.text(x, y, str(self.content), **self.textprops)
281 |
282 | def __repr__(self) -> str:
283 | return f"TextCell(xy={self.xy}, content={self.content}, row_idx={self.index[0]}, col_idx={self.index[1]})" # noqa
284 |
285 |
286 | class Sequence: # Row and Column can inherit from this
287 | """A Sequence of Table Cells."""
288 |
289 | def __init__(self, cells: List[TableCell], index: int):
290 | """
291 |
292 | Args:
293 | cells (List[TableCell]): List of TableCells.
294 | index (int): an index denoting the sequences place in a Table.
295 | """
296 | self.cells = cells
297 | self.index = index
298 |
299 | def append(self, cell: TableCell):
300 | """Appends another TableCell to its `cells` propery.
301 |
302 | Args:
303 | cell (TableCell): A TableCell object
304 | """
305 | self.cells.append(cell)
306 |
307 | def set_alpha(self, *args) -> Sequence:
308 | """Sets the alpha for all cells of the Sequence and returns self.
309 |
310 | Return:
311 | self[Sequence]: A Sequence of Cells
312 | """
313 | for cell in self.cells:
314 | cell.rectangle_patch.set_alpha(*args)
315 | return self
316 |
317 | def set_color(self, *args) -> Sequence:
318 | """Sets the color for all cells of the Sequence and returns self.
319 |
320 | Return:
321 | self[Sequence]: A Sequence of Cells
322 | """
323 | for cell in self.cells:
324 | cell.rectangle_patch.set_color(*args)
325 | return self
326 |
327 | def set_facecolor(self, *args) -> Sequence:
328 | """Sets the facecolor for all cells of the Sequence and returns self.
329 |
330 | Return:
331 | self[Sequence]: A Sequence of Cells
332 | """
333 | for cell in self.cells:
334 | cell.rectangle_patch.set_facecolor(*args)
335 | return self
336 |
337 | def set_edgecolor(self, *args) -> Sequence:
338 | """Sets the edgecolor for all cells of the Sequence and returns self.
339 |
340 | Return:
341 | self[Sequence]: A Sequence of Cells
342 | """
343 | for cell in self.cells:
344 | cell.rectangle_patch.set_edgecolor(*args)
345 | return self
346 |
347 | def set_fill(self, *args) -> Sequence:
348 | """Sets the fill for all cells of the Sequence and returns self.
349 |
350 | Return:
351 | self[Sequence]: A Sequence of Cells
352 | """
353 | for cell in self.cells:
354 | cell.rectangle_patch.set_fill(*args)
355 | return self
356 |
357 | def set_hatch(self, *args) -> Sequence:
358 | """Sets the hatch for all cells of the Sequence and returns self.
359 |
360 | Return:
361 | self[Sequence]: A Sequence of Cells
362 | """
363 | for cell in self.cells:
364 | cell.rectangle_patch.set_hatch(*args)
365 | return self
366 |
367 | def set_linestyle(self, *args) -> Sequence:
368 | """Sets the linestyle for all cells of the Sequence and returns self.
369 |
370 | Return:
371 | self[Sequence]: A Sequence of Cells
372 | """
373 | for cell in self.cells:
374 | cell.rectangle_patch.set_linestyle(*args)
375 | return self
376 |
377 | def set_linewidth(self, *args) -> Sequence:
378 | """Sets the linewidth for all cells of the Sequence and returns self.
379 |
380 | Return:
381 | self[Sequence]: A Sequence of Cells
382 | """
383 | for cell in self.cells:
384 | cell.rectangle_patch.set_linewidth(*args)
385 | return self
386 |
387 | def set_fontcolor(self, *args) -> Sequence:
388 | """Sets the fontcolor for all cells texts of the Sequence and returns self.
389 |
390 | Return:
391 | self[Sequence]: A Sequence of Cells
392 | """
393 | for cell in self.cells:
394 | if hasattr(cell, "text"):
395 | cell.text.set_color(*args)
396 | return self
397 |
398 | def set_fontfamily(self, *args) -> Sequence:
399 | """Sets the fontfamily for all cells texts of the Sequence and returns self.
400 |
401 | Return:
402 | self[Sequence]: A Sequence of Cells
403 | """
404 | for cell in self.cells:
405 | if hasattr(cell, "text"):
406 | cell.text.set_fontfamily(*args)
407 | return self
408 |
409 | def set_fontsize(self, *args) -> Sequence:
410 | """Sets the fontsize for all cells texts of the Sequence and returns self.
411 |
412 | Return:
413 | self[Sequence]: A Sequence of Cells
414 | """
415 | for cell in self.cells:
416 | if hasattr(cell, "text"):
417 | cell.text.set_fontsize(*args)
418 | return self
419 |
420 | def set_ha(self, *args) -> Sequence:
421 | """Sets the horizontal alignment for all cells texts of the Sequence and returns self.
422 |
423 | Return:
424 | self[Sequence]: A Sequence of Cells
425 | """
426 | for cell in self.cells:
427 | if hasattr(cell, "text"):
428 | cell.text.set_ha(*args)
429 | return self
430 |
431 | def set_ma(self, *args) -> Sequence:
432 | """Sets the multialignment for all cells tests of the Sequence and returns self.
433 |
434 | Return:
435 | self[Sequence]: A Sequence of Cells
436 | """
437 | for cell in self.cells:
438 | if hasattr(cell, "text"):
439 | cell.text.set_ma(*args)
440 | return self
441 |
442 |
443 | class Row(Sequence):
444 | """A Row of TableCells."""
445 |
446 | def __init__(self, cells: List[TableCell], index: int):
447 | super().__init__(cells=cells, index=index)
448 |
449 | def get_xrange(self) -> Tuple[float, float]:
450 | """Gets the xrange of the Row.
451 |
452 | Returns:
453 | Tuple[float, float]: Tuple of min and max x.
454 | """
455 | return min([cell.xy[0] for cell in self.cells]), max(
456 | [cell.xy[0] + cell.width for cell in self.cells]
457 | )
458 |
459 | def get_yrange(self) -> Tuple[float, float]:
460 | """Gets the yrange of the Row.
461 |
462 | Returns:
463 | Tuple[float, float]: Tuple of min and max y.
464 | """
465 | cell = self.cells[0]
466 | return cell.xy[1], cell.xy[1] + cell.height
467 |
468 | @property
469 | def x(self) -> float:
470 | return self.cells[0].xy[0]
471 |
472 | @property
473 | def y(self) -> float:
474 | return self.cells[0].xy[1]
475 |
476 | @property
477 | def height(self) -> float:
478 | return self.cells[0].height
479 |
480 | def __repr__(self) -> str:
481 | return f"Row(cells={self.cells}, index={self.index})"
482 |
483 |
484 | class Column(Sequence):
485 | """A Column of TableCells."""
486 |
487 | def __init__(self, cells: List[TableCell], index: int, name: str = None):
488 | super().__init__(cells=cells, index=index)
489 | self.name = name
490 |
491 | def get_xrange(self) -> Tuple[float, float]:
492 | """Gets the xrange of the Column.
493 |
494 | Returns:
495 | Tuple[float, float]: Tuple of min and max x.
496 | """
497 | cell = self.cells[0]
498 | return cell.xy[0], cell.xy[0] + cell.width
499 |
500 | def get_yrange(self) -> Tuple[float, float]:
501 | """Gets the yrange of the Column.
502 |
503 | Returns:
504 | Tuple[float, float]: Tuple of min and max y.
505 | """
506 | return min([cell.xy[1] for cell in self.cells]), max(
507 | [cell.xy[1] + cell.height for cell in self.cells]
508 | )
509 |
510 | @property
511 | def x(self) -> float:
512 | return self.cells[0].xy[0]
513 |
514 | @property
515 | def y(self) -> float:
516 | return self.cells[0].xy[1]
517 |
518 | @property
519 | def width(self) -> float:
520 | return self.cells[0].width
521 |
522 | def __repr__(self) -> str:
523 | return f"Column(cells={self.cells}, index={self.index})"
524 |
--------------------------------------------------------------------------------
/plottable/table.py:
--------------------------------------------------------------------------------
1 | """Module containing Table Class to plot matplotlib tables."""
2 |
3 | from __future__ import annotations
4 |
5 | from numbers import Number
6 | from typing import Any, Callable, Dict, List, Tuple
7 |
8 | import matplotlib as mpl
9 | import matplotlib.pyplot as plt
10 | import pandas as pd
11 |
12 | from .cell import Column, Row, SubplotCell, TextCell, create_cell
13 | from .column_def import ColumnDefinition, ColumnType
14 | from .font import contrasting_font_color
15 | from .formatters import apply_formatter
16 | from .helpers import _replace_lw_key
17 |
18 |
19 | class Table:
20 | """Class to plot a beautiful matplotlib table.
21 |
22 | Args:
23 | df (pd.DataFrame):
24 | A pandas DataFrame with your table data
25 | ax (mpl.axes.Axes, optional):
26 | matplotlib axes. Defaults to None.
27 | index_col (str, optional):
28 | column to set as the DataFrame index. Defaults to None.
29 | columns (List[str], optional):
30 | columns to use. If None defaults to all columns.
31 | column_definitions (List[plottable.column_def.ColumnDefinition], optional):
32 | ColumnDefinitions for columns that should be styled. Defaults to None.
33 | textprops (Dict[str, Any], optional):
34 | textprops are passed to each TextCells matplotlib.pyplot.text. Defaults to {}.
35 | cell_kw (Dict[str, Any], optional):
36 | cell_kw are passed to to each cells matplotlib.patches.Rectangle patch.
37 | Defaults to {}.
38 | col_label_cell_kw (Dict[str, Any], optional):
39 | col_label_cell_kw are passed to to each ColumnLabels cells
40 | matplotlib.patches.Rectangle patch. Defaults to {}.
41 | col_label_divider (bool, optional):
42 | Whether to plot a divider line below the column labels. Defaults to True.
43 | col_label_divider_kw (Dict[str, Any], optional):
44 | col_label_divider_kw are passed to plt.plot. Defaults to {}.
45 | footer_divider (bool, optional):
46 | Whether to plot a divider line below the table. Defaults to False.
47 | footer_divider_kw (Dict[str, Any], optional):
48 | footer_divider_kw are passed to plt.plot. Defaults to {}.
49 | row_dividers (bool, optional):
50 | Whether to plot divider lines between rows. Defaults to True.
51 | row_divider_kw (Dict[str, Any], optional):
52 | row_divider_kw are passed to plt.plot. Defaults to {}.
53 | column_border_kw (Dict[str, Any], optional):
54 | column_border_kw are passed to plt.plot. Defaults to {}.
55 | even_row_color (str | Tuple, optional):
56 | facecolor of the even row cell's patches. Top Row has an even (0) index.
57 | odd_row_color (str | Tuple, optional):
58 | facecolor of the even row cell's patches. Top Row has an even (0) index.
59 |
60 | Examples
61 | --------
62 |
63 | >>> import matplotlib.pyplot as plt
64 | >>> import numpy as np
65 | >>> import pandas as pd
66 | >>>
67 | >>> from plottable import Table
68 | >>>
69 | >>> d = pd.DataFrame(np.random.random((10, 5)), columns=["A", "B", "C", "D", "E"]).round(2)
70 | >>> fig, ax = plt.subplots(figsize=(5, 8))
71 | >>> tab = Table(d)
72 | >>>
73 | >>> plt.show()
74 |
75 | """
76 |
77 | def __init__(
78 | self,
79 | df: pd.DataFrame,
80 | ax: mpl.axes.Axes = None,
81 | index_col: str = None,
82 | columns: List[str] = None,
83 | column_definitions: List[ColumnDefinition] = None,
84 | textprops: Dict[str, Any] = {},
85 | cell_kw: Dict[str, Any] = {},
86 | col_label_cell_kw: Dict[str, Any] = {},
87 | col_label_divider: bool = True,
88 | footer_divider: bool = False,
89 | row_dividers: bool = True,
90 | row_divider_kw: Dict[str, Any] = {},
91 | col_label_divider_kw: Dict[str, Any] = {},
92 | footer_divider_kw: Dict[str, Any] = {},
93 | column_border_kw: Dict[str, Any] = {},
94 | even_row_color: str | Tuple = None,
95 | odd_row_color: str | Tuple = None,
96 | ):
97 |
98 | if index_col is not None:
99 | if index_col in df.columns:
100 | df = df.set_index(index_col)
101 | else:
102 | raise KeyError(
103 | f"The index_col `{index_col}` you provided does not exist."
104 | )
105 | self.index_col = index_col
106 |
107 | if columns is not None:
108 | self.df = df[columns]
109 | else:
110 | self.df = df
111 |
112 | if self.df.index.name is None:
113 | self.df.index.name = "index"
114 |
115 | self.ax = ax or plt.gca()
116 | self.figure = self.ax.figure
117 |
118 | self.n_rows, self.n_cols = self.df.shape
119 |
120 | self.column_names = [self.df.index.name] + list(self.df.columns)
121 | self._init_column_definitions(column_definitions)
122 |
123 | self.column_name_to_idx = {col: i for i, col in enumerate(self.column_names)}
124 | self.cell_kw = cell_kw
125 | self.col_label_cell_kw = col_label_cell_kw
126 | self.textprops = textprops
127 | if "ha" not in textprops:
128 | self.textprops.update({"ha": "right"})
129 |
130 | self.cells = {}
131 | self._init_columns()
132 | self._init_rows()
133 | self.ax.axis("off")
134 |
135 | self.set_alternating_row_colors(even_row_color, odd_row_color)
136 | self._apply_column_formatters()
137 | self._apply_column_cmaps()
138 | self._apply_column_text_cmaps()
139 |
140 | self._plot_col_group_labels()
141 |
142 | ymax = self.n_rows
143 |
144 | if col_label_divider:
145 | self._plot_col_label_divider(**col_label_divider_kw)
146 | if footer_divider:
147 | self._plot_footer_divider(**footer_divider_kw)
148 | if row_dividers:
149 | self._plot_row_dividers(**row_divider_kw)
150 | self._plot_column_borders(**column_border_kw)
151 |
152 | self.ax.set_xlim(-0.025, sum(self._get_column_widths()) + 0.025)
153 | if self.col_group_cells:
154 | miny = -2
155 | else:
156 | miny = -1
157 |
158 | self.ax.set_ylim(miny - 0.025, ymax + 0.05)
159 | self.ax.invert_yaxis()
160 |
161 | self._make_subplots()
162 |
163 | def _init_column_definitions(
164 | self, column_definitions: List[ColumnDefinition]
165 | ) -> None:
166 | """Initializes the Tables ColumnDefinitions.
167 |
168 | Args:
169 | column_definitions (List[ColumnDefinition]):
170 | List of ColumnDefinitions
171 | """
172 | if column_definitions is not None:
173 | self.column_definitions = {
174 | _def.name: _def._as_non_none_dict() for _def in column_definitions
175 | }
176 | else:
177 | self.column_definitions = {}
178 | for col in self.column_names:
179 | if col not in self.column_definitions:
180 | self.column_definitions[col] = ColumnDefinition(
181 | name=col
182 | )._as_non_none_dict()
183 |
184 | def _get_column_titles(self) -> List[str]:
185 | """Returns a List of Column Titles.
186 |
187 | Returns:
188 | List[str]: List of Column Titles
189 | """
190 | return [
191 | self.column_definitions[col].get("title", col) for col in self.column_names
192 | ]
193 |
194 | def _get_col_groups(self) -> set[str]:
195 | """Gets the column_groups from the ColumnDefinitions.
196 |
197 | Returns:
198 | set[str]: a set of column group names
199 | """
200 | return set(
201 | _dict.get("group")
202 | for _dict in self.column_definitions.values()
203 | if _dict.get("group") is not None
204 | )
205 |
206 | def _get_non_group_colnames(self) -> set[str]:
207 | """Gets the column_names that have no column_group.
208 |
209 | Returns:
210 | set[str]: a set of column names
211 | """
212 | return set(
213 | _dict.get("name")
214 | for _dict in self.column_definitions.values()
215 | if _dict.get("group") is None
216 | )
217 |
218 | def _plot_col_group_labels(self) -> None:
219 | """Plots the column group labels."""
220 | col_groups = self._get_col_groups()
221 |
222 | self.col_group_cells = {}
223 |
224 | for group in col_groups:
225 | columns = [
226 | self.columns[colname]
227 | for colname, _dict in self.column_definitions.items()
228 | if _dict.get("group") == group
229 | ]
230 | x_min = min(col.get_xrange()[0] for col in columns)
231 | x_max = max(col.get_xrange()[1] for col in columns)
232 | dx = x_max - x_min
233 |
234 | y = 0 - self.col_label_row.height
235 |
236 | textprops = self.textprops.copy()
237 | textprops.update({"ha": "center", "va": "bottom"})
238 |
239 | self.col_group_cells[group] = TextCell(
240 | xy=(x_min, y),
241 | content=group,
242 | row_idx=y,
243 | col_idx=columns[0].index,
244 | width=x_max - x_min,
245 | height=1,
246 | ax=self.ax,
247 | textprops=textprops,
248 | )
249 | self.col_group_cells[group].draw()
250 | self.ax.plot(
251 | [x_min + 0.05 * dx, x_max - 0.05 * dx],
252 | [y, y],
253 | lw=0.2,
254 | color=plt.rcParams["text.color"],
255 | )
256 |
257 | def _plot_col_label_divider(self, **kwargs):
258 | """Plots a line below the column labels."""
259 | COL_LABEL_DIVIDER_KW = {"color": plt.rcParams["text.color"], "linewidth": 1}
260 | if "lw" in kwargs:
261 | kwargs["linewidth"] = kwargs.pop("lw")
262 | COL_LABEL_DIVIDER_KW.update(kwargs)
263 | self.COL_LABEL_DIVIDER_KW = COL_LABEL_DIVIDER_KW
264 |
265 | x0, x1 = self.rows[0].get_xrange()
266 | self.ax.plot(
267 | [x0, x1],
268 | [0, 0],
269 | **COL_LABEL_DIVIDER_KW,
270 | )
271 |
272 | def _plot_footer_divider(self, **kwargs):
273 | """Plots a line below the bottom TableRow."""
274 | FOOTER_DIVIDER_KW = {"color": plt.rcParams["text.color"], "linewidth": 1}
275 | if "lw" in kwargs:
276 | kwargs["linewidth"] = kwargs.pop("lw")
277 | FOOTER_DIVIDER_KW.update(kwargs)
278 | self.FOOTER_DIVIDER_KW = FOOTER_DIVIDER_KW
279 |
280 | x0, x1 = list(self.rows.values())[-1].get_xrange()
281 | y = len(self.df)
282 | self.ax.plot([x0, x1], [y, y], **FOOTER_DIVIDER_KW)
283 |
284 | def _plot_row_dividers(self, **kwargs):
285 | """Plots lines between all TableRows."""
286 | ROW_DIVIDER_KW = {
287 | "color": plt.rcParams["text.color"],
288 | "linewidth": 0.2,
289 | }
290 | kwargs = _replace_lw_key(kwargs)
291 | ROW_DIVIDER_KW.update(kwargs)
292 |
293 | for idx, row in list(self.rows.items())[1:]:
294 | x0, x1 = row.get_xrange()
295 |
296 | self.ax.plot([x0, x1], [idx, idx], **ROW_DIVIDER_KW)
297 |
298 | def _plot_column_borders(self, **kwargs):
299 | """Plots lines between all TableColumns where "border" is defined."""
300 | COLUMN_BORDER_KW = {"linewidth": 1, "color": plt.rcParams["text.color"]}
301 |
302 | kwargs = _replace_lw_key(kwargs)
303 | COLUMN_BORDER_KW.update(kwargs)
304 |
305 | for name, _def in self.column_definitions.items():
306 | if "border" in _def:
307 | col = self.columns[name]
308 |
309 | y0, y1 = col.get_yrange()
310 |
311 | if "l" in _def["border"].lower() or _def["border"].lower() == "both":
312 | x = col.get_xrange()[0]
313 | self.ax.plot([x, x], [y0, y1], **COLUMN_BORDER_KW)
314 |
315 | if "r" in _def["border"].lower() or _def["border"].lower() == "both":
316 | x = col.get_xrange()[1]
317 | self.ax.plot([x, x], [y0, y1], **COLUMN_BORDER_KW)
318 |
319 | def _init_columns(self):
320 | """Initializes the Tables columns."""
321 | self.columns = {}
322 | for idx, name in enumerate(self.column_names):
323 | self.columns[name] = Column(index=idx, cells=[], name=name)
324 |
325 | def _init_rows(self):
326 | """Initializes the Tables Rows."""
327 | self.rows = {}
328 | for idx, values in enumerate(self.df.to_records()):
329 | self.rows[idx] = self._get_row(idx, values)
330 |
331 | self.col_label_row = self._get_col_label_row(-1, self._get_column_titles())
332 |
333 | def get_column(self, name: str) -> Column:
334 | """Gets a Column by its column_name.
335 |
336 | Args:
337 | name (str): the column_name in the df.
338 |
339 | Returns:
340 | Column: A Column of the Table
341 | """
342 | return self.columns[name]
343 |
344 | def get_column_by_index(self, index: int) -> Column:
345 | """Gets a Column by its numeric index.
346 |
347 | Args:
348 | index (int): numeric index
349 |
350 | Returns:
351 | Column: A Column of the Table
352 | """
353 | return list(self.columns.values())[index]
354 |
355 | def get_even_rows(self) -> List[Row]:
356 | return list(self.rows.values())[::2]
357 |
358 | def get_odd_rows(self) -> List[Row]:
359 | return list(self.rows.values())[1::2]
360 |
361 | def set_alternating_row_colors(
362 | self, color: str | Tuple[float] = None, color2: str | Tuple[float] = None
363 | ) -> Table:
364 | """Sets the color of even row's rectangle patches to `color`.
365 |
366 | Args:
367 | color (str): color recognized by matplotlib for the even rows 0 ...
368 | color2 (str): color recognized by matplotlib for the odd rows 1 ...
369 |
370 | Returns:
371 | Table: plottable.table.Table
372 | """
373 | if color is not None:
374 | for row in self.get_even_rows():
375 | row.set_facecolor(color)
376 |
377 | if color2 is not None:
378 | for row in self.get_odd_rows():
379 | row.set_facecolor(color2)
380 |
381 | return self
382 |
383 | def _get_column_widths(self):
384 | """Gets the Column Widths."""
385 | return [
386 | self.column_definitions[col].get("width", 1) for col in self.column_names
387 | ]
388 |
389 | def _get_col_label_row(self, idx: int, content: List[str | Number]) -> Row:
390 | """Creates the Column Label Row.
391 |
392 | Args:
393 | idx (int): index of the Row
394 | content (List[str | Number]): content that is plotted as text.
395 |
396 | Returns:
397 | Row: Column Label Row
398 | """
399 | widths = self._get_column_widths()
400 |
401 | if "height" in self.col_label_cell_kw:
402 | height = self.col_label_cell_kw["height"]
403 | else:
404 | height = 1
405 |
406 | x = 0
407 |
408 | row = Row(cells=[], index=idx)
409 |
410 | for col_idx, (colname, width, _content) in enumerate(
411 | zip(self.column_names, widths, content)
412 | ):
413 | col_def = self.column_definitions[colname]
414 | textprops = self._get_column_textprops(col_def)
415 |
416 | # don't apply bbox around text in header
417 | if "bbox" in textprops:
418 | textprops.pop("bbox")
419 |
420 | cell = create_cell(
421 | column_type=ColumnType.STRING,
422 | xy=(
423 | x,
424 | idx + 1 - height,
425 | ), # if height is different from 1 we need to adjust y
426 | content=_content,
427 | row_idx=idx,
428 | col_idx=col_idx,
429 | width=width,
430 | height=height,
431 | rect_kw=self.col_label_cell_kw,
432 | textprops=textprops,
433 | ax=self.ax,
434 | )
435 |
436 | row.append(cell)
437 | cell.draw()
438 |
439 | x += width
440 |
441 | return row
442 |
443 | def _get_subplot_cells(self) -> Dict[Tuple[int, int], SubplotCell]:
444 | return {
445 | key: cell
446 | for key, cell in self.cells.items()
447 | if isinstance(cell, SubplotCell)
448 | }
449 |
450 | def _make_subplots(self) -> None:
451 | self.subplots = {}
452 | for key, cell in self._get_subplot_cells().items():
453 | self.subplots[key] = cell.make_axes_inset()
454 | self.subplots[key].axis("off")
455 | cell.draw()
456 | cell.plot()
457 |
458 | def _get_column_textprops(self, col_def: ColumnDefinition) -> Dict[str, Any]:
459 | textprops = self.textprops.copy()
460 | column_textprops = col_def.get("textprops", {})
461 | textprops.update(column_textprops)
462 | textprops["multialignment"] = textprops["ha"]
463 |
464 | return textprops
465 |
466 | def _get_row(self, idx: int, content: List[str | Number]) -> Row:
467 | widths = self._get_column_widths()
468 |
469 | x = 0
470 |
471 | row = Row(cells=[], index=idx)
472 |
473 | for col_idx, (colname, width, _content) in enumerate(
474 | zip(self.column_names, widths, content)
475 | ):
476 | col_def = self.column_definitions[colname]
477 |
478 | if "plot_fn" in col_def:
479 | plot_fn = col_def.get("plot_fn")
480 | plot_kw = col_def.get("plot_kw", {})
481 |
482 | cell = create_cell(
483 | column_type=ColumnType.SUBPLOT,
484 | xy=(x, idx),
485 | content=_content,
486 | plot_fn=plot_fn,
487 | plot_kw=plot_kw,
488 | row_idx=idx,
489 | col_idx=col_idx,
490 | width=width,
491 | rect_kw=self.cell_kw,
492 | ax=self.ax,
493 | )
494 |
495 | else:
496 | textprops = self._get_column_textprops(col_def)
497 |
498 | cell = create_cell(
499 | column_type=ColumnType.STRING,
500 | xy=(x, idx),
501 | content=_content,
502 | row_idx=idx,
503 | col_idx=col_idx,
504 | width=width,
505 | rect_kw=self.cell_kw,
506 | textprops=textprops,
507 | ax=self.ax,
508 | )
509 |
510 | row.append(cell)
511 | self.columns[colname].append(cell)
512 | self.cells[(idx, col_idx)] = cell
513 | cell.draw()
514 |
515 | x += width
516 |
517 | return row
518 |
519 | def _apply_column_formatters(self) -> None:
520 | for colname, _dict in self.column_definitions.items():
521 | formatter = _dict.get("formatter")
522 | if formatter is None:
523 | continue
524 |
525 | for cell in self.columns[colname].cells:
526 | if not hasattr(cell, "text"):
527 | continue
528 |
529 | formatted = apply_formatter(formatter, cell.content)
530 | cell.text.set_text(formatted)
531 |
532 | def _apply_column_cmaps(self) -> None:
533 | for colname, _dict in self.column_definitions.items():
534 | cmap_fn = _dict.get("cmap")
535 | if cmap_fn is None:
536 | continue
537 |
538 | for cell in self.columns[colname].cells:
539 | if not isinstance(cell.content, Number):
540 | continue
541 |
542 | if ("bbox" in _dict.get("textprops")) & hasattr(cell, "text"):
543 | cell.text.set_bbox(
544 | {
545 | "color": cmap_fn(cell.content),
546 | **_dict.get("textprops").get("bbox"),
547 | }
548 | )
549 | else:
550 | cell.rectangle_patch.set_facecolor(cmap_fn(cell.content))
551 |
552 | def _apply_column_text_cmaps(self) -> None:
553 | for colname, _dict in self.column_definitions.items():
554 | cmap_fn = _dict.get("text_cmap")
555 | if cmap_fn is None:
556 | continue
557 |
558 | for cell in self.columns[colname].cells:
559 | if isinstance(cell.content, Number) & hasattr(cell, "text"):
560 | cell.text.set_color(cmap_fn(cell.content))
561 |
562 | def autoset_fontcolors(
563 | self, fn: Callable = None, colnames: List[str] = None, **kwargs
564 | ) -> Table:
565 | """Sets the fontcolor of each table cell based on the facecolor of its rectangle patch.
566 |
567 | Args:
568 | fn (Callable, optional):
569 | Callable that takes the rectangle patches facecolor as
570 | rgba-value as argument.
571 | Defaults to plottable.font.contrasting_font_color if fn is None.
572 | colnames (List[str], optional):
573 | columns to apply the function to
574 | kwargs are passed to fn.
575 |
576 | Returns:
577 | plottable.table.Table
578 | """
579 |
580 | if fn is None:
581 | fn = contrasting_font_color
582 | if "thresh" not in kwargs:
583 | kwargs.update({"thresh": 150})
584 |
585 | if colnames is not None:
586 | cells = []
587 | for col in colnames:
588 | cells.extend(self.get_column(col).cells)
589 |
590 | else:
591 | cells = self.cells.values()
592 |
593 | for cell in cells:
594 | if hasattr(cell, "text"):
595 | text_bbox = cell.text.get_bbox_patch()
596 |
597 | if text_bbox:
598 | bg_color = text_bbox.get_facecolor()
599 | else:
600 | bg_color = cell.rectangle_patch.get_facecolor()
601 |
602 | textcolor = fn(bg_color, **kwargs)
603 | cell.text.set_color(textcolor)
604 |
605 | return self
606 |
--------------------------------------------------------------------------------
/docs/notebooks/plots.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "# Plotting onto Column Cells"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "By providing a `plot_fn (Callable)` to a ColumnDefinition, plottable creates an axes on top of each Cell of this column, and creates plots based on each cell's value.\n",
15 | "\n",
16 | "You can provide additional keywords to the plot function by passing a `plot_kw` dictionary to ColumnDefinition.\n",
17 | "\n",
18 | "```python\n",
19 | "\n",
20 | " plot_fn: Callable = None\n",
21 | " A Callable that will take the cells value as input and create a subplot\n",
22 | " on top of each cell and plot onto them.\n",
23 | " To pass additional arguments to it, use plot_kw (see below).\n",
24 | " plot_kw: Dict[str, Any] = field(default_factory=dict)\n",
25 | " Additional keywords provided to plot_fn.\n",
26 | "```"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "metadata": {},
32 | "source": [
33 | "Commonly used example plots are provided in plottable.plots. You can have a look at them below.\n",
34 | "\n",
35 | "## Creating Custom Plot Functions\n",
36 | "\n",
37 | "You can also easily create your own functions. Just make sure to have \n",
38 | "``ax: matplotlib.axes.Axes`` as first and \n",
39 | "`val: Any` (the cells value) as second arguments.\n",
40 | "\n",
41 | "```python\n",
42 | "def custom_plot_fn(\n",
43 | " ax: matplotlib.axes.Axes,\n",
44 | " val: Any,\n",
45 | " # further arguments that can be passed via plot_kw\n",
46 | " ):\n",
47 | " ...\n",
48 | "```\n",
49 | "\n",
50 | "for more complex data you can create a dictionary or function that gets data based on the cells value, ie.\n",
51 | "\n",
52 | "```python\n",
53 | "def custom_plot_fn(\n",
54 | " ax: matplotlib.axes.Axes,\n",
55 | " val: Any,\n",
56 | " # further arguments that can be passed via plot_kw\n",
57 | " ):\n",
58 | " \n",
59 | " data = my_data_dict.get(val)\n",
60 | " or\n",
61 | " data = my_data_getter_function(val)\n",
62 | "```\n",
63 | "\n",
64 | "You can create Sparklines, Histograms, ... you name it. \n",
65 | "\n",
66 | "If you create any cool plots to use with plottable, please consider sharing them by creating a Pull Request!"
67 | ]
68 | },
69 | {
70 | "cell_type": "markdown",
71 | "metadata": {},
72 | "source": [
73 | "## Available Plots"
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": 1,
79 | "metadata": {},
80 | "outputs": [],
81 | "source": [
82 | "%load_ext autoreload\n",
83 | "%autoreload 2\n",
84 | "\n",
85 | "from pathlib import Path\n",
86 | "\n",
87 | "import matplotlib.pyplot as plt\n",
88 | "import numpy as np\n",
89 | "import pandas as pd\n",
90 | "from matplotlib.colors import LinearSegmentedColormap\n",
91 | "\n",
92 | "from plottable import ColumnDefinition, Table\n",
93 | "from plottable.plots import *"
94 | ]
95 | },
96 | {
97 | "cell_type": "code",
98 | "execution_count": 2,
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "path = list(Path(\"../example_notebooks/country_flags\").glob(\"*.png\"))[0]\n",
103 | "cmap = LinearSegmentedColormap.from_list(\n",
104 | " name=\"bugw\", colors=[\"#ffffff\", \"#f2fbd2\", \"#c9ecb4\", \"#93d3ab\", \"#35b0ab\"], N=256\n",
105 | ")"
106 | ]
107 | },
108 | {
109 | "cell_type": "markdown",
110 | "metadata": {},
111 | "source": [
112 | "## percentile_bars"
113 | ]
114 | },
115 | {
116 | "cell_type": "code",
117 | "execution_count": 3,
118 | "metadata": {},
119 | "outputs": [
120 | {
121 | "data": {
122 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAGEAAAA6CAYAAACgTzeXAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAACU0lEQVR4nO3cS2sTURjG8eecyUzGqdbEakUUN4KoGwU/gKC41c/qzo3oVnEhiIg3vEAp1to2oU7neo6L2EmmqZcB2z6L57fK6WF6OvOHBApvjPfeQ46UPeo/QBSBgiIQUAQCikBAEQgoAgFFIKAIBBSBQK/rBS/WHiAtN2GMxaB/HleX7jR7O9UYr9YfIq9/oGcjXB7ewjC+0Oyvpe/xcfQUlSvQDxZw/cw9hEHc7L/+/ghb+Qq8d0jCIW4s3z+0s7MsQ13XAABjDJIkaZ2dpil2/8MTBAHieHqtcw5ZlsE5B2MMoihCGIb//Ew7RfiWfsDn8fOZG3uHK6duwxgDAPg0eoaV7ZfNfhycaD2INxuPsZF9adYXF29iObk0uRFf4+3mk+lhO8C1pbuIguRQzs7zvHWvzjlYO3mjqKoKZVk2e1VVod/vN2cXRdHa9953iqC3IwKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEFIGAIhDoFOFYb9C+2PSaQYn99qNgobWOe4utdWin0y7WBLCmPbMyuz7os/ea/d2zr/f72e4wye/Wf2O6fstLVo2RllswxmIxOovAtidStov1ZmTpZP/c3PWjfLUZWToenW7t1a7EuPj6a1xqMPfgDvJs730zLmWtnXuQzjk45wBMxqX2hqnrGt57GGMQBPNR/6RzBPn/9JlAQBEIKAIBRSCgCAQUgYAiEFAEAopAQBEI/AQWzeShNKPijQAAAABJRU5ErkJggg==",
123 | "text/plain": [
124 | ""
125 | ]
126 | },
127 | "metadata": {},
128 | "output_type": "display_data"
129 | }
130 | ],
131 | "source": [
132 | "fig, ax = plt.subplots(figsize=(1, 0.5))\n",
133 | "\n",
134 | "bars = percentile_bars(ax=ax, val=72, color=\"#b0db7c\", background_color=\"#f0f0f0\", is_pct=False)\n",
135 | "\n",
136 | "plt.show()\n",
137 | "\n",
138 | "# fig.savefig(Path.home() / \"Downloads/percentile_plot.png\")"
139 | ]
140 | },
141 | {
142 | "cell_type": "markdown",
143 | "metadata": {},
144 | "source": [
145 | "## image"
146 | ]
147 | },
148 | {
149 | "cell_type": "code",
150 | "execution_count": 4,
151 | "metadata": {},
152 | "outputs": [
153 | {
154 | "data": {
155 | "image/png": "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",
156 | "text/plain": [
157 | ""
158 | ]
159 | },
160 | "metadata": {},
161 | "output_type": "display_data"
162 | }
163 | ],
164 | "source": [
165 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
166 | "\n",
167 | "image(ax, path)\n",
168 | "\n",
169 | "plt.show()\n"
170 | ]
171 | },
172 | {
173 | "cell_type": "markdown",
174 | "metadata": {},
175 | "source": [
176 | "## circled_image"
177 | ]
178 | },
179 | {
180 | "cell_type": "code",
181 | "execution_count": 5,
182 | "metadata": {},
183 | "outputs": [
184 | {
185 | "data": {
186 | "image/png": "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",
187 | "text/plain": [
188 | ""
189 | ]
190 | },
191 | "metadata": {},
192 | "output_type": "display_data"
193 | }
194 | ],
195 | "source": [
196 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
197 | "\n",
198 | "im = circled_image(ax, path)\n",
199 | "\n",
200 | "plt.show()\n"
201 | ]
202 | },
203 | {
204 | "cell_type": "markdown",
205 | "metadata": {},
206 | "source": [
207 | "## circled_image with border"
208 | ]
209 | },
210 | {
211 | "cell_type": "code",
212 | "execution_count": 6,
213 | "metadata": {},
214 | "outputs": [
215 | {
216 | "data": {
217 | "image/png": "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",
218 | "text/plain": [
219 | ""
220 | ]
221 | },
222 | "metadata": {},
223 | "output_type": "display_data"
224 | }
225 | ],
226 | "source": [
227 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
228 | "\n",
229 | "im = circled_image(ax, path, linewidth=2, visible=True, edgecolor=\"#999999\")\n",
230 | "\n",
231 | "plt.show()\n"
232 | ]
233 | },
234 | {
235 | "cell_type": "markdown",
236 | "metadata": {},
237 | "source": [
238 | "## bar"
239 | ]
240 | },
241 | {
242 | "cell_type": "code",
243 | "execution_count": 7,
244 | "metadata": {},
245 | "outputs": [
246 | {
247 | "data": {
248 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAGEAAABhCAYAAADGBs+jAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAABFklEQVR4nO3dsQnDMBBAUctkJq/hJmOmyRpZSl7BKYI+4b36BAefqzXmnHNjqX31AoiQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBDy+fXC8X7/Y4+98zuftWZcQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBIgQIEKACAEiBAxfAa/nEgJECBAhQIQAEQJECBAhQIQAEQJECBAhQIQAEQJECBAhQIQAEQIulI8Lu0/4qLQAAAAASUVORK5CYII=",
249 | "text/plain": [
250 | ""
251 | ]
252 | },
253 | "metadata": {},
254 | "output_type": "display_data"
255 | }
256 | ],
257 | "source": [
258 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
259 | "b = bar(ax, 1, color=\"k\", cmap=cmap)\n",
260 | "plt.show()"
261 | ]
262 | },
263 | {
264 | "cell_type": "markdown",
265 | "metadata": {},
266 | "source": [
267 | "## bar with value annotation and linecolor"
268 | ]
269 | },
270 | {
271 | "cell_type": "code",
272 | "execution_count": 8,
273 | "metadata": {},
274 | "outputs": [
275 | {
276 | "data": {
277 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAGEAAABhCAYAAADGBs+jAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAD6UlEQVR4nO3cTWgUdxzG8WfXxNityppNKlYSNcTdxLwYNiaHQvWiiFhreyjFkoAFobWxSElLFRqpeqiQStMqxNeDaBtRMIrSHmwv6UUICYRCN4gVN01f3Mwm63ta2Z2emrAYcSfJ/ucxPB+YQ4b9Jz/2y8xkJ0M8tm3bEFd53R5AFIGCIhBQBAKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEFIGAIhBQBAKKQEARCCgCAUUgoAgEcpwuGBgYgGVZ2ZhlRikoKEBxcXFmL7YdiEaj9hxfng1A2zM2n89nR6PRjN5XR0eCZVkYffgP9rY3Y2mwyMnSrKr0v+r2CGkikQgaGhpgWVZGR4Pj0xEALA0WoWxl6WSWZkU4EHZ7hCnRhZmAIhBQBAKKQGBSF+apOH/yCr49fAHx2AiWVyxD84H3UBEOTfjaKx0/Yv+HbWn7Zufl4uc/Og1Mao7RCFc7u/B1ywl8+mUTKmpDOHvkEna+tQfnrh1FfqF/wjUvzvPh/LWj4zs8ZmY1yejpqKP9IjY3rsemd9ahJFSMXQebMOeFPFz+7upT13g8HgQWLhjfXlpgcGIzjEV4/O9j9PfdQP2amvEf7vWibk0Nfunuf+q6Rw8eYXPNu9hUvRUfN+zHzf6ogWnNMhYhEb+LZDL1xGknv9CP4djIhGuWlC7GZ9/sROvpFuxtb4adSmHbhk9w+8+Zde/K+IXZiaq6clTVlY99XV1fjrdf2Y7OUz/g/d2NLk42vYwdCf7AfMya5cXwUCJt//BQAvkZnudzcnMQrCrB4M2/sjChe4xFyJ2di7KVpeju6hvbl0ql0N3Vh6q6soy+RzKZxG+/RlGwcGZdnI2ejrZsfwP7dnyF8prlWBEO4uyRSxh9OIrXtqwFAHz+wUEULgqgqWUrAOBEawcqV4VQtOxl3LtzH2cOX8DfgzG83rje5NhZZzTCujdXIxG/g2MHziAeG0GwsgRt5/aN/dp5e3AIXu/4wXkvcR9ffHQI8dgI5vnnoqy6FMe/b0VJKMM/ljwnPE7+B15vby9qa2tx6qc2qlvZ9YGNbo+Q5v/3qaenB+Hws2+z694RAUUgoAgEFIGAIhBQBAKKQEARCEzqE/Ot679P9xxTkuPvdXuENJFIxNkCp49B+nw+1x8xfB42J49BOrptAeiB4Ew5eSDYcQSZfrowE1AEAopAQBEIKAIBRSCgCAQUgYAiEFAEAopAQBEIKAIBRSCgCAQUgYAiEFAEAopAQBEIKAIBRSCgCAQUgYAiEFAEAopAQBEIKAIBRSCgCAQUgcB/U2Ea/RxSxI4AAAAASUVORK5CYII=",
278 | "text/plain": [
279 | ""
280 | ]
281 | },
282 | "metadata": {},
283 | "output_type": "display_data"
284 | }
285 | ],
286 | "source": [
287 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
288 | "b = bar(ax, 0.5, plot_bg_bar=True, cmap=cmap, annotate=True, lw=1, height=0.35)\n",
289 | "plt.show()"
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "metadata": {},
295 | "source": [
296 | "## percentile_stars"
297 | ]
298 | },
299 | {
300 | "cell_type": "code",
301 | "execution_count": 9,
302 | "metadata": {},
303 | "outputs": [
304 | {
305 | "data": {
306 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAK8AAABhCAYAAACgcPGxAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAE9UlEQVR4nO3dvW9bVRzG8eecc30dX5sMgFhQC2qlRCoDE8qQjDBUDKgbrAzQdoP/oP8ALN2ZoEOiJn8CLxUSQh0QLxUMICfdGoXgJm6Se885DEmsvNjJPb62r3/281mqNLafqP7W9/omUpT33oNIIF32F0DUL8ZLYjFeEovxkliMl8RivCQW4yWxGC+JxXhJLMZLYjFeEovxkliMl8RivCQW4yWxGC+JxXhJLMZLYjFeEovxkliMl8RivCQW4yWxGC+JxXhJLMZLYjFeEovxkliMl8RivCQW4yWxGC+JxXhJLMZLYjFeEovxkliMl8RivCTWWMVrt5/ArbwGu/2EO6Eb1g5tAwCstWi1WkPfCTFW8fq/v4Y+eAb3zzfcCdxI03RoGwCQpim890PfCTFW8aqnywAAvbHMncCNUcQ7ip0QYxOv/fcPmJ2/AABm58+hHWonaefkhnNuaId0ay2cc0PfCTU28brmCvzRl+Oh4Zor3AnYAIb3qnj2ccfl1Xds4j08tB7/GmQ3tEPtJO2c3pi+eKNRDfnsBezmzzj5j92xv4Xo+W+dDxUA0/oVWXMVqL7c5dEUzKvvQEW1id4J2QAOD+lpmkIp1WUDMMZ0/Zz3vuepgPe+c8pQdGfQ1Kh+63v6+5eo/PJ5z897aCi4nh+fe7y3v0Dlrc8meidk47/3t3ve7tjMzAyq1eq5v9/f38fe3t6l98+r186gjey0IZq/jezaHQCAx/n/lWef2G5P9PH9smt3EM3fnvid0I2LxHGMOI6DPxdqkI91mZG98h7Lmg+hf/oYyu5C+Sz3/bwy8KYBt/AVojduTdVOno2LXnmTJEGlUrn0a0rTFO12+9LbFd0ZlJHHCwBuZwPu0YcwWz92eT05zwOwryxCLz6AblyZyp3LNrrFa4xBkiTQOv8B1jmHdrsddDmsn51BKOVqg25cgXnvO2Q37nU9HJ7koZDduAfz7rdBQU3aTsgGAFSrVdTr9eCgtNao1+u5z1n73RmE0i6VKRPBzH2S67Zm7lMo09+FkUnaCdmI47jvd/xKqdznrUV2iir1Oq9bf5jjVj7n7aZjJ+99syz/+XeR+xfdKaLcb1KsLwMnDoFemVN/HjJA0Qv8k7TTY+Osg4OD/jcC7l90p4jS4nUvNmE2f+hc7vFQcI15ZEtrcI25znmdgoV59j38/tbU71y0cfac01qLft+Ld/v5Ba111zdlRXaKKi/ejTUo2M6Taq/fhb75GNHVD6BvPoa9fhfA4ROkYGGbq1O/c9FGo9E4d57a77dxz54KxHGMRqOBSqUy0J2iyjtt2Dj8QRUfzSJbWkO0cB8qmgEAqKiGaOE+ssVV+Gj21O2neueiDaVQq9WQJEnn5v1GdfJ+SZKgVqt13pQNcqeoUq7zAoBbvQqXvAm99AC6/nrv2+0+hXv0EXS7CX1rfap3cm8cXat1zmF2djZoAwBarVbP04RB7hRVWrz+4DkQ1aFyXB/0zgHZLlT80lTvBG0cPa39XMYKuW+RnaJKi5eoqLH5eV6iUIyXxGK8JBbjJbEYL4nFeEksxktiMV4Si/GSWIyXxGK8JBbjJbEYL4nFeEksxktiMV4Si/GSWIyXxGK8JBbjJbEYL4nFeEksxktiMV4Si/GSWIyXxGK8JBbjJbEYL4nFeEksxktiMV4Si/GSWIyXxGK8JBbjJbEYL4n1P2duXPwwMywiAAAAAElFTkSuQmCC",
307 | "text/plain": [
308 | ""
309 | ]
310 | },
311 | "metadata": {},
312 | "output_type": "display_data"
313 | }
314 | ],
315 | "source": [
316 | "fig, ax = plt.subplots(figsize=(2, 1))\n",
317 | "\n",
318 | "stars = percentile_stars(ax, 70, background_color=\"#f0f0f0\")"
319 | ]
320 | },
321 | {
322 | "cell_type": "markdown",
323 | "metadata": {},
324 | "source": [
325 | "## progress_donut"
326 | ]
327 | },
328 | {
329 | "cell_type": "code",
330 | "execution_count": 10,
331 | "metadata": {},
332 | "outputs": [
333 | {
334 | "data": {
335 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAGEAAABhCAYAAADGBs+jAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/av/WaAAAACXBIWXMAAA9hAAAPYQGoP6dpAAAMTElEQVR4nO2de3BU1R3HP3c3L8g74SFCIU0gQAEJghINDQWEUKBWwcijIHRgbKdC0bG2A3QmE2trGanQEagKiKnAmDjC8DYIAYaXPISolIghhhgMZCEkhEBeu3v6x0mW7CPJ5u5u9gb2M7OT7LnnnnN2v/e8f+e3ihBC4MOr6LxdAB8+ETSBTwQN4BNBA/hE0AA+ETSATwQN4BNBA/hE0AA+ETSATwQN4BNBA/h5uwCtYjZB+WUoK4CbBfJv2SW4YwBjHYQ9DDM2Q2Cot0uqGm2KUF0Olw5AfjZc/AxqK2W4ogNFD+b6e3HL8qUwDyd4pajuQDsimOrh/Fb48gMoPgXCDDo/MBvvxRFm+brP8L4Itbfhyww4/g5UXZNPe+MX3VSA+xjviVB3F46uhC/WyP9p2Fty5knX+TepFQL8O3uypB7HOyJc/Ax2vQxVpa1/6Tq97JwVHYT1hC7x0KUfhPcCvyCIjIGu8e1Rao/RviJUV8DeP8PXmdbNji2N17rEw4DJ0C8Feg4Hv4B2LW570X4iXP0aNj8Hd27I93YCKICATpGQ+BIMnQERP2m34nmT9hEhfz9kzgZTHQiT4ziRfSDpZRg6E/yD2qVYWsHzInz5Iex6paHftXn6FT0EBMOEN2DYbNn+P4B4VoRD/4RDbzq40ND0DJgCU96G4C4eLYbW8ZwIZz5wLEDjrPfXa2DodI9l35FQPGL8lb8ftqTad76KHgJDYFYW9E50e7YdFffXhGvnIWuOZe5lQdHLxbYXtkN0nNuz7ci4V4TqCtg0DYy1WHXCOj850VpwAEK6ujXL+wH37id8ngZ3rlsPQxWdHAHN2eYToBncVxMuH4OzH9oEKlKEWVm+JqgF3FMT6mtg+x9ku2+FgKdX+zrhVnBPTTi6EsqLsOqNdXqIGyeXH3y0iOs14e5NOPZv7IZDugCYshIUxeUs7ndcF+Hke2CqtQ8f/7pcbvbRKq41R/U1cPI/1pMyRQdRsfDYfBeL9uDgWk343zaouWUdJsww+i8P7GKcGlwT4cwH8slvSkh3GDTVpWQfNNQ3R9XlcOU0Vh2yooORvwO99+0HOhLqa0JBDnYjImGGR3xD0raiXoT8fXJNqCld+0N4TxeL9OChTgSzWVpMNLUL0umh/ySnkzh9+jSTJk0iIiKC4OBgEhMTycrKcvr+mJgYFEVp8XXkyBFL/NLSUhYuXMjIkSPp3r07gYGB9OrVi3HjxrF161a8eZxb3X7CjXxYPcI+/Ld7oc+Trd5+8OBBUlJSCAoKYsaMGYSGhvLpp59SVFTEihUrePXVV1tNY9WqVVRUVNgX7cYN1qxZQ2RkJCUlJQQFyf3qM2fOMHbsWBITE4mNjSUqKgqDwcDOnTsxGAwsWLCAdevWtZqvRxBquPiZEGlh1q/0SCGMda3eWl9fL+Li4kRgYKA4d+6cJbyiokLEx8eLgIAAcfnyZVXFEkKIFStWCEAsWrTIKryurk4YjUa7+JWVlWLgwIECEOfPn1edryuoa45uFtoPTUN7gt6/1VtzcnIoKChg1qxZJCQkWMLDw8NZunQpdXV1ZGRkqCoWwIYNGwCYP996sujv749ebz93CQ0NZeLEiQBcunRJdb6uoFKE7+1XTLv0derWQ4cOATBhwgS7aykpKQAcPnxYVbGOHz9OXl4eI0aMYOjQoU7dU1NTQ05ODoqiMGjQIFX5uoq6Af3NAmvzdJ0/RDm3X5Cfnw9Av3797K499NBDhISEWOK0lcZasGDBgmbjGAwG1q5di9lsxmAwsGfPHoqLi0lLS6NvX+ceJHejToTbV63fC7PT1nK3bslljvDwcIfXw8LCLHHaQlVVFVlZWXTu3JmZM2c2G89gMJCenm557+/vz1tvveXUYMBTqJwnOFietp0ztDOZmZlUVVWRmppKWFhYs/EGDx6MEAKj0UhhYSHp6eksW7aMadOmYTR6xxRfnQiO9gicPLzRWAOae9orKyubrSUt4UxT1BS9Xk9MTAxLlizhjTfeYNu2bV4boqoUwcFtTk43GvsCR+3+tWvXqKqqcthftMSFCxc4ceIEAwYMYNSoUW26F+4NEhoHDe2NyubIkQjO1YTRo0cDsG/fPrtr2dnZVnGcpblhqbOUlJQAsn/wCqpmFxsn20zUooXIXubUrfX19SI2NrbFyVphYaElvKSkROTl5YmKigqH6dXV1YmuXbsKf39/UVpa2my+ubm5oq7OfjJZVlYmEhISBCA2b97s1GeQGd8VIv9zIQzfClF7x/n7HKCuN43sAz+cuLd2JEwNG/2t4+fnx/r160lJSSE5OdnhskVMTIwl/pIlS8jIyGDjxo3MmzfPLr0dO3Zw/fp1pk6dSrdu3ZrNd+XKlezatYukpCR69+5Np06dKCoqYvfu3dy5c4fU1NQWR1V27HoZvvr43vtOUTB9E8QkOZ9GA+pEiOhj/V6Y4fq3Tt8+ZswYjh49SlpaGpmZmdTX1zNkyBCWL1/O9OltMxJ2tkOeM2cOZrOZkydPcvDgQaqrq4mOjiY5OZm5c+e2OV9+PGv9vrpcPpgqRFC3gHdhO2S9YB2m08OyUqeWLjo8xlp4s5c89NKU1AwY9Eybk1PXMXf7mX2Y2QQl51Ql1+H44Qt7AQC6DVSVnDoRomLt3Rjo/ODiXlXJdTgKDthPToO7yIOOKlAngk4vT1Q2tagwGyFvp6rkOhzfZdtsaPnJ70OloZv67c3+v5RNUFPK8p0eJXVYKkvsByFmI8SNVZ2kehH6PmW/nK3o4JtPVCfZIXBk5oMCsWNUJ6lehE4R0PsJ6wIJM5xYIy3z7kfqq+HU+zYWh3ro9RgER6tO1jXjr2G/sV+uqL4JX21xKVnN8s0nDiwOTfDkQpeSdU2Ewc9JizsrFGkqb9tfdHRMxgbrc5vON7QH9J/sUtKuieAXAEmLsS6YgIof4Kz6fWJNcuo96XHMyuBNgSdectni0HXT+EfnymOxtmQvg1tXXE5eE9y6Agdetw/3C4Rhc1xO3nURAkOkQxDbEYOxFnYsdnqfQdPsec16Tx0ABZJfkwMUF3HPmbWkxfJASNMhqzBBwX7rlcaOyFcfw8U91n2copcHIZ/8o1uycI8IAZ3hmXcdeHBRYOdiKMl1SzbtTkku7FhkHy5M8PQ7bvO/5L5zzDFJMGK+TbMk5Gxy83NQUey2rNqFimJ5MN52lKfoIWG2U+aezuLew+Tj0yGkm32zdPcmZEyBqutuzc5j3C6Fj56BmnKbg/F6OSSf8De3ZudeEQJDYcaWhoW9JsNWYZJP1obx0oeplrmRD+t+IU09rWqBIj/XrEzoHOXWLN3vprnncHj2PewPkJjk/GHdGCg67vZs3ULxaVg/rsFBom3/JmDq+9DjEbdn6xlf2YOnQooDX0fCJP2gZvwKcrdoZ/gqBJzbBB9Ogtoqx7P9CX+HQc96JHvP+Dtq5OA/4PDy5q/HT4TJ//LueedbV2D7Ivg+B4tHMlvG/hV+/iePHYz3rAgAp9ZJN5yOfODp9PLk/1Np8PiL7Xvs1mSEcx9B9lK5VWnnjVgnNXl6tVyo9CCeFwGkJ7CsOXIW3Zw3yKg4Oel75Hnw7+S5stTXQO4mOPI2VP7oOI6ilwYL0zdBv/GeK0tjdu0iAkDpBTnudtjpgaUpCIqAkb+H4fMgrIf78q8olkvRJ1bLITPgsOlRdHJldPom6Pmo+/JvgfYTAeSH/zwNzv1XPm3N1YpGD8E9EqTHyJ8myy+kLeY09dVS+IID8O1uuJrbilfihvI8/iKMS3O8KOkh2leERi4fgx0L5Vjc0dNoocFplTDdc+EWHSetPSJ6gz5AjmSESf69Wyb3fw0XoPKqTLslsS3ZNPjhfvZdiGm7QbGreEcEkP3D0VVwbJV8alsUw4ZGr/EILJNCRZH/m41tSEuBwDC5M/bES9JNnBfwngiN1NyCsx/BiXfg9rWWmwx30Jh+cDcY9QoMn+u1L99SJK+L0IjJCHk74Iu1cOUMTjclzmBJR5Gu4IbNhiHPa8YLvXZEaMrtUvhuL1zaD98fdvCbOi00OTp/+YU31qaAYGkT1Pcp6XEgpHnLbW+hTRGaYjZLo7KbhfJXpsovQ3mhXIcC2WErejnRC+gM0f3u/dBFl3jZ4eq0/Utm2hfhAUDbj8gDgk8EDeATQQP4RNAAPhE0gE8EDeATQQP4RNAAPhE0gE8EDeATQQP4RNAA/wcCipQDZ+qSTQAAAABJRU5ErkJggg==",
336 | "text/plain": [
337 | ""
338 | ]
339 | },
340 | "metadata": {},
341 | "output_type": "display_data"
342 | }
343 | ],
344 | "source": [
345 | "fig, ax = plt.subplots(figsize=(1, 1))\n",
346 | "donut = progress_donut(ax, 73, textprops={\"fontsize\": 14})\n",
347 | "plt.show()"
348 | ]
349 | }
350 | ],
351 | "metadata": {
352 | "kernelspec": {
353 | "display_name": "Python 3.10.5 ('env': venv)",
354 | "language": "python",
355 | "name": "python3"
356 | },
357 | "language_info": {
358 | "codemirror_mode": {
359 | "name": "ipython",
360 | "version": 3
361 | },
362 | "file_extension": ".py",
363 | "mimetype": "text/x-python",
364 | "name": "python",
365 | "nbconvert_exporter": "python",
366 | "pygments_lexer": "ipython3",
367 | "version": "3.10.5"
368 | },
369 | "orig_nbformat": 4,
370 | "vscode": {
371 | "interpreter": {
372 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
373 | }
374 | }
375 | },
376 | "nbformat": 4,
377 | "nbformat_minor": 2
378 | }
379 |
--------------------------------------------------------------------------------
/docs/example_notebooks/heatmap.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "c4c59ec3",
6 | "metadata": {},
7 | "source": [
8 | "# Heatmap Example"
9 | ]
10 | },
11 | {
12 | "cell_type": "code",
13 | "execution_count": 1,
14 | "id": "f01d7352-fadb-48c3-a124-77c56a6ac0df",
15 | "metadata": {},
16 | "outputs": [],
17 | "source": [
18 | "%load_ext autoreload\n",
19 | "%autoreload 2\n",
20 | "\n",
21 | "import matplotlib.pyplot as plt\n",
22 | "import numpy as np\n",
23 | "import pandas as pd\n",
24 | "from matplotlib.colors import LinearSegmentedColormap\n",
25 | "\n",
26 | "from plottable import ColDef, Table"
27 | ]
28 | },
29 | {
30 | "cell_type": "code",
31 | "execution_count": 2,
32 | "id": "a381419c-1c73-4ce5-8a8c-9a5e5c368d79",
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "cmap = LinearSegmentedColormap.from_list(\n",
37 | " name=\"BuYl\", colors=[\"#01a6ff\", \"#eafedb\", \"#fffdbb\", \"#ffc834\"], N=256\n",
38 | ")"
39 | ]
40 | },
41 | {
42 | "cell_type": "code",
43 | "execution_count": 3,
44 | "id": "91651fe1-22e6-4671-981c-fd17816c5771",
45 | "metadata": {},
46 | "outputs": [
47 | {
48 | "data": {
49 | "image/png": "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rT//+c+KiIiQJEVERCgkJCTbuUaMGKFSpUppyJAhTst8fX1Nl9Fkx8/PT4mJiU798fHxkqQSJUpY3q+vvvpKZcuWVYsWLRz7tWbNGqf59+3bp6SkJEfr27dvtvMVLVpU48aNk6+v720FMQAAAAAAwJlbhCjp6en69ttvHZe+dO3a1XQWSrdu3RQbG6utW7dKkpYvX67AwEA1bNjQaa74+HiNGDFCw4cP10MPPeS0HV9f35vWkpKSolKlSjn1+/v7S1KuIcz1Tp06pW3btqlr164qWrSonn766Wwv5enQoYPq1avnaDeOuT5g6d27t7p06aLz589brgMAAAAAAOTOLUIU6dqlL0899ZRatmypqlWrOl3KExAQoNTUVKWmpsput8vT0zPHG8zOmjVLR44c0cSJE0390dHR2d5L5IknntDAgQMlXbtsp3r16k5jbDab4uPjFR8fr7S0NElyumTIx8fHsez6/erSpYs6dOggLy8v01N5rq/r6NGjjnZjUJMVrgwbNkze3t6O+7cAAAAAAIA7x21ClO+//16ZmZn65JNP9Pvvv+vgwYOSpPLlyys4OFjdunUzna0xYsQI9ejRI9u5rl69qjfeeEMhISF65JFHTNto0KCBatasaRo/cOBAPf7445KklStXqm3btk5nsYSFhWnVqlWSrj0tJyEhQfXq1TONqV+/vqPuLF999ZUqV66sDz74QD/++GO2lwrlJitcmTx5sk6fPq0RI0bkeQ4AAAAAAHBzbhOipKWl6dtvv1VQUJDpLJTu3bvr9OnT+uabbxQVFeVos2fPVoUKFfTEE09kO9/mzZu1aNEiPfDAA46+3bt368svv9SiRYvUtGlTBQYGavDgwerUqZPGjRsnSfrxxx8VGRmpr7/+Ws2aNVPVqlU1YsQItWvXTh988IFjrilTpmjixInq2LGjqlatqtDQUL3zzjuaMGGCqY6TJ09q69atTvt1vbJly6p8+fKmlnUz2utdvXpVb775pl577TVVqVLF8rEFAAAAAAC5c5sQRZIjZLg+bAgJCdHy5cudxp49e1Y//fRTjpf0SNLbb7+t5ORkU1/fvn21du1affPNN9q1a5e6deump59+Wrt27XKM6datm3766SdFRERo7969atWqlf70pz/pyJEjjjGjR4/WjBkz9H//9386ePCghg8frkGDBmnhwoXZ7tfly5ez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YAEhCgAAAAAAgAWEKAAAAAAAABYQogAAAAAAAFhAiAIAAAAAAGABIQoAAAAAAIAFhCgAAAAAAAAWEKL8f0888YQ2btyo5ORk2e12DR8+XB4eHgoODpZhGDm28PBw2e32HJevW7fOtJ0HH3xQV65cUb9+/Uz9ISEhio+PN/U9++yzysjIkJ+fn6Ovbdu2ysjIULFixUxj9+/fr//85z93+KgAAAAAAIAshCiSGjdurNWrV2vZsmUKCgpS37591bt3b02dOlVbtmxRQECAAgIC1LhxY8f4rL7Fixebft6yZYsmTpzo+Llr166mbfXo0UOSFBoaaurftm2bSpYsqRo1ajj6WrduLS8vLz3xxBOmWn///XddunTJ0Ve3bl3VrFlTnTp1UpEiRe748QEAAAAAAIQokqTx48fr888/19SpUxUdHa1169bp+eefV//+/VW5cmXFxMQoJiZG586dkySdO3fO0ZeWlqbz5887fr58+bKSk5MdP994dklISIimTZumVq1aqVy5co7+48ePKzY21hHUSNdClCNHjig4ONjR16hRI23dutVpzhUrVujChQvq2LHj3ThEAAAAAAAUeAU+RClZsqRatGihuXPnmvp37dqlffv23dFQonLlymratKkmTZqkvXv3qnv37qbl27ZtU6NGjSRJDzzwgOrUqaMpU6Y4hSjbtm0zrRcSEqKIiAgtX77c6QwXAAAAAABwZxT4EKVq1aoqVKiQjh075rTMbrerWrVqd2xboaGh2rlzp06dOqWIiAinwGPr1q2OM1FatWql33//XUuXLlXDhg1VrFgxlStXTpUqVTKdidKkSRNVrFhRK1euVEREhDp06KDixYvfsZoBAAAAAMA1BT5Eybppa1JSktOy+Ph4lShR4o5tK+uMEUmKiIhQ8+bNVbFiRcfybdu2qX79+vL09FTr1q21du1axcbG6vDhw2revLkaNWqkixcv6uDBg6Y5161bp4SEBG3ZskUJCQnq3LnzHasZAAAAAABcU+BDlJSUFEnXLuu5kb+/f7bhyq2oUaOGGjRo4AhRDhw4oEOHDikkJMQx5pdfflHRokVVp04dtW7dWpGRkZKkdevWKTg4WI0aNdL27dtN8/bo0cMxp2EY+uabb7ikBwAAAACAu6DAhyjR0dG6evWqqlev7rTMZrPJbrffke1kBRt79uxRamqqUlNTVaNGDVPgkZiYqIMHD6pjx44KDAzUxo0bJf0vRGnQoIHpUp4WLVqoYsWKmjZtmmPOfv366amnnso2FAIAAAAAALeuwIco58+f19atW9WnTx9Tf506dVS3bl2tWrXqjmwnJCREH330kerVq+do7du3V6NGjRQYGOgYt3XrVvXq1Uvbt293nCWzYcMGNWjQQHXq1DGFKCEhIYqMjFTdunUdcz766KOKi4tTt27d7kjdAAAAAADgmgIfokjS0KFD1adPHw0ZMkQ2m00tWrTQokWL9Nlnn+nw4cO3Pf8jjzyioKAgzZ49W1FRUY72448/6sCBA6azUbZt26agoCDHpTySdOHCBR05ckQ1atRwPJnH09NTzz33nL788kvTnFFRUTylBwAAAACAu4AQRdJPP/2kjh07qmvXrtq/f7/mz5+vZcuW6bXXXrsj84eEhGjPnj3ZPgFo6dKlTiGKJK1du9Y0bt26dTp8+LDi4uIkSa1bt1aZMmX0zTffZDtnq1atVK5cuTtSPwAAAAAAkDwMwzDytIKHx92qBfeBOt+lu7oEt/bvFmdcXYJbC/S9MzdyLqi8lOHqEtxa8fQ7cw+sAiv5uKsrcH8XD7i6AvdWsb2rK3BrGT7849vtSPXwc3UJbu37M2VdXYJbG7XnAVeX4Nb2tffJ03jORAEAAAAAALCAEAUAAAAAAMACQhQAAAAAAAALCFEAAAAAAAAsIEQBAAAAAACwgBAFAAAAAADAAkIUAAAAAAAACwhRAAAAAAAALCBEAQAAAAAAsIAQBQAAAAAAwAJCFAAAAAAAAAsIUQAAAAAAACzwcnUBuL8El7/k6hLc2pbYoq4uwa1Vr5ro6hJQkGWmuboC95Zkd3UF7q/UI66uwK1d9Snl6hLcWoYKu7oEtxZ3xc/VJbi16iUyXF0CYBlnogAAAAAAAFhAiAIAAAAAAGABIQoAAAAAAIAFhCgAAAAAAAAWEKIAAAAAAABYQIgCAAAAAABgASEKAAAAAACABYQoAAAAAAAAFhCiAAAAAAAAWECIAgAAAAAAYEG+CFHsdruSk5Pl6+ub43LDMBw/BwUF6bvvvlNCQoJOnz6tiRMnqnDhwo7l4eHhysjI0KOPPmqax2azyTAM2Ww2hYeHyzCMHFtwcLCCg4NNfefOndPKlStVo0YNx5zr1q3Ldv2//e1vpm03bdpUhmGoTZs22e5feHj4LR07AAAAAABgTb4IUSTJ19dXXbt2depv2bKlqlSp4vjZZrNp48aN+vXXX1W/fn116dJFzZs315IlS0zreXl5adq0aTlub/HixQoICFBAQIBju1k/BwQEaMuWLY6xWX3BwcG6ePGiIiMjVbRoUcfyiRMnmtYNCAjQvHnzTNsLDQ1VZmamQkND83RcAAAAAADAnZFvQpTDhw8rLCzMqT8sLExRUVGOn//xj39o48aNGjFihI4dO6bt27erS5cuateunVq1auUYl5ycrObNm6tHjx7Zbi8tLU0xMTGKiYlRXFycJDl+jomJUUZGhmNsVt/+/fvVr18/FStWTB07djRt6/p1Y2JilJaW5lju4eGh7t27a9q0aeratau8vLxu+TgBAAAAAIBbk29ClIULF6p169YKCAhw9Pn4+Oi5557TggULHH3PPPOM5s6da1o3JiZGP/zwgzp16uToO3/+vKZNm6YJEyaYzhq5XZcvX9bRo0dVtWpVy+u0aNFC/v7+eu+993TlyhW1a9fujtUDAAAAAACsyTchit1u1/bt29WzZ09H3zPPPKPExERt2rRJklSqVCmVKVNGx44dy3b9atWqmfpGjRolb29vDR8+/I7V6enpqUqVKunChQuW1wkNDdX333+vlJQUrVixgkt6AAAAAABwgXwTokjS/PnzTZf0hIWFaeHChY6byvr5+UmSEhMTndaNj49XiRIlTH1JSUl65513NGTIENN9VW5V0aJFNW7cOPn6+mrFihWO/uHDhyspKcnR1q5d61jm6empbt26KSIiQpIUERGhTp06qUiRIrddDwAAAAAAsC5fhShLlixRUFCQ6tSpo1KlSql9+/amS3lSUlIkXTsj5Ub+/v5KSkpy6v/3v/+t3bt3a/Lkybdc1/UBSe/evdWlSxedP3/esfxf//qX6tWr52i9e/d2LGvTpo1KlCihVatWSZIiIyNlGIbpnioAAAAAAODuy1chSlxcnFavXq1evXopJCREBw4c0L59+0zLExISVL16dad1bTab7HZ7tvMOGDBAnTp1uuV7kWSFI8OGDZO3t7d2797tVPfRo0cd7fTp045loaGh8vHxUWxsrFJTU5WYmCg/Pz8u6QEAAAAA4B7LVyGKdO2Snp49e6pXr16aP3++aZlhGFq1apX69Olj6i9durSefvpprVy5Mts5d+zYoS+++EJjxoy5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",
50 | "text/plain": [
51 | ""
52 | ]
53 | },
54 | "metadata": {},
55 | "output_type": "display_data"
56 | }
57 | ],
58 | "source": [
59 | "cities = [\n",
60 | " \"TORONTO\",\n",
61 | " \"VANCOUVER\",\n",
62 | " \"HALIFAX\",\n",
63 | " \"CALGARY\",\n",
64 | " \"OTTAWA\",\n",
65 | " \"MONTREAL\",\n",
66 | " \"WINNIPEG\",\n",
67 | " \"EDMONTON\",\n",
68 | " \"LONDON\",\n",
69 | " \"ST. JONES\",\n",
70 | "]\n",
71 | "months = [\n",
72 | " \"JAN\",\n",
73 | " \"FEB\",\n",
74 | " \"MAR\",\n",
75 | " \"APR\",\n",
76 | " \"MAY\",\n",
77 | " \"JUN\",\n",
78 | " \"JUL\",\n",
79 | " \"AUG\",\n",
80 | " \"SEP\",\n",
81 | " \"OCT\",\n",
82 | " \"NOV\",\n",
83 | " \"DEC\",\n",
84 | "]\n",
85 | "\n",
86 | "data = np.random.random((10, 12)) + np.abs(np.arange(12) - 5.5)\n",
87 | "data = (1 - data / (np.max(data)))\n",
88 | "\n",
89 | "\n",
90 | "d = pd.DataFrame(data, columns=months, index=cities).round(2)\n",
91 | "\n",
92 | "fig, ax = plt.subplots(figsize=(14, 5))\n",
93 | "\n",
94 | "\n",
95 | "column_definitions = [\n",
96 | " ColDef(name, cmap=cmap, formatter=lambda x: \"\") for name in months\n",
97 | "] + [ColDef(\"index\", title=\"\", width=1.5, textprops={\"ha\": \"right\"})]\n",
98 | "\n",
99 | "tab = Table(\n",
100 | " d,\n",
101 | " column_definitions=column_definitions,\n",
102 | " row_dividers=False,\n",
103 | " col_label_divider=False,\n",
104 | " textprops={\"ha\": \"center\", \"fontname\": \"Roboto\"},\n",
105 | " cell_kw={\n",
106 | " \"edgecolor\": \"w\",\n",
107 | " \"linewidth\": 0,\n",
108 | " },\n",
109 | ")\n",
110 | "\n",
111 | "\n",
112 | "tab.col_label_row.set_facecolor(\"k\")\n",
113 | "tab.col_label_row.set_fontcolor(\"w\")\n",
114 | "tab.columns[\"index\"].set_facecolor(\"k\")\n",
115 | "tab.columns[\"index\"].set_fontcolor(\"w\")\n",
116 | "tab.columns[\"index\"].set_linewidth(0)\n",
117 | "\n",
118 | "\n",
119 | "plt.show()\n",
120 | "\n",
121 | "\n",
122 | "fig.savefig(\"images/calender.png\", dpi=200)"
123 | ]
124 | },
125 | {
126 | "cell_type": "code",
127 | "execution_count": null,
128 | "id": "70cdae3a-37ca-418a-849c-5d29c4b7b9da",
129 | "metadata": {},
130 | "outputs": [],
131 | "source": []
132 | }
133 | ],
134 | "metadata": {
135 | "kernelspec": {
136 | "display_name": "Python 3.10.5 ('env': venv)",
137 | "language": "python",
138 | "name": "python3"
139 | },
140 | "language_info": {
141 | "codemirror_mode": {
142 | "name": "ipython",
143 | "version": 3
144 | },
145 | "file_extension": ".py",
146 | "mimetype": "text/x-python",
147 | "name": "python",
148 | "nbconvert_exporter": "python",
149 | "pygments_lexer": "ipython3",
150 | "version": "3.10.5"
151 | },
152 | "vscode": {
153 | "interpreter": {
154 | "hash": "fad163352f6b6c4f05b9b8d41b1f28c58b235e61ec56c8581176f01128143b49"
155 | }
156 | }
157 | },
158 | "nbformat": 4,
159 | "nbformat_minor": 5
160 | }
161 |
--------------------------------------------------------------------------------