├── 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": "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", 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 | --------------------------------------------------------------------------------