diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9df8c1b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: 🐛 Bug Report +about: Report a bug +# based on matplotlib issue template +--- + + + + +### Bug report + +**Bug summary** + + + +**Code for reproduction** + + + +**Expected outcome** + + + + +**Version Info** + + * Operating system: + * Matplotlib version: + * Matplotlib backend (`print(matplotlib.get_backend())`): + * Python version: + * Jupyter version (if applicable): + * Other libraries: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..a934fdb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,6 @@ +# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser +blank_issues_enabled: true # default +contact_links: + - name: ❓ Question/Support/Other + url: https://discourse.matplotlib.org/c/3rdparty/18 + about: If you have a usage question diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..12e5a52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,29 @@ +--- +name: 📖 Documentation improvement +about: Report parts of the docs that are wrong or unclear +labels: documentation, bug +--- + + + + +### Problem + + + + +### Suggested Improvement + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..03e327a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,35 @@ +--- +name: 🚀 Enhancement/Feature Request +about: Suggest something that could be improved or a New Feature to add +labels: enhancement +--- + + + +### Problem + + + +### Proposed Solution + + + +### Additional context + + + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0e61f94 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: psf/black@stable + with: + options: "--check" + src: "." + - name: docstrings + run: | + pip install flit + pushd $(mktemp -d) + git clone https://github.com/Carreau/velin.git --single-branch --depth 1 + cd velin + flit install + popd + velin . --check diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..494b409 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,28 @@ +# heavily based on https://github.com/jupyterlab/jupyterlab-git/blob/v0.22.2/.github/workflows/publish.yml +name: Publish Package + +on: + release: + types: [published] + + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install packaging setuptools twine wheel build + - name: Publish the Python package + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m build -s -w + twine upload dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..1c843dd --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: "0 16 * * 1" # monday at noon est + +jobs: + test: + name: Python ${{ matrix.python-version }} - mpl ${{ matrix.mpl-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.8.x', '3.9.x'] + mpl-version: ['3.4', 'latest'] + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: pip cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + + - if: matrix.mpl-version=='latest' + name: Install dev Matplotlib + run: pip install git+https://github.com/matplotlib/matplotlib.git + + - if: matrix.mpl-version!='latest' + name: Install matplotlib pinned + run: pip install --upgrade --pre --index-url https://pypi.anaconda.org/scipy-wheels-nightly/simple --extra-index-url https://pypi.org/simple matplotlib + + - name: Install + run: | + pip install ".[test]" + + - name: Tests + run: | + pytest --mpl --color=yes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..49df861 --- /dev/null +++ b/.gitignore @@ -0,0 +1,155 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# mac stuff +.DS_store + +# editors + +## vim +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] +[._]*.un~ + +## vscode +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b579aec --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Jae-Joon Lee +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a81361 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# mpl-visual-context + +A 3rd party package for Matplotlib + +## Installation + +You can install using `pip`: + +```bash +pip install mpl_visual_context +``` + +## Development Installation + + +```bash +pip install -e ".[dev]" +``` + diff --git a/mpl_visual_context/__init__.py b/mpl_visual_context/__init__.py new file mode 100644 index 0000000..1f24ebd --- /dev/null +++ b/mpl_visual_context/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Jae-Joon Lee. +# Distributed under the terms of the Modified BSD License. + +# Must import __version__ first to avoid errors importing this file during the build process. +# See https://github.com/pypa/setuptools/issues/1724#issuecomment-627241822 +from ._version import __version__ + +from .example import example_function diff --git a/mpl_visual_context/_version.py b/mpl_visual_context/_version.py new file mode 100644 index 0000000..7466fea --- /dev/null +++ b/mpl_visual_context/_version.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# coding: utf-8 + +# Copyright (c) Jae-Joon Lee. +# Distributed under the terms of the Modified BSD License. + +version_info = (0, 1, 0) +__version__ = ".".join(map(str, version_info)) diff --git a/mpl_visual_context/patheffects.py b/mpl_visual_context/patheffects.py new file mode 100644 index 0000000..fe1881c --- /dev/null +++ b/mpl_visual_context/patheffects.py @@ -0,0 +1,134 @@ +from matplotlib import patches as mpatches +from matplotlib.patheffects import AbstractPathEffect + +import numpy as np +import matplotlib.colors as mcolors +import colorsys + +def set_hls(c, dh=0, dl=0, ds=0, dalpha=0): + """ + c : (array -like, str) color in RGB space + dh : (float) change in Hue + default = 0 + dl : (float) change in Lightness + default = 0 + ds : (float) change in Saturation + default = 0 + """ + c_rgba = mcolors.to_rgba(c) + + c_rgb = c_rgba[:3] + alpha = c_rgba[3] + + c_hls = colorsys.rgb_to_hls(*c_rgb) + h = c_hls[0] + dh + l = np.clip(c_hls[1] + dl, 0, 1) + s = np.clip(c_hls[2] + ds, 0, 1) + + c_rgb_new = colorsys.hls_to_rgb(h, l, s) + alpha = np.clip(alpha+dalpha, 0, 1) + + return np.append(c_rgb_new, alpha) + + +class PathPatchEffect(AbstractPathEffect): + """ + Draws a `.PathPatch` instance whose Path comes from the original + PathEffect artist. + """ + + def __init__(self, offset=(0, 0), **kwargs): + """ + Parameters + ---------- + offset : (float, float), default: (0, 0) + The (x, y) offset to apply to the path, in points. + **kwargs + All keyword arguments are passed through to the + :class:`~matplotlib.patches.PathPatch` constructor. The + properties which cannot be overridden are "path", "clip_box" + "transform" and "clip_path". + """ + super().__init__(offset=offset) + self.patch = mpatches.PathPatch([], **kwargs) + + def draw_path(self, renderer, gc, tpath, affine, rgbFace): + self.patch._path = tpath + self.patch.set_transform(affine + self._offset_transform(renderer)) + self.patch.set_clip_box(gc.get_clip_rectangle()) + clip_path = gc.get_clip_path() + if clip_path: + self.patch.set_clip_path(*clip_path) + self.patch.draw(renderer) + + +class Stroke(AbstractPathEffect): + """A line based PathEffect which re-draws a stroke.""" + + def __init__(self, offset=(0, 0), **kwargs): + """ + The path will be stroked with its gc updated with the given + keyword arguments, i.e., the keyword arguments should be valid + gc parameter values. + """ + super().__init__(offset) + self._gc = kwargs + + def draw_path(self, renderer, gc, tpath, affine, rgbFace): + """Draw the path with updated gc.""" + gc0 = renderer.new_gc() # Don't modify gc, but a copy! + gc0.copy_properties(gc) + gc0 = self._update_gc(gc0, self._gc) + renderer.draw_path( + gc0, tpath, affine + self._offset_transform(renderer), rgbFace) + gc0.restore() + +class ColorModifyStroke(AbstractPathEffect): + """A line based PathEffect which re-draws a stroke.""" + + def __init__(self, dh=0, dl=0, ds=0, dalpha=0): + """ + The path will be stroked with its gc updated with the given + keyword arguments, i.e., the keyword arguments should be valid + gc parameter values. + """ + super().__init__() + self.dh = dh + self.dl = dl + self.ds = ds + self.dalpha = dalpha + + def draw_path(self, renderer, gc, tpath, affine, rgbFace): + """Draw the path with updated gc.""" + gc0 = renderer.new_gc() + gc0.copy_properties(gc) + + # change the stroke color + rgb = set_hls(gc0.get_rgb(), self.dh, self.dl, self.ds) + gc0.set_foreground(rgb) + + # chage the fill color + if rgbFace is not None: + rgbFace = set_hls(rgbFace, self.dh, self.dl, self.ds) + renderer.draw_path( + gc0, tpath, affine, rgbFace) + # gc0.restore() + + +def main(): + import matplotlib.pyplot as plt + import seaborn as sns + df_peng = sns.load_dataset("penguins") + + fig, ax = plt.subplots(figsize=(5, 3), constrained_layout=True, clear=True) + sns.countplot(y="species", data=df_peng, ax=ax) + + pe = [ColorModifyStroke(ds=-0.2, dl=0.4)] + + p = ax.patches[0] + p.set_ec("k") + p.set_path_effects(pe) + p = ax.patches[1] + p.set_ec("k") + p = ax.patches[2] + p.set_path_effects(pe) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..427bbc8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.black] +skip-string-normalization = true + +[tool.pytest.ini_options] +testpaths = [ + "mpl_visual_context/tests" +] + +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/readthedocs.yml b/readthedocs.yml new file mode 100644 index 0000000..e346d9b --- /dev/null +++ b/readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 +python: + version: 3.8 + install: + - method: pip + path: . + extra_requirements: + - doc + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b572120 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,39 @@ +[metadata] +name=mpl_visual_context +author=Jae-Joon Lee +author_email=lee.j.joon@gmail.com +url = https://github.com//mpl-visual-context +license=BSD 3-Clause +long_description = file: README.md +long_description_content_type = text/markdown + +[options] +packages = find: + +install_requires = + matplotlib + +classifiers = + Intended Audience :: Developers + Intended Audience :: Science/Research + License :: OSI Approved :: BSD License + Programming Language :: Python + Programming Language :: Python :: 3 + Framework :: Matplotlib + +[options.extras_require] +test = + black + pytest + pytest-mpl +doc = + sphinx + numpydoc + sphinx_rtd_theme + sphinx-copybutton + sphinx-autobuild + sphinx_gallery>=0.8.2 + autoapi +dev = + %(test)s + %(doc)s diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b1b317f --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from os import path + +from setuptools import find_packages, setup + +# extract version +path = path.realpath("mpl_visual_context/_version.py") +version_ns = {} +with open(path, encoding="utf8") as f: + exec(f.read(), {}, version_ns) +version = version_ns["__version__"] + +setup_args = dict( + version=version, +) + +if __name__ == "__main__": + setup(**setup_args)