From 5e28e9aeeaf2a26e347f5fc092f93a4da09fb6c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20G=C3=BCtzkow?= Date: Tue, 18 Feb 2020 22:38:31 +0100 Subject: [PATCH 01/47] Fix crash when the file save dialog is cancelled The filename is an empty string when the file save dialog is cancelled, which results in an attempt to open a non-existent file in `ViewerWindow`'s `save()`, `EditorWindow`'s `export()` and `save()`. Since the error isn't caught viscm terminates. This fix checks if the returned string is empty and if it is, no file I/O operation is performed. --- viscm/gui.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/viscm/gui.py b/viscm/gui.py index caaa746..1bd0c86 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -1107,7 +1107,8 @@ def save(self): caption="Save file", directory=self.cmapname + ".png", filter="Image Files (*.png *.jpg *.bmp)") - self.viscm.save_figure(fileName) + if fileName: + self.viscm.save_figure(fileName) class EditorWindow(QtWidgets.QMainWindow): @@ -1296,7 +1297,8 @@ def export(self): caption="Export file", directory=self.viscm_editor.name + ".py", filter=".py (*.py)") - self.viscm_editor.export_py(fileName) + if fileName: + self.viscm_editor.export_py(fileName) def fileQuit(self): self.close() @@ -1309,7 +1311,8 @@ def save(self): caption="Save file", directory=self.viscm_editor.name + ".jscm", filter="JSCM Files (*.jscm)") - self.viscm_editor.save_colormap(fileName) + if fileName: + self.viscm_editor.save_colormap(fileName) def loadviewer(self): newfig = plt.figure() From b0516307330b35833cfac9f7b3c5ba683c555a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 9 Jun 2022 14:16:45 +0200 Subject: [PATCH 02/47] Fix import of qt backend for matplotlib without qt4 support --- viscm/gui.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/viscm/gui.py b/viscm/gui.py index 1bd0c86..c769108 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -16,14 +16,11 @@ # Do this first before any other matplotlib imports, to force matplotlib to # use a Qt backend from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui, _getSaveFileName -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas4 -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas5 -def FigureCanvas(fig): - try: - return FigureCanvas5(fig) - except Exception: - return FigureCanvas4(fig) +try: + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas +except Exception: + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas import matplotlib import matplotlib.pyplot as plt From dc18503589d9833dfb5866bdf3225260fc269ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 9 Jun 2022 14:17:01 +0200 Subject: [PATCH 03/47] Add missing scipy depedency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index df0caf2..796b780 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,6 @@ "Programming Language :: Python :: 3", ], packages=find_packages(), - install_requires=["numpy", "matplotlib", "colorspacious"], + install_requires=["numpy", "matplotlib", "colorspacious", "scipy"], package_data={'viscm': ['examples/*']}, ) From 4e7138f036cb2624f875b7ce07a84e524f0b149f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20N=C3=B6the?= Date: Thu, 14 Jul 2022 15:58:49 +0200 Subject: [PATCH 04/47] Update viscm/gui.py Co-authored-by: Thomas A Caswell --- viscm/gui.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/viscm/gui.py b/viscm/gui.py index c769108..ecd3bc6 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -18,9 +18,12 @@ from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui, _getSaveFileName try: - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -except Exception: - from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas +except ImportError: + try: + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas + except ImportError: + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas import matplotlib import matplotlib.pyplot as plt From 0a7ea27ee2672c03d30273c7f5ec88d3cbcaa154 Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Mon, 17 Oct 2022 13:41:20 -0700 Subject: [PATCH 05/47] Add instructions on how to reproduce viridis --- README.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.rst b/README.rst index 790594a..f7dc601 100644 --- a/README.rst +++ b/README.rst @@ -30,3 +30,15 @@ Dependencies: License: MIT, see LICENSE.txt for details. + +Reproducing viridis +------------------- +Load [viridis AKA option_d.py](https://github.com/BIDS/colormap/) using: + +``` +python -m viscm --uniform-space buggy-CAM02-UCS -m Bezier edit /tmp/option_d.py +``` + +Note that there was a small bug in the assumed sRGB viewing conditions +while designing viridis. It does not affect the outcome by much. Also +see `python -m viscm --help`. From d309d5132870bb2cb5785edd72f02dcd9d7ae2e0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sat, 15 Apr 2023 23:13:31 +0200 Subject: [PATCH 06/47] Fix compatibility with Matplotlib 3.7 and Qt6. - enum scoping change. - exec vs. exec_. - Matplotlib no longer exports _getSaveFileName (only PyQt4 required a separate _getSaveFileName because it called that function getSaveFileNameAndFilter instead; let's not try to support qt4 anymore). Also, PyQt and PySide use different parameter names for this function; pass parameters positionally instead. - addAction parameter order changed between qt5 and qt6, but we can call it in a way that's compatible with everyone by using setShortcut instead. - QAction moved between qt5 and qt6 but we don't need to explicitly instantiate it; we can use addAction instead to do so. --- README.rst | 1 + viscm/bezierbuilder.py | 11 +++-- viscm/gui.py | 100 ++++++++++++++++++----------------------- 3 files changed, 52 insertions(+), 60 deletions(-) diff --git a/README.rst b/README.rst index 790594a..7ba636a 100644 --- a/README.rst +++ b/README.rst @@ -27,6 +27,7 @@ Dependencies: * `colorspacious `_ * Matplotlib * NumPy + * one of PyQt6, PySide6, PyQt5 (>=5.13.1), or PySide2 License: MIT, see LICENSE.txt for details. diff --git a/viscm/bezierbuilder.py b/viscm/bezierbuilder.py index 71c25f0..a97b453 100644 --- a/viscm/bezierbuilder.py +++ b/viscm/bezierbuilder.py @@ -42,6 +42,10 @@ from matplotlib.backends.qt_compat import QtGui, QtCore from .minimvc import Trigger + +Qt = QtCore.Qt + + class ControlPointModel(object): def __init__(self, xp, yp, fixed=None): # fixed is either None (if no point is fixed) or and index of a fixed @@ -113,12 +117,13 @@ def on_button_press(self, event): if event.inaxes != self.ax: return res, ind = self.control_polygon.contains(event) - if res and modkey == QtCore.Qt.NoModifier: + if res and modkey == Qt.KeyboardModifier.NoModifier: self._index = ind["ind"][0] - if res and (modkey == QtCore.Qt.ControlModifier or self.mode == "remove"): + if res and (modkey == Qt.KeyboardModifier.ControlModifier + or self.mode == "remove"): # Control-click deletes self.control_point_model.remove_point(ind["ind"][0]) - if (modkey == QtCore.Qt.ShiftModifier or self.mode == "add"): + if (modkey == Qt.KeyboardModifier.ShiftModifier or self.mode == "add"): # Adding a new point. Find the two closest points and insert it in # between them. diff --git a/viscm/gui.py b/viscm/gui.py index ecd3bc6..5a8a547 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -15,7 +15,7 @@ #matplotlib.rcParams['backend'] = "QT4AGG" # Do this first before any other matplotlib imports, to force matplotlib to # use a Qt backend -from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui, _getSaveFileName +from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui try: from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas @@ -39,6 +39,9 @@ from .minimvc import Trigger +Qt = QtCore.Qt + + # The correct L_A value for the standard sRGB viewing conditions is: # (64 / np.pi) / 5 # Due to an error in our color conversion code, the matplotlib colormaps were @@ -1033,8 +1036,8 @@ def main(argv): if args.quit: sys.exit() - figureCanvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + figureCanvas.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) figureCanvas.updateGeometry() mainwindow.resize(800, 600) @@ -1050,7 +1053,7 @@ def main(argv): import signal signal.signal(signal.SIGINT, signal.SIG_DFL) - app.exec_() + (getattr(app, "exec", None) or getattr(app, "exec_"))() def about(): QtWidgets.QMessageBox.about(None, "VISCM", @@ -1061,19 +1064,17 @@ def about(): class ViewerWindow(QtWidgets.QMainWindow): def __init__(self, figurecanvas, viscm, cmapname, parent=None): QtWidgets.QMainWindow.__init__(self, parent) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.main_widget = QtWidgets.QWidget(self) self.cmapname = cmapname file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu.addAction('&Save', self.save).setShortcut('Ctrl+S') + file_menu.addAction('&Quit', self.fileQuit).setShortcut('Ctrl+Q') options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Toggle Gamut', self.toggle_gamut, - QtCore.Qt.CTRL + QtCore.Qt.Key_G) + options_menu.addAction( + '&Toggle Gamut', self.toggle_gamut).setShortcut('Ctrl+G') help_menu = QtWidgets.QMenu('&Help', self) help_menu.addAction('&About', about) @@ -1103,9 +1104,8 @@ def closeEvent(self, ce): self.fileQuit() def save(self): - fileName, _ = _getSaveFileName( - caption="Save file", - directory=self.cmapname + ".png", + fileName, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "Save file", self.cmapname + ".png", filter="Image Files (*.png *.jpg *.bmp)") if fileName: self.viscm.save_figure(fileName) @@ -1114,19 +1114,17 @@ def save(self): class EditorWindow(QtWidgets.QMainWindow): def __init__(self, figurecanvas, viscm_editor, parent=None): QtWidgets.QMainWindow.__init__(self, parent) - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self.viscm_editor = viscm_editor file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) + file_menu.addAction('&Save', self.save).setShortcut('Ctrl+S') file_menu.addAction("&Export .py", self.export) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu.addAction('&Quit', self.fileQuit).setShortcut('Ctrl+Q') options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Load in Viewer', self.loadviewer, - QtCore.Qt.CTRL + QtCore.Qt.Key_V) + options_menu.addAction( + '&Load in Viewer', self.loadviewer).setShortcut('Ctrl+V') help_menu = QtWidgets.QMenu('&Help', self) help_menu.addAction('&About', about) @@ -1138,21 +1136,23 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.main_widget = QtWidgets.QWidget(self) - self.max_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) + self.max_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal) self.max_slider.setMinimum(0) self.max_slider.setMaximum(100) self.max_slider.setValue(viscm_editor.max_Jp) - self.max_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) + self.max_slider.setTickPosition( + QtWidgets.QSlider.TickPosition.TicksBelow) self.max_slider.setTickInterval(10) self.max_slider.valueChanged.connect(self.updatejp) self.max_slider_num = QtWidgets.QLabel(str(viscm_editor.max_Jp)) self.max_slider_num.setFixedWidth(30) - self.min_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) + self.min_slider = QtWidgets.QSlider(Qt.Orientation.Horizontal) self.min_slider.setMinimum(0) self.min_slider.setMaximum(100) self.min_slider.setValue(viscm_editor.min_Jp) - self.min_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) + self.min_slider.setTickPosition( + QtWidgets.QSlider.TickPosition.TicksBelow) self.min_slider.setTickInterval(10) self.min_slider.valueChanged.connect(self.updatejp) self.min_slider_num = QtWidgets.QLabel(str(viscm_editor.min_Jp)) @@ -1176,7 +1176,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): mainlayout.addLayout(min_slider_layout) if viscm_editor.cmtype == "diverging": - smoothness_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) + smoothness_slider = QtWidgets.QSlider(Qt.Horizontal) # We want the slider to vary filter_k exponentially between 5 and # 1000. So it should go from [log10(5), log10(1000)] # which is about [0.699, 3.0] @@ -1193,7 +1193,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): metrics = QtGui.QFontMetrics(smoothness_slider_num.font()) max_width = metrics.width("1000.00") smoothness_slider_num.setFixedWidth(max_width) - smoothness_slider_num.setAlignment(QtCore.Qt.AlignRight) + smoothness_slider_num.setAlignment(Qt.AlignRight) self.smoothness_slider_num = smoothness_slider_num smoothness_slider_layout = QtWidgets.QHBoxLayout() @@ -1208,42 +1208,30 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): viscm_editor.cmap_model.filter_k_trigger.add_callback( self.update_smoothness_slider) - self.moveAction = QtWidgets.QAction("Drag points", self) + self.toolbar = self.addToolBar('Tools') + self.moveAction = self.toolbar.addAction("Drag points") self.moveAction.triggered.connect(self.set_move_mode) self.moveAction.setCheckable(True) - - self.addAction = QtWidgets.QAction("Add points", self) + self.addAction = self.toolbar.addAction("Add points") self.addAction.triggered.connect(self.set_add_mode) self.addAction.setCheckable(True) - - self.removeAction = QtWidgets.QAction("Remove points", self) + self.removeAction = self.toolbar.addAction("Remove points") self.removeAction.triggered.connect(self.set_remove_mode) self.removeAction.setCheckable(True) - - self.swapAction = QtWidgets.QAction("Flip brightness", self) + self.toolbar.addSeparator() + self.swapAction = self.toolbar.addAction("Flip brightness") self.swapAction.triggered.connect(self.swapjp) - renameAction = QtWidgets.QAction("Rename colormap", self) + self.toolbar.addSeparator() + renameAction = self.toolbar.addAction("Rename colormap") renameAction.triggered.connect(self.rename) - - saveAction = QtWidgets.QAction('Save', self) + saveAction = self.toolbar.addAction("Save") saveAction.triggered.connect(self.save) - - self.toolbar = self.addToolBar('Tools') - self.toolbar.addAction(self.moveAction) - self.toolbar.addAction(self.addAction) - self.toolbar.addAction(self.removeAction) - self.toolbar.addSeparator() - self.toolbar.addAction(self.swapAction) - self.toolbar.addSeparator() - self.toolbar.addAction(renameAction) - self.toolbar.addAction(saveAction) - self.moveAction.setChecked(True) self.main_widget.setFocus() figurecanvas.setFocus() - figurecanvas.setFocusPolicy(QtCore.Qt.StrongFocus) + figurecanvas.setFocusPolicy(Qt.FocusPolicy.StrongFocus) self.setCentralWidget(self.main_widget) def rename(self): @@ -1293,9 +1281,8 @@ def set_remove_mode(self): self.viscm_editor.bezier_builder.mode = "remove" def export(self): - fileName, _ = _getSaveFileName( - caption="Export file", - directory=self.viscm_editor.name + ".py", + fileName, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "Export file", self.viscm_editor.name + ".py", filter=".py (*.py)") if fileName: self.viscm_editor.export_py(fileName) @@ -1307,9 +1294,8 @@ def closeEvent(self, ce): self.fileQuit() def save(self): - fileName, _ = _getSaveFileName( - caption="Save file", - directory=self.viscm_editor.name + ".jscm", + fileName, _ = QtWidgets.QFileDialog.getSaveFileName( + self, "Save file", self.viscm_editor.name + ".jscm", filter="JSCM Files (*.jscm)") if fileName: self.viscm_editor.save_colormap(fileName) @@ -1320,8 +1306,8 @@ def loadviewer(self): cm = self.viscm_editor.show_viscm() v = viscm(cm, name=self.viscm_editor.name, figure=newfig) - newcanvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + newcanvas.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding) newcanvas.updateGeometry() newwindow = ViewerWindow(newcanvas, v, self.viscm_editor.name, parent=self) From 7b68e791de4afc88df1b058352dbac99b34560e4 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 20 May 2023 18:53:08 -0600 Subject: [PATCH 07/47] Drop support for Python 2 --- README.rst | 2 +- setup.cfg | 3 --- setup.py | 6 ++---- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 790594a..0339be0 100644 --- a/README.rst +++ b/README.rst @@ -23,7 +23,7 @@ Contact: Nathaniel J. Smith and Stéfan van der Walt Dependencies: - * Python 2.6+, or 3.3+ + * Python 3.7+ * `colorspacious `_ * Matplotlib * NumPy diff --git a/setup.cfg b/setup.cfg index 5c6311d..0c9e0fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,2 @@ -[bdist_wheel] -universal=1 - [metadata] license_file = LICENSE diff --git a/setup.py b/setup.py index 796b780..de84229 100644 --- a/setup.py +++ b/setup.py @@ -6,24 +6,22 @@ # Must be one line or PyPI will cut it off DESC = ("A colormap tool") - LONG_DESC = open("README.rst").read() setup( name="viscm", - version="0.9", + version="0.10", description=DESC, long_description=LONG_DESC, author="Nathaniel J. Smith, Stefan van der Walt", author_email="njs@pobox.com, stefanv@berkeley.edu", - url="https://github.com/bids/viscm", + url="https://github.com/matplotlib/viscm", license="MIT", classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", ], packages=find_packages(), From 1df583877c4033f7bbad1a3089b453c1a8c1fc36 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 20 May 2023 20:45:47 -0600 Subject: [PATCH 08/47] Migrate to modern packaging toolchain --- doc/contributing.md | 23 ++++++++++++++++++++++ environment.yml | 12 ++++++++++++ pyproject.toml | 48 +++++++++++++++++++++++++++++++++++++++++++++ setup.cfg | 2 -- setup.py | 31 ++--------------------------- viscm/__init__.py | 1 - viscm/__main__.py | 3 +-- viscm/gui.py | 5 +++-- 8 files changed, 89 insertions(+), 36 deletions(-) create mode 100644 doc/contributing.md create mode 100644 environment.yml create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/doc/contributing.md b/doc/contributing.md new file mode 100644 index 0000000..fc5825b --- /dev/null +++ b/doc/contributing.md @@ -0,0 +1,23 @@ +# Contributing + +Install development dependencies: + +``` +conda env create # or `mamba env create` +``` + + +## Development install + +``` +pip install -e . +``` + + +## Testing the build + +``` +rm -rf dist +python -m build +pip install dist/*.whl # or `dist/*.tar.gz` +``` diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..33c0a90 --- /dev/null +++ b/environment.yml @@ -0,0 +1,12 @@ +name: "viscm" +channels: + - "conda-forge" + - "nodefaults" +dependencies: + - "python ~=3.11" + - "numpy ~=1.24" + - "matplotlib ~=3.7" + - "colorspacious ~=1.1" + - "scipy ~=1.10" + - pip: + - "build ~=0.10" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..308e631 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[project] +name = "viscm" +dynamic = ["version"] +description = "A colormap tool" +readme = "README.rst" +authors = [ + {name = "Nathaniel J. Smith", email = "njs@pobox.com"}, + {name = "Stefan van der Walt", email = "stefanv@berkeley.edu"}, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", +] + +requires-python = "~=3.7" +dependencies = [ + "numpy", + "matplotlib", + "colorspacious", + "scipy", +] + +[project.urls] +repository = "https://github.com/matplotlib/viscm" +# documentation = "https://viscm.readthedocs.io" + +[project.license] +text = "MIT" +files = ["LICENSE"] + +[project.scripts] +viscm = "viscm.gui:main" + + +[build-system] +requires = ["setuptools", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +zip-safe = false +packages = {find = {}} +package-data = {viscm = ["examples/*"]} + + +# [tool.black] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 0c9e0fc..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -license_file = LICENSE diff --git a/setup.py b/setup.py index de84229..d5d43d7 100644 --- a/setup.py +++ b/setup.py @@ -1,30 +1,3 @@ -from setuptools import setup, find_packages -import sys -import os.path +from setuptools import setup -import numpy as np - -# Must be one line or PyPI will cut it off -DESC = ("A colormap tool") -LONG_DESC = open("README.rst").read() - -setup( - name="viscm", - version="0.10", - description=DESC, - long_description=LONG_DESC, - author="Nathaniel J. Smith, Stefan van der Walt", - author_email="njs@pobox.com, stefanv@berkeley.edu", - url="https://github.com/matplotlib/viscm", - license="MIT", - classifiers = - [ "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", - ], - packages=find_packages(), - install_requires=["numpy", "matplotlib", "colorspacious", "scipy"], - package_data={'viscm': ['examples/*']}, -) +setup(use_scm_version=True) diff --git a/viscm/__init__.py b/viscm/__init__.py index 2152218..f51aa85 100644 --- a/viscm/__init__.py +++ b/viscm/__init__.py @@ -1,4 +1,3 @@ -# This file is part of pycam02ucs # Copyright (C) 2014 Nathaniel Smith # See file LICENSE.txt for license information. diff --git a/viscm/__main__.py b/viscm/__main__.py index db0cdb5..144eb2e 100644 --- a/viscm/__main__.py +++ b/viscm/__main__.py @@ -3,6 +3,5 @@ # Copyright (C) 2015 Stefan van der Walt # See file LICENSE.txt for license information. -import sys from .gui import main -main(sys.argv[1:]) +main() diff --git a/viscm/gui.py b/viscm/gui.py index ecd3bc6..5fabe0a 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -948,8 +948,9 @@ def load(self, path): self.name = path -def main(argv): +def main(): import argparse + argv = sys.argv[1:] # Usage: # python -m viscm @@ -1331,4 +1332,4 @@ def loadviewer(self): if __name__ == "__main__": - main(sys.argv[1:]) + main() From ca5a02814af8a6bf427ce7c5407c6670b177a470 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 20 May 2023 21:10:12 -0600 Subject: [PATCH 09/47] Extract CLI into new module --- pyproject.toml | 2 +- viscm/__main__.py | 4 +- viscm/cli.py | 111 ++++++++++++++++++++++++++++++++++++++++++++++ viscm/gui.py | 106 +------------------------------------------ 4 files changed, 115 insertions(+), 108 deletions(-) create mode 100644 viscm/cli.py diff --git a/pyproject.toml b/pyproject.toml index 308e631..1b53c90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ text = "MIT" files = ["LICENSE"] [project.scripts] -viscm = "viscm.gui:main" +viscm = "viscm.cli:cli" [build-system] diff --git a/viscm/__main__.py b/viscm/__main__.py index 144eb2e..f4b3e03 100644 --- a/viscm/__main__.py +++ b/viscm/__main__.py @@ -3,5 +3,5 @@ # Copyright (C) 2015 Stefan van der Walt # See file LICENSE.txt for license information. -from .gui import main -main() +from .cli import cli +cli() diff --git a/viscm/cli.py b/viscm/cli.py new file mode 100644 index 0000000..9960804 --- /dev/null +++ b/viscm/cli.py @@ -0,0 +1,111 @@ +import sys + +import matplotlib.pyplot as plt + +from . import gui + + +def cli(): + import argparse + argv = sys.argv[1:] + + # Usage: + # python -m viscm + # python -m viscm edit + # python -m viscm edit + # (file.py must define some appropriate globals) + # python -m viscm view + # (file.py must define a global named "test_cm") + # python -m viscm view "matplotlib builtin colormap" + # python -m viscm view --save=foo.png ... + + parser = argparse.ArgumentParser( + prog="python -m viscm", + description="A colormap tool.", + ) + parser.add_argument("action", metavar="ACTION", + help="'edit' or 'view' (or 'show', same as 'view')", + choices=["edit", "view", "show"], + default="edit", + nargs="?") + parser.add_argument("colormap", metavar="COLORMAP", + default=None, + help="A .json file saved from the editor, or " + "the name of a matplotlib builtin colormap", + nargs="?") + parser.add_argument("--uniform-space", metavar="SPACE", + default="CAM02-UCS", + dest="uniform_space", + help="The perceptually uniform space to use. Usually " + "you should leave this alone. You can pass 'CIELab' " + "if you're curious how uniform some colormap is in " + "CIELab space. You can pass 'buggy-CAM02-UCS' if " + "you're trying to reproduce the matplotlib colormaps " + "(which turn out to have had a small bug in the " + "assumed sRGB viewing conditions) from their bezier " + "curves.") + parser.add_argument("-t", "--type", type=str, + default="linear", choices=["linear", "diverging", "diverging-continuous"], + help="Choose a colormap type. Supported options are 'linear', 'diverging', and 'diverging-continuous") + parser.add_argument("-m", "--method", type=str, + default="CatmulClark", choices=["Bezier", "CatmulClark"], + help="Choose a spline construction method. 'CatmulClark' is the default, but you may choose the legacy option 'Bezier'") + parser.add_argument("--save", metavar="FILE", + default=None, + help="Immediately save visualization to a file " + "(view-mode only).") + parser.add_argument("--quit", default=False, action="store_true", + help="Quit immediately after starting " + "(useful with --save).") + args = parser.parse_args(argv) + + cm = gui.Colormap(args.type, args.method, args.uniform_space) + app = gui.QtWidgets.QApplication([]) + + if args.colormap: + cm.load(args.colormap) + + + # Easter egg! I keep typing 'show' instead of 'view' so accept both + if args.action in ("view", "show"): + if cm is None: + sys.exit("Please specify a colormap") + fig = plt.figure() + figureCanvas = gui.FigureCanvas(fig) + v = gui.viscm(cm.cmap, name=cm.name, figure=fig, uniform_space=cm.uniform_space) + mainwindow = gui.ViewerWindow(figureCanvas, v, cm.name) + if args.save is not None: + v.figure.set_size_inches(20, 12) + v.figure.savefig(args.save) + elif args.action == "edit": + if not cm.can_edit: + sys.exit("Sorry, I don't know how to edit the specified colormap") + # Hold a reference so it doesn't get GC'ed + fig = plt.figure() + figureCanvas = gui.FigureCanvas(fig) + v = gui.viscm_editor(figure=fig, uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) + mainwindow = gui.EditorWindow(figureCanvas, v) + else: + raise RuntimeError("can't happen") + + if args.quit: + sys.exit() + + figureCanvas.setSizePolicy(gui.QtWidgets.QSizePolicy.Expanding, + gui.QtWidgets.QSizePolicy.Expanding) + figureCanvas.updateGeometry() + + mainwindow.resize(800, 600) + mainwindow.show() + + # PyQt messes up signal handling by default. Python signal handlers (e.g., + # the default handler for SIGINT that raises KeyboardInterrupt) can only + # run when we enter the Python interpreter, which doesn't happen while + # idling in the Qt mainloop. (Unless we register a timer to poll + # explicitly.) So here we unregister Python's default signal handler and + # replace it with... the *operating system's* default signal handler, so + # instead of a KeyboardInterrupt our process just exits. + import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) + + app.exec_() diff --git a/viscm/gui.py b/viscm/gui.py index 5fabe0a..669a529 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -948,117 +948,13 @@ def load(self, path): self.name = path -def main(): - import argparse - argv = sys.argv[1:] - - # Usage: - # python -m viscm - # python -m viscm edit - # python -m viscm edit - # (file.py must define some appropriate globals) - # python -m viscm view - # (file.py must define a global named "test_cm") - # python -m viscm view "matplotlib builtin colormap" - # python -m viscm view --save=foo.png ... - - parser = argparse.ArgumentParser( - prog="python -m viscm", - description="A colormap tool.", - ) - parser.add_argument("action", metavar="ACTION", - help="'edit' or 'view' (or 'show', same as 'view')", - choices=["edit", "view", "show"], - default="edit", - nargs="?") - parser.add_argument("colormap", metavar="COLORMAP", - default=None, - help="A .json file saved from the editor, or " - "the name of a matplotlib builtin colormap", - nargs="?") - parser.add_argument("--uniform-space", metavar="SPACE", - default="CAM02-UCS", - dest="uniform_space", - help="The perceptually uniform space to use. Usually " - "you should leave this alone. You can pass 'CIELab' " - "if you're curious how uniform some colormap is in " - "CIELab space. You can pass 'buggy-CAM02-UCS' if " - "you're trying to reproduce the matplotlib colormaps " - "(which turn out to have had a small bug in the " - "assumed sRGB viewing conditions) from their bezier " - "curves.") - parser.add_argument("-t", "--type", type=str, - default="linear", choices=["linear", "diverging", "diverging-continuous"], - help="Choose a colormap type. Supported options are 'linear', 'diverging', and 'diverging-continuous") - parser.add_argument("-m", "--method", type=str, - default="CatmulClark", choices=["Bezier", "CatmulClark"], - help="Choose a spline construction method. 'CatmulClark' is the default, but you may choose the legacy option 'Bezier'") - parser.add_argument("--save", metavar="FILE", - default=None, - help="Immediately save visualization to a file " - "(view-mode only).") - parser.add_argument("--quit", default=False, action="store_true", - help="Quit immediately after starting " - "(useful with --save).") - args = parser.parse_args(argv) - - cm = Colormap(args.type, args.method, args.uniform_space) - app = QtWidgets.QApplication([]) - - if args.colormap: - cm.load(args.colormap) - - - # Easter egg! I keep typing 'show' instead of 'view' so accept both - if args.action in ("view", "show"): - if cm is None: - sys.exit("Please specify a colormap") - fig = plt.figure() - figureCanvas = FigureCanvas(fig) - v = viscm(cm.cmap, name=cm.name, figure=fig, uniform_space=cm.uniform_space) - mainwindow = ViewerWindow(figureCanvas, v, cm.name) - if args.save is not None: - v.figure.set_size_inches(20, 12) - v.figure.savefig(args.save) - elif args.action == "edit": - if not cm.can_edit: - sys.exit("Sorry, I don't know how to edit the specified colormap") - # Hold a reference so it doesn't get GC'ed - fig = plt.figure() - figureCanvas = FigureCanvas(fig) - v = viscm_editor(figure=fig, uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) - mainwindow = EditorWindow(figureCanvas, v) - else: - raise RuntimeError("can't happen") - - if args.quit: - sys.exit() - - figureCanvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) - figureCanvas.updateGeometry() - - mainwindow.resize(800, 600) - mainwindow.show() - - # PyQt messes up signal handling by default. Python signal handlers (e.g., - # the default handler for SIGINT that raises KeyboardInterrupt) can only - # run when we enter the Python interpreter, which doesn't happen while - # idling in the Qt mainloop. (Unless we register a timer to poll - # explicitly.) So here we unregister Python's default signal handler and - # replace it with... the *operating system's* default signal handler, so - # instead of a KeyboardInterrupt our process just exits. - import signal - signal.signal(signal.SIGINT, signal.SIG_DFL) - - app.exec_() - def about(): QtWidgets.QMessageBox.about(None, "VISCM", "Copyright (C) 2015-2016 Nathaniel Smith\n" + "Copyright (C) 2015-2016 Stéfan van der Walt\n" "Copyright (C) 2016 Hankun Zhao") + class ViewerWindow(QtWidgets.QMainWindow): def __init__(self, figurecanvas, viscm, cmapname, parent=None): QtWidgets.QMainWindow.__init__(self, parent) From 019945d4ed7f069fd4242c12b43cd8f4c4cf9fe5 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 20 May 2023 21:43:02 -0600 Subject: [PATCH 10/47] Code / docs cleanup * Add anaconda.org URL to README * Fix minor error in README * Document passing a .py file as COLORMAP, clean up unnecessary comment * Relocate `__name__ == "__main__"` check to `cli` module --- README.rst | 5 +++-- viscm/cli.py | 21 ++++++++------------- viscm/gui.py | 4 ---- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/README.rst b/README.rst index 0339be0..c6c76c7 100644 --- a/README.rst +++ b/README.rst @@ -14,7 +14,8 @@ resulting visualizations and use the editor tool `on this website `_. Downloads: - https://pypi.python.org/pypi/viscm/ + * https://pypi.python.org/pypi/viscm/ + * https://anaconda.org/conda-forge/viscm/ Code and bug tracker: https://github.com/matplotlib/viscm @@ -29,4 +30,4 @@ Dependencies: * NumPy License: - MIT, see LICENSE.txt for details. + MIT, see `LICENSE `__ for details. diff --git a/viscm/cli.py b/viscm/cli.py index 9960804..246ae46 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -2,23 +2,13 @@ import matplotlib.pyplot as plt -from . import gui +from viscm import gui def cli(): import argparse argv = sys.argv[1:] - # Usage: - # python -m viscm - # python -m viscm edit - # python -m viscm edit - # (file.py must define some appropriate globals) - # python -m viscm view - # (file.py must define a global named "test_cm") - # python -m viscm view "matplotlib builtin colormap" - # python -m viscm view --save=foo.png ... - parser = argparse.ArgumentParser( prog="python -m viscm", description="A colormap tool.", @@ -30,8 +20,9 @@ def cli(): nargs="?") parser.add_argument("colormap", metavar="COLORMAP", default=None, - help="A .json file saved from the editor, or " - "the name of a matplotlib builtin colormap", + help="A .json file saved from the editor, a .py file containing" + " a global named `test_cm`, or the name of a matplotlib" + " builtin colormap", nargs="?") parser.add_argument("--uniform-space", metavar="SPACE", default="CAM02-UCS", @@ -109,3 +100,7 @@ def cli(): signal.signal(signal.SIGINT, signal.SIG_DFL) app.exec_() + + +if __name__ == "__main__": + cli() diff --git a/viscm/gui.py b/viscm/gui.py index 669a529..71e44ce 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -1225,7 +1225,3 @@ def loadviewer(self): newwindow.resize(800, 600) newwindow.show() - - -if __name__ == "__main__": - main() From cf118341dde20a9ba27de4e4a2ce3eaf1d5cfb6a Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:02:04 -0600 Subject: [PATCH 11/47] Configure black & ruff with pre-commit --- .pre-commit-config.yaml | 11 +++++++++++ doc/contributing.md | 14 ++++++++++++++ environment.yml | 5 +++++ pyproject.toml | 10 +++++++++- 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2d58682 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: + - repo: "https://github.com/charliermarsh/ruff-pre-commit" + rev: "v0.0.269" + hooks: + - id: "ruff" + args: ["--fix", "--exit-non-zero-on-fix"] + + - repo: "https://github.com/psf/black" + rev: "23.3.0" + hooks: + - id: "black" diff --git a/doc/contributing.md b/doc/contributing.md index fc5825b..a7de17e 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -21,3 +21,17 @@ rm -rf dist python -m build pip install dist/*.whl # or `dist/*.tar.gz` ``` + + +## Code formatting and linting + +This codebase uses [black](https://black.readthedocs.io/en/stable/) and +[ruff](https://github.com/charliermarsh/ruff) to automatically format and lint the code. + +[`pre-commit`](https://pre-commit.com/) is configured to run them automatically. You can +trigger this manually with `pre-commit run --all-files`. + +Thanks to pre-commit, all commits should be formatted. In cases where formatting needs +to be fixed (e.g. changing config of a linter), a format-only commit should be created, +and then another commit should immediately follow which updates +`.git-blame-ignore-revs`. diff --git a/environment.yml b/environment.yml index 33c0a90..21689cc 100644 --- a/environment.yml +++ b/environment.yml @@ -3,10 +3,15 @@ channels: - "conda-forge" - "nodefaults" dependencies: + # Runtime - "python ~=3.11" - "numpy ~=1.24" - "matplotlib ~=3.7" - "colorspacious ~=1.1" - "scipy ~=1.10" + + # Development + - "pre-commit" + - pip: - "build ~=0.10" diff --git a/pyproject.toml b/pyproject.toml index 1b53c90..e5146ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,4 +45,12 @@ packages = {find = {}} package-data = {viscm = ["examples/*"]} -# [tool.black] +[tool.black] +target-version = ["py37", "py38", "py39", "py310", "py311"] + +[tool.ruff] +target-version = "py37" +select = ["F", "E", "W", "C90", "I", "UP", "YTT", "B", "A", "C4", "T10", "RUF"] + +[tool.ruff.mccabe] +max-complexity = 11 From 1fec42d0baf90e00d510efd76cb6006fa0c70dc4 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:04:08 -0600 Subject: [PATCH 12/47] Lint and format with `ruff` and `black` --- tests.py | 86 +++-- viscm/__main__.py | 1 + viscm/bezierbuilder.py | 97 +++-- viscm/cli.py | 116 ++++-- viscm/gui.py | 819 +++++++++++++++++++++++------------------ viscm/minimvc.py | 3 +- 6 files changed, 655 insertions(+), 467 deletions(-) diff --git a/tests.py b/tests.py index ec6a02e..ac8d37f 100644 --- a/tests.py +++ b/tests.py @@ -1,13 +1,13 @@ -from viscm.gui import * -from viscm.bezierbuilder import * import numpy as np -import matplotlib as mpl -from matplotlib.backends.qt_compat import QtGui, QtCore -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas -cms = {"viscm/examples/sample_linear.jscm", - "viscm/examples/sample_diverging.jscm", - "viscm/examples/sample_diverging_continuous.jscm"} +from viscm.bezierbuilder import json +from viscm.gui import Colormap, viscm_editor + +cms = { + "viscm/examples/sample_linear.jscm", + "viscm/examples/sample_diverging.jscm", + "viscm/examples/sample_diverging_continuous.jscm", +} def test_editor_loads_native(): @@ -16,8 +16,13 @@ def test_editor_loads_native(): data = json.loads(f.read()) cm = Colormap(None, "CatmulClark", "CAM02-UCS") cm.load(k) - viscm = viscm_editor(uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) - assert viscm.name == data["name"] + viscm = viscm_editor( + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) + assert viscm.name == data["name"] extensions = data["extensions"]["https://matplotlib.org/viscm"] xp, yp, fixed = viscm.control_point_model.get_control_points() @@ -26,7 +31,7 @@ def test_editor_loads_native(): assert len(extensions["xp"]) == len(xp) assert len(extensions["yp"]) == len(yp) assert len(xp) == len(yp) - for i in range(len(xp)): + for i in range(len(xp)): assert extensions["xp"][i] == xp[i] assert extensions["yp"][i] == yp[i] assert extensions["min_Jp"] == viscm.min_Jp @@ -35,19 +40,34 @@ def test_editor_loads_native(): assert extensions["cmtype"] == viscm.cmtype colors = data["colors"] - colors = [[int(c[i:i + 2], 16) / 256 for i in range(0, 6, 2)] for c in [colors[i:i + 6] for i in range(0, len(colors), 6)]] + colors = [ + [int(c[i : i + 2], 16) / 256 for i in range(0, 6, 2)] + for c in [colors[i : i + 6] for i in range(0, len(colors), 6)] + ] editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() for i in range(len(colors)): for z in range(3): assert colors[i][z] == np.rint(editor_colors[i][z] / 256) + +# import matplotlib as mpl +# from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +# from matplotlib.backends.qt_compat import QtCore, QtGui +# # def test_editor_add_point(): # # Testing linear - +# # fig = plt.figure() # figure_canvas = FigureCanvas(fig) -# linear = viscm_editor(min_Jp=40, max_Jp=60, xp=[-10, 10], yp=[0,0], figure=fig, cmtype="linear") - +# linear = viscm_editor( +# min_Jp=40, +# max_Jp=60, +# xp=[-10, 10], +# yp=[0,0], +# figure=fig, +# cmtype="linear", +# ) +# # Jp, ap, bp = linear.cmap_model.get_Jpapbp(3) # eJp, eap, ebp = [40, 50, 60], [-10, 0, 10], [0, 0, 0] # for i in range(3): @@ -61,12 +81,24 @@ def test_editor_loads_native(): # for i in range(3): # for z in range(3): # assert approxeq(rgb[i][z], ergb[i][z]) - + # # Testing adding a point to linear # linear.bezier_builder.mode = "add" -# qtEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, QtCore.QPoint(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.ShiftModifier) -# event = mpl.backend_bases.MouseEvent("button_press_event", figure_canvas, 0, 10, guiEvent=qtEvent) +# qtEvent = QtGui.QMouseEvent( +# QtCore.QEvent.MouseButtonPress, +# QtCore.QPoint(), +# QtCore.Qt.LeftButton, +# QtCore.Qt.LeftButton, +# QtCore.Qt.ShiftModifier, +# ) +# event = mpl.backend_bases.MouseEvent( +# "button_press_event", +# figure_canvas, +# 0, +# 10, +# guiEvent=qtEvent, +# ) # event.xdata = 0 # event.ydata = 10 # event.inaxes = linear.bezier_builder.ax @@ -87,8 +119,20 @@ def test_editor_loads_native(): # # Removing a point from linear # linear.bezier_builder.mode = "remove" -# qtEvent = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, QtCore.QPoint(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.ControlModifier) -# event = mpl.backend_bases.MouseEvent("button_press_event", figure_canvas, 0, 10, guiEvent=qtEvent) +# qtEvent = QtGui.QMouseEvent( +# QtCore.QEvent.MouseButtonPress, +# QtCore.QPoint(), +# QtCore.Qt.LeftButton, +# QtCore.Qt.LeftButton, +# QtCore.Qt.ControlModifier, +# ) +# event = mpl.backend_bases.MouseEvent( +# "button_press_event", +# figure_canvas, +# 0, +# 10, +# guiEvent=qtEvent, +# ) # event.xdata = 0 # event.ydata = 10 # event.inaxes = linear.bezier_builder.ax @@ -102,7 +146,5 @@ def test_editor_loads_native(): # # print(linear.cmap_model.get_Jpapbp(3)) - def approxeq(x, y, err=0.0001): return abs(y - x) < err - diff --git a/viscm/__main__.py b/viscm/__main__.py index f4b3e03..7d19fdb 100644 --- a/viscm/__main__.py +++ b/viscm/__main__.py @@ -4,4 +4,5 @@ # See file LICENSE.txt for license information. from .cli import cli + cli() diff --git a/viscm/bezierbuilder.py b/viscm/bezierbuilder.py index 71c25f0..cbffbeb 100644 --- a/viscm/bezierbuilder.py +++ b/viscm/bezierbuilder.py @@ -1,4 +1,3 @@ -# coding=utf8 # BézierBuilder # # Copyright (c) 2013, Juan Luis Cano Rodríguez @@ -32,17 +31,17 @@ $ python bezier_builder.py """ -from __future__ import division, print_function, absolute_import -import numpy as np from math import factorial -from scipy import signal +import numpy as np +from matplotlib.backends.qt_compat import QtCore from matplotlib.lines import Line2D -from matplotlib.backends.qt_compat import QtGui, QtCore + from .minimvc import Trigger -class ControlPointModel(object): + +class ControlPointModel: def __init__(self, xp, yp, fixed=None): # fixed is either None (if no point is fixed) or and index of a fixed # point @@ -84,22 +83,22 @@ def set_control_points(self, xp, yp, fixed=None): self.trigger.fire() -class ControlPointBuilder(object): +class ControlPointBuilder: def __init__(self, ax, control_point_model): self.ax = ax self.control_point_model = control_point_model self.canvas = self.ax.figure.canvas xp, yp, _ = self.control_point_model.get_control_points() - self.control_polygon = Line2D(xp, yp, - ls="--", c="#666666", marker="x", - mew=2, mec="#204a87") + self.control_polygon = Line2D( + xp, yp, ls="--", c="#666666", marker="x", mew=2, mec="#204a87" + ) self.ax.add_line(self.control_polygon) # Event handler for mouse clicking - self.canvas.mpl_connect('button_press_event', self.on_button_press) - self.canvas.mpl_connect('button_release_event', self.on_button_release) - self.canvas.mpl_connect('motion_notify_event', self.on_motion_notify) + self.canvas.mpl_connect("button_press_event", self.on_button_press) + self.canvas.mpl_connect("button_release_event", self.on_button_release) + self.canvas.mpl_connect("motion_notify_event", self.on_motion_notify) self._index = None # Active vertex @@ -109,7 +108,7 @@ def __init__(self, ax, control_point_model): def on_button_press(self, event): modkey = event.guiEvent.modifiers() - # Ignore clicks outside axes + # Ignore clicks outside axes if event.inaxes != self.ax: return res, ind = self.control_polygon.contains(event) @@ -118,8 +117,7 @@ def on_button_press(self, event): if res and (modkey == QtCore.Qt.ControlModifier or self.mode == "remove"): # Control-click deletes self.control_point_model.remove_point(ind["ind"][0]) - if (modkey == QtCore.Qt.ShiftModifier or self.mode == "add"): - + if modkey == QtCore.Qt.ShiftModifier or self.mode == "add": # Adding a new point. Find the two closest points and insert it in # between them. total_squared_dists = [] @@ -132,10 +130,7 @@ def on_button_press(self, event): total_squared_dists.append(dist) best = np.argmin(total_squared_dists) - self.control_point_model.add_point(best + 1, - event.xdata, - event.ydata) - + self.control_point_model.add_point(best + 1, event.xdata, event.ydata) def on_button_release(self, event): if event.button != 1: @@ -170,7 +165,7 @@ def compute_bezier_points(xp, yp, at, method, grid=256): # arclength(t), and then invert it. t = np.linspace(0, 1, grid) - arclength = compute_arc_length(xp, yp, method, t=t) + arclength = compute_arc_length(xp, yp, method, t=t) arclength /= arclength[-1] # Now (t, arclength) is a lookup table describing the t -> arclength # mapping. Invert it to get at -> t @@ -181,6 +176,7 @@ def compute_bezier_points(xp, yp, at, method, grid=256): return method(list(zip(xp, yp)), at_t).T + def compute_arc_length(xp, yp, method, t=None, grid=256): if t is None: t = np.linspace(0, 1, grid) @@ -194,7 +190,8 @@ def compute_arc_length(xp, yp, method, t=None, grid=256): np.hypot(x_deltas, y_deltas, out=arclength_deltas[1:]) return np.cumsum(arclength_deltas) -class SingleBezierCurveModel(object): + +class SingleBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): self.method = eval(method) self.control_point_model = control_point_model @@ -216,7 +213,7 @@ def _refresh(self): # self.canvas.draw() -class TwoBezierCurveModel(object): +class TwoBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): self.method = eval(method) self.control_point_model = control_point_model @@ -224,7 +221,6 @@ def __init__(self, control_point_model, method="CatmulClark"): self.bezier_curve = Line2D(x, y) self.trigger = self.control_point_model.trigger self.trigger.add_callback(self._refresh) - def get_bezier_points(self, num=200): return self.get_bezier_points_at(np.linspace(0, 1, num)) @@ -233,15 +229,15 @@ def get_bezier_points_at(self, at, grid=256): at = np.asarray(at) if at.ndim == 0: at = np.array([at]) - - low_mask = (at < 0.5) - high_mask = (at >= 0.5) + + low_mask = at < 0.5 + high_mask = at >= 0.5 xp, yp, fixed = self.control_point_model.get_control_points() assert fixed is not None - low_xp = xp[:fixed + 1] - low_yp = yp[:fixed + 1] + low_xp = xp[: fixed + 1] + low_yp = yp[: fixed + 1] high_xp = xp[fixed:] high_yp = yp[fixed:] @@ -257,13 +253,15 @@ def get_bezier_points_at(self, at, grid=256): low_at = (0.5 - (0.5 - low_at) * sf) * 2 else: high_at = (0.5 + (high_at - 0.5) * sf) * 2 - 1 - low_at = low_at * 2 - - low_points = compute_bezier_points(low_xp, low_yp, - low_at, self.method, grid=grid) - high_points = compute_bezier_points(high_xp, high_yp, - high_at, self.method, grid=grid) - out = np.concatenate([low_points,high_points], 1) + low_at = low_at * 2 + + low_points = compute_bezier_points( + low_xp, low_yp, low_at, self.method, grid=grid + ) + high_points = compute_bezier_points( + high_xp, high_yp, high_at, self.method, grid=grid + ) + out = np.concatenate([low_points, high_points], 1) return out def _refresh(self): @@ -271,7 +269,7 @@ def _refresh(self): self.bezier_curve.set_data(x, y) -class BezierCurveView(object): +class BezierCurveView: def __init__(self, ax, bezier_curve_model): self.ax = ax self.bezier_curve_model = bezier_curve_model @@ -291,19 +289,18 @@ def _refresh(self): # We used to use scipy.special.binom here, -# but reimplementing it ourself lets us avoid pulling in a dependency +# but reimplementing it ourself lets us avoid pulling in a dependency # scipy just for that one function. def binom(n, k): return factorial(n) * 1.0 / (factorial(k) * factorial(n - k)) -def Bernstein(n, k): - """Bernstein polynomial. - """ +def Bernstein(n, k): + """Bernstein polynomial.""" coeff = binom(n, k) def _bpoly(x): - return coeff * x ** k * (1 - x) ** (n - k) + return coeff * x**k * (1 - x) ** (n - k) return _bpoly @@ -318,7 +315,8 @@ def Bezier(points, at): curve = np.zeros((at_flat.shape[0], 2)) for ii in range(N): curve += np.outer(Bernstein(N - 1, ii)(at_flat), points[ii]) - return curve.reshape(at.shape + (2,)) + return curve.reshape((*at.shape, 2)) + def CatmulClark(points, at): points = np.asarray(points) @@ -327,19 +325,10 @@ def CatmulClark(points, at): new_p = np.zeros((2 * len(points), 2)) new_p[0] = points[0] new_p[-1] = points[-1] - new_p[1:-2:2] = 3/4. * points[:-1] + 1/4. * points[1:] - new_p[2:-1:2] = 1/4. * points[:-1] + 3/4. * points[1:] + new_p[1:-2:2] = 3 / 4.0 * points[:-1] + 1 / 4.0 * points[1:] + new_p[2:-1:2] = 1 / 4.0 * points[:-1] + 3 / 4.0 * points[1:] points = new_p xp, yp = zip(*points) xp = np.interp(at, np.linspace(0, 1, len(xp)), xp) yp = np.interp(at, np.linspace(0, 1, len(yp)), yp) return np.asarray(list(zip(xp, yp))) - - - - - - - - - diff --git a/viscm/cli.py b/viscm/cli.py index 246ae46..f7f80cc 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -7,47 +7,78 @@ def cli(): import argparse + argv = sys.argv[1:] parser = argparse.ArgumentParser( prog="python -m viscm", description="A colormap tool.", ) - parser.add_argument("action", metavar="ACTION", - help="'edit' or 'view' (or 'show', same as 'view')", - choices=["edit", "view", "show"], - default="edit", - nargs="?") - parser.add_argument("colormap", metavar="COLORMAP", - default=None, - help="A .json file saved from the editor, a .py file containing" - " a global named `test_cm`, or the name of a matplotlib" - " builtin colormap", - nargs="?") - parser.add_argument("--uniform-space", metavar="SPACE", - default="CAM02-UCS", - dest="uniform_space", - help="The perceptually uniform space to use. Usually " - "you should leave this alone. You can pass 'CIELab' " - "if you're curious how uniform some colormap is in " - "CIELab space. You can pass 'buggy-CAM02-UCS' if " - "you're trying to reproduce the matplotlib colormaps " - "(which turn out to have had a small bug in the " - "assumed sRGB viewing conditions) from their bezier " - "curves.") - parser.add_argument("-t", "--type", type=str, - default="linear", choices=["linear", "diverging", "diverging-continuous"], - help="Choose a colormap type. Supported options are 'linear', 'diverging', and 'diverging-continuous") - parser.add_argument("-m", "--method", type=str, - default="CatmulClark", choices=["Bezier", "CatmulClark"], - help="Choose a spline construction method. 'CatmulClark' is the default, but you may choose the legacy option 'Bezier'") - parser.add_argument("--save", metavar="FILE", - default=None, - help="Immediately save visualization to a file " - "(view-mode only).") - parser.add_argument("--quit", default=False, action="store_true", - help="Quit immediately after starting " - "(useful with --save).") + parser.add_argument( + "action", + metavar="ACTION", + help="'edit' or 'view' (or 'show', same as 'view')", + choices=["edit", "view", "show"], + default="edit", + nargs="?", + ) + parser.add_argument( + "colormap", + metavar="COLORMAP", + default=None, + help="A .json file saved from the editor, a .py file containing" + " a global named `test_cm`, or the name of a matplotlib builtin" + " colormap", + nargs="?", + ) + parser.add_argument( + "--uniform-space", + metavar="SPACE", + default="CAM02-UCS", + dest="uniform_space", + help="The perceptually uniform space to use. Usually " + "you should leave this alone. You can pass 'CIELab' " + "if you're curious how uniform some colormap is in " + "CIELab space. You can pass 'buggy-CAM02-UCS' if " + "you're trying to reproduce the matplotlib colormaps " + "(which turn out to have had a small bug in the " + "assumed sRGB viewing conditions) from their bezier " + "curves.", + ) + parser.add_argument( + "-t", + "--type", + type=str, + default="linear", + choices=["linear", "diverging", "diverging-continuous"], + help=( + "Choose a colormap type. Supported options are 'linear', 'diverging'," + " and 'diverging-continuous" + ), + ) + parser.add_argument( + "-m", + "--method", + type=str, + default="CatmulClark", + choices=["Bezier", "CatmulClark"], + help=( + "Choose a spline construction method. 'CatmulClark' is the default, but" + " you may choose the legacy option 'Bezier'" + ), + ) + parser.add_argument( + "--save", + metavar="FILE", + default=None, + help="Immediately save visualization to a file " "(view-mode only).", + ) + parser.add_argument( + "--quit", + default=False, + action="store_true", + help="Quit immediately after starting " "(useful with --save).", + ) args = parser.parse_args(argv) cm = gui.Colormap(args.type, args.method, args.uniform_space) @@ -56,7 +87,6 @@ def cli(): if args.colormap: cm.load(args.colormap) - # Easter egg! I keep typing 'show' instead of 'view' so accept both if args.action in ("view", "show"): if cm is None: @@ -74,7 +104,13 @@ def cli(): # Hold a reference so it doesn't get GC'ed fig = plt.figure() figureCanvas = gui.FigureCanvas(fig) - v = gui.viscm_editor(figure=fig, uniform_space=cm.uniform_space, cmtype=cm.cmtype, method=cm.method, **cm.params) + v = gui.viscm_editor( + figure=fig, + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) mainwindow = gui.EditorWindow(figureCanvas, v) else: raise RuntimeError("can't happen") @@ -82,8 +118,9 @@ def cli(): if args.quit: sys.exit() - figureCanvas.setSizePolicy(gui.QtWidgets.QSizePolicy.Expanding, - gui.QtWidgets.QSizePolicy.Expanding) + figureCanvas.setSizePolicy( + gui.QtWidgets.QSizePolicy.Expanding, gui.QtWidgets.QSizePolicy.Expanding + ) figureCanvas.updateGeometry() mainwindow.resize(800, 600) @@ -97,6 +134,7 @@ def cli(): # replace it with... the *operating system's* default signal handler, so # instead of a KeyboardInterrupt our process just exits. import signal + signal.signal(signal.SIGINT, signal.SIG_DFL) app.exec_() diff --git a/viscm/gui.py b/viscm/gui.py index 71e44ce..b3a1a2e 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -5,17 +5,17 @@ # Simple script using CIECAM02 and CAM02-UCS to visualize properties of a # matplotlib colormap -from __future__ import division, print_function, absolute_import -import sys -import os.path + import json +import os.path +import sys import numpy as np -#matplotlib.rcParams['backend'] = "QT4AGG" +# matplotlib.rcParams['backend'] = "QT4AGG" # Do this first before any other matplotlib imports, to force matplotlib to # use a Qt backend -from matplotlib.backends.qt_compat import QtWidgets, QtCore, QtGui, _getSaveFileName +from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets, _getSaveFileName try: from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas @@ -26,19 +26,20 @@ from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas import matplotlib +import matplotlib.colors import matplotlib.pyplot as plt import mpl_toolkits.mplot3d -from matplotlib.gridspec import GridSpec -import matplotlib.colors +from colorspacious import ( + CIECAM02Space, + CIECAM02Surround, + cspace_convert, + cspace_converter, +) from matplotlib.colors import ListedColormap +from matplotlib.gridspec import GridSpec -from scipy.interpolate import UnivariateSpline - -from colorspacious import (cspace_converter, cspace_convert, - CIECAM02Space, CIECAM02Surround) from .minimvc import Trigger - # The correct L_A value for the standard sRGB viewing conditions is: # (64 / np.pi) / 5 # Due to an error in our color conversion code, the matplotlib colormaps were @@ -56,43 +57,43 @@ XYZ100_w="D65", Y_b=20, L_A=(64 / np.pi) * 5, # bug: should be / 5 - surround=CIECAM02Surround.AVERAGE) -buggy_CAM02UCS = {"name": "CAM02-UCS", - "ciecam02_space": buggy_sRGB_viewing_conditions, - } + surround=CIECAM02Surround.AVERAGE, +) +buggy_CAM02UCS = { + "name": "CAM02-UCS", + "ciecam02_space": buggy_sRGB_viewing_conditions, +} GREYSCALE_CONVERSION_SPACE = "JCh" _sRGB1_to_JCh = cspace_converter("sRGB1", GREYSCALE_CONVERSION_SPACE) _JCh_to_sRGB1 = cspace_converter(GREYSCALE_CONVERSION_SPACE, "sRGB1") + + def to_greyscale(sRGB1): JCh = _sRGB1_to_JCh(sRGB1) JCh[..., 1] = 0 return np.clip(_JCh_to_sRGB1(JCh), 0, 1) -_deuter50_space = {"name": "sRGB1+CVD", - "cvd_type": "deuteranomaly", - "severity": 50} + +_deuter50_space = {"name": "sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 50} _deuter50_to_sRGB1 = cspace_converter(_deuter50_space, "sRGB1") -_deuter100_space = {"name": "sRGB1+CVD", - "cvd_type": "deuteranomaly", - "severity": 100} +_deuter100_space = {"name": "sRGB1+CVD", "cvd_type": "deuteranomaly", "severity": 100} _deuter100_to_sRGB1 = cspace_converter(_deuter100_space, "sRGB1") -_prot50_space = {"name": "sRGB1+CVD", - "cvd_type": "protanomaly", - "severity": 50} +_prot50_space = {"name": "sRGB1+CVD", "cvd_type": "protanomaly", "severity": 50} _prot50_to_sRGB1 = cspace_converter(_prot50_space, "sRGB1") -_prot100_space = {"name": "sRGB1+CVD", - "cvd_type": "protanomaly", - "severity": 100} +_prot100_space = {"name": "sRGB1+CVD", "cvd_type": "protanomaly", "severity": 100} _prot100_to_sRGB1 = cspace_converter(_prot100_space, "sRGB1") + def _show_cmap(ax, rgb): ax.imshow(rgb[np.newaxis, ...], aspect="auto") + def _apply_rgb_mat(mat, rgb): return np.clip(np.einsum("...ij,...j->...i", mat, rgb), 0, 1) + # sRGB corners: a' goes from -37.4 to 45 AP_LIM = (-38, 46) # b' goes from -46.5 to 42 @@ -109,6 +110,7 @@ def _setup_Jpapbp_axis(ax): ax.set_ylim(*BP_LIM) ax.set_zlim(*JP_LIM) + # Adapt a matplotlib colormap to a linearly transformed version -- useful for # visualizing how colormaps look given color deficiency. # Kinda a hack, b/c we inherit from Colormap (this is required), but then @@ -119,11 +121,11 @@ def __init__(self, transform, base_cmap): self.base_cmap = base_cmap def __call__(self, *args, **kwargs): - bts = kwargs.pop('bytes', False) + bts = kwargs.pop("bytes", False) fx = self.base_cmap(*args, bytes=False, **kwargs) tfx = self.transform(fx) if bts: - return (tfx * 255).astype('uint8') + return (tfx * 255).astype("uint8") return tfx def set_bad(self, *args, **kwargs): @@ -138,39 +140,39 @@ def set_over(self, *args, **kwargs): def is_gray(self): return False -def _vis_axes(fig): - grid = GridSpec(10, 4, - left=0.02, - right=0.98, - bottom=0.02, - width_ratios=[1] * 4, - height_ratios=[1] * 10) - axes = {'cmap': grid[0, 0], - 'deltas': grid[1:4, 0], - - 'cmap-greyscale': grid[0, 1], - 'lightness-deltas': grid[1:4, 1], - - 'deuteranomaly': grid[4, 0], - 'deuteranopia': grid[5, 0], - 'protanomaly': grid[4, 1], - 'protanopia': grid[5, 1], - - # 'lightness': grid[4:6, 1], - # 'colourfulness': grid[4:6, 2], - # 'hue': grid[4:6, 3], - - 'image0': grid[0:3, 2], - 'image0-cb': grid[0:3, 3], - 'image1': grid[3:6, 2], - 'image1-cb': grid[3:6, 3], - 'image2': grid[6:8, 2:], - 'image2-cb': grid[8:, 2:] - } - axes = dict([(key, fig.add_subplot(value)) - for (key, value) in axes.items()]) - axes['gamut'] = fig.add_subplot(grid[6:, :2], projection='3d') +def _vis_axes(fig): + grid = GridSpec( + 10, + 4, + left=0.02, + right=0.98, + bottom=0.02, + width_ratios=[1] * 4, + height_ratios=[1] * 10, + ) + axes = { + "cmap": grid[0, 0], + "deltas": grid[1:4, 0], + "cmap-greyscale": grid[0, 1], + "lightness-deltas": grid[1:4, 1], + "deuteranomaly": grid[4, 0], + "deuteranopia": grid[5, 0], + "protanomaly": grid[4, 1], + "protanopia": grid[5, 1], + # 'lightness': grid[4:6, 1], + # 'colourfulness': grid[4:6, 2], + # 'hue': grid[4:6, 3], + "image0": grid[0:3, 2], + "image0-cb": grid[0:3, 3], + "image1": grid[3:6, 2], + "image1-cb": grid[3:6, 3], + "image2": grid[6:8, 2:], + "image2-cb": grid[8:, 2:], + } + + axes = {key: fig.add_subplot(value) for (key, value) in axes.items()} + axes["gamut"] = fig.add_subplot(grid[6:, :2], projection="3d") return axes @@ -186,25 +188,35 @@ def lookup_colormap_by_name(name): module_name, object_name = name.split(":", 1) object_path = object_name.split(".") import importlib + cm = importlib.import_module(module_name) for entry in object_path: cm = getattr(cm, entry) return cm - raise ValueError("Can't find colormap {!r}".format(name)) - -class viscm(object): - def __init__(self, cm, figure=None, uniform_space="CAM02-UCS", - name=None, N=256, N_dots=50, show_gamut=False): + raise ValueError(f"Can't find colormap {name!r}") + + +class viscm: + def __init__( + self, + cm, + figure=None, + uniform_space="CAM02-UCS", + name=None, + N=256, + N_dots=50, + show_gamut=False, + ): if isinstance(cm, str): cm = lookup_colormap_by_name(cm) if name is None: name = cm.name - if figure == None: + if figure is None: figure = plt.figure() self._sRGB1_to_uniform = cspace_converter("sRGB1", uniform_space) self.figure = figure - self.figure.suptitle("Colormap evaluation: %s" % (name,), fontsize=24) + self.figure.suptitle(f"Colormap evaluation: {name}", fontsize=24) axes = _vis_axes(self.figure) @@ -229,50 +241,60 @@ def __init__(self, cm, figure=None, uniform_space="CAM02-UCS", x_dots = np.linspace(0, 1, N_dots) RGB_dots = cm(x_dots)[:, :3] - ax = axes['cmap'] + ax = axes["cmap"] _show_cmap(ax, RGB) ax.set_title("The colormap in its glory") ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) def label(ax, s): - ax.text(0.95, 0.05, s, - horizontalalignment="right", - verticalalignment="bottom", - transform=ax.transAxes) + ax.text( + 0.95, + 0.05, + s, + horizontalalignment="right", + verticalalignment="bottom", + transform=ax.transAxes, + ) def title(ax, s): - ax.text(0.98, 0.98, s, - horizontalalignment="right", - verticalalignment="top", - transform=ax.transAxes) + ax.text( + 0.98, + 0.98, + s, + horizontalalignment="right", + verticalalignment="top", + transform=ax.transAxes, + ) Jpapbp = self._sRGB1_to_uniform(RGB) def delta_ymax(values): return max(np.max(values) * 1.1, 0) - ax = axes['deltas'] - local_deltas = np.sqrt( - np.sum((Jpapbp[:-1, :] - Jpapbp[1:, :]) ** 2, axis=-1)) + ax = axes["deltas"] + local_deltas = np.sqrt(np.sum((Jpapbp[:-1, :] - Jpapbp[1:, :]) ** 2, axis=-1)) local_derivs = N * local_deltas ax.plot(x[1:], local_derivs) arclength = np.sum(local_deltas) rmse = np.std(local_derivs) title(ax, "Perceptual derivative") - label(ax, - "Length: %0.1f\nRMS deviation from flat: %0.1f (%0.1f%%)" - % (arclength, rmse, 100 * rmse / arclength)) + label( + ax, + "Length: {:0.1f}\nRMS deviation from flat: {:0.1f} ({:0.1f}%)".format( + arclength, rmse, 100 * rmse / arclength + ), + ) ax.set_ylim(-delta_ymax(-local_derivs), delta_ymax(local_derivs)) ax.get_xaxis().set_visible(False) - ax = axes['cmap-greyscale'] + ax = axes["cmap-greyscale"] _show_cmap(ax, to_greyscale(RGB)) ax.set_title("Black-and-white printed") ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - ax = axes['lightness-deltas'] + ax = axes["lightness-deltas"] ax.axhline(0, linestyle="--", color="grey") lightness_deltas = np.diff(Jpapbp[:, 0]) lightness_derivs = N * lightness_deltas @@ -281,14 +303,16 @@ def delta_ymax(values): title(ax, "Perceptual lightness derivative") lightness_arclength = np.sum(np.abs(lightness_deltas)) lightness_rmse = np.std(lightness_derivs) - label(ax, - "Length: %0.1f\nRMS deviation from flat: %0.1f (%0.1f%%)" - % (lightness_arclength, - lightness_rmse, - 100 * lightness_rmse / lightness_arclength)) - - ax.set_ylim(-delta_ymax(-lightness_derivs), - delta_ymax(lightness_derivs)) + label( + ax, + "Length: {:0.1f}\nRMS deviation from flat: {:0.1f} ({:0.1f}%)".format( + lightness_arclength, + lightness_rmse, + 100 * lightness_rmse / lightness_arclength, + ), + ) + + ax.set_ylim(-delta_ymax(-lightness_derivs), delta_ymax(lightness_derivs)) ax.get_xaxis().set_visible(False) # ax = axes['lightness'] @@ -311,28 +335,22 @@ def anom(ax, converter, name): ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - anom(axes['deuteranomaly'], - _deuter50_to_sRGB1, - "Moderate deuteranomaly") - anom(axes['deuteranopia'], - _deuter100_to_sRGB1, - "Complete deuteranopia") - - anom(axes['protanomaly'], - _prot50_to_sRGB1, - "Moderate protanomaly") - anom(axes['protanopia'], - _prot100_to_sRGB1, - "Complete protanopia") - - ax = axes['gamut'] + anom(axes["deuteranomaly"], _deuter50_to_sRGB1, "Moderate deuteranomaly") + anom(axes["deuteranopia"], _deuter100_to_sRGB1, "Complete deuteranopia") + + anom(axes["protanomaly"], _prot50_to_sRGB1, "Moderate protanomaly") + anom(axes["protanopia"], _prot100_to_sRGB1, "Complete protanopia") + + ax = axes["gamut"] ax.plot(Jpapbp[:, 1], Jpapbp[:, 2], Jpapbp[:, 0]) Jpapbp_dots = self._sRGB1_to_uniform(RGB_dots) - ax.scatter(Jpapbp_dots[:, 1], - Jpapbp_dots[:, 2], - Jpapbp_dots[:, 0], - c=RGB_dots[:, :], - s=80) + ax.scatter( + Jpapbp_dots[:, 1], + Jpapbp_dots[:, 2], + Jpapbp_dots[:, 0], + c=RGB_dots[:, :], + s=80, + ) # Draw a wireframe indicating the sRGB gamut self.gamut_patch = sRGB_gamut_patch(uniform_space) @@ -352,8 +370,9 @@ def anom(ax, converter, name): image_args = [] example_dir = os.path.join(os.path.dirname(__file__), "examples") - images.append(np.loadtxt(os.path.join(example_dir, - "st-helens_before-modified.txt.gz")).T) + images.append( + np.loadtxt(os.path.join(example_dir, "st-helens_before-modified.txt.gz")).T + ) image_args.append({}) # Adapted from @@ -376,21 +395,22 @@ def _deuter_transform(RGBA): RGB = RGBA[..., :3] RGB = np.clip(_deuter50_to_sRGB1(RGB), 0, 1) return np.concatenate((RGB, RGBA[..., 3:]), axis=-1) + deuter_cm = TransformedCMap(_deuter_transform, cm) for i, (image, args) in enumerate(zip(images, image_args)): - ax = axes['image%i' % (i,)] + ax = axes["image%i" % (i,)] ax.imshow(image, cmap=cm, **args) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) - ax_cb = axes['image%i-cb' % (i,)] + ax_cb = axes["image%i-cb" % (i,)] ax_cb.imshow(image, cmap=deuter_cm, **args) ax_cb.get_xaxis().set_visible(False) ax_cb.get_yaxis().set_visible(False) - axes['image0'].set_title("Sample images") - axes['image0-cb'].set_title("Moderate deuter.") + axes["image0"].set_title("Sample images") + axes["image0-cb"].set_title("Moderate deuter.") self.axes = axes def toggle_gamut(self): @@ -410,26 +430,35 @@ def sRGB_gamut_patch(uniform_space, resolution=20): for i in range(resolution): for j in range(resolution): # R quad - sRGB_quads.append([[fixed, i * step, j * step], - [fixed, (i+1) * step, j * step], - [fixed, (i+1) * step, (j+1) * step], - [fixed, i * step, (j+1) * step]]) - sRGB_values.append((fixed, (i + 0.5) * step, (j + 0.5) * step, - 1)) + sRGB_quads.append( + [ + [fixed, i * step, j * step], + [fixed, (i + 1) * step, j * step], + [fixed, (i + 1) * step, (j + 1) * step], + [fixed, i * step, (j + 1) * step], + ] + ) + sRGB_values.append((fixed, (i + 0.5) * step, (j + 0.5) * step, 1)) # G quad - sRGB_quads.append([[i * step, fixed, j * step], - [(i+1) * step, fixed, j * step], - [(i+1) * step, fixed, (j+1) * step], - [i * step, fixed, (j+1) * step]]) - sRGB_values.append(((i + 0.5) * step, fixed, (j + 0.5) * step, - 1)) + sRGB_quads.append( + [ + [i * step, fixed, j * step], + [(i + 1) * step, fixed, j * step], + [(i + 1) * step, fixed, (j + 1) * step], + [i * step, fixed, (j + 1) * step], + ] + ) + sRGB_values.append(((i + 0.5) * step, fixed, (j + 0.5) * step, 1)) # B quad - sRGB_quads.append([[i * step, j * step, fixed], - [(i+1) * step, j * step, fixed], - [(i+1) * step, (j+1) * step, fixed], - [i * step, (j+1) * step, fixed]]) - sRGB_values.append(((i + 0.5) * step, (j + 0.5) * step, fixed, - 1)) + sRGB_quads.append( + [ + [i * step, j * step, fixed], + [(i + 1) * step, j * step, fixed], + [(i + 1) * step, (j + 1) * step, fixed], + [i * step, (j + 1) * step, fixed], + ] + ) + sRGB_values.append(((i + 0.5) * step, (j + 0.5) * step, fixed, 1)) sRGB_quads = np.asarray(sRGB_quads) # work around colorspace transform bugginess in handling high-dim # arrays @@ -437,51 +466,55 @@ def sRGB_gamut_patch(uniform_space, resolution=20): Jpapbp_quads_2d = cspace_convert(sRGB_quads_2d, "sRGB1", uniform_space) Jpapbp_quads = Jpapbp_quads_2d.reshape((-1, 4, 3)) gamut_patch = mpl_toolkits.mplot3d.art3d.Poly3DCollection( - Jpapbp_quads[:, :, [1, 2, 0]]) + Jpapbp_quads[:, :, [1, 2, 0]] + ) gamut_patch.set_facecolor(sRGB_values) gamut_patch.set_edgecolor(sRGB_values) return gamut_patch -def sRGB_gamut_Jp_slice(Jp, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200): - bp_grid, ap_grid = np.mgrid[bp_lim[0] : bp_lim[1] : resolution * 1j, - ap_lim[0] : ap_lim[1] : resolution * 1j] +def sRGB_gamut_Jp_slice( + Jp, uniform_space, ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200 +): + bp_grid, ap_grid = np.mgrid[ + bp_lim[0] : bp_lim[1] : resolution * 1j, ap_lim[0] : ap_lim[1] : resolution * 1j + ] Jp_grid = Jp * np.ones((resolution, resolution)) - Jpapbp = np.concatenate((Jp_grid[:, :, np.newaxis], - ap_grid[:, :, np.newaxis], - bp_grid[:, :, np.newaxis]), - axis=2) + Jpapbp = np.concatenate( + ( + Jp_grid[:, :, np.newaxis], + ap_grid[:, :, np.newaxis], + bp_grid[:, :, np.newaxis], + ), + axis=2, + ) sRGB = cspace_convert(Jpapbp, uniform_space, "sRGB1") - sRGBA = np.concatenate((sRGB, np.ones(sRGB.shape[:2] + (1,))), - axis=2) + sRGBA = np.concatenate((sRGB, np.ones(sRGB.shape[:2] + (1,))), axis=2) sRGBA[np.any((sRGB < 0) | (sRGB > 1), axis=-1)] = [0, 0, 0, 0] return sRGBA def draw_pure_hue_angles(ax): # Pure hue angles from CIECAM-02 - for color, angle in [("r", 20.14), - ("y", 90.00), - ("g", 164.25), - ("b", 237.53)]: + for color, angle in [("r", 20.14), ("y", 90.00), ("g", 164.25), ("b", 237.53)]: x = np.cos(np.deg2rad(angle)) y = np.sin(np.deg2rad(angle)) ax.plot([0, x * 1000], [0, y * 1000], color + "--") -def draw_sRGB_gamut_Jp_slice(ax, Jp, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50), - **kwargs): - sRGB = sRGB_gamut_Jp_slice(Jp, uniform_space, - ap_lim=ap_lim, bp_lim=bp_lim, **kwargs) - im = ax.imshow(sRGB, aspect="equal", - extent=ap_lim + bp_lim, origin="lower") +def draw_sRGB_gamut_Jp_slice( + ax, Jp, uniform_space, ap_lim=(-50, 50), bp_lim=(-50, 50), **kwargs +): + sRGB = sRGB_gamut_Jp_slice( + Jp, uniform_space, ap_lim=ap_lim, bp_lim=bp_lim, **kwargs + ) + im = ax.imshow(sRGB, aspect="equal", extent=ap_lim + bp_lim, origin="lower") draw_pure_hue_angles(ax) ax.set_xlim(ap_lim) ax.set_ylim(bp_lim) return im + # def sRGB_gamut_J_slice(J, # ap_lim=(-50, 50), bp_lim=(-50, 50), resolution=200): # a_grid, b_grid = np.mgrid[ap_lim[0] : ap_lim[1] : resolution * 1j, @@ -496,21 +529,35 @@ def draw_sRGB_gamut_Jp_slice(ax, Jp, uniform_space, def _viscm_editor_axes(fig): - grid = GridSpec(1, 2, - width_ratios=[9, 1], - height_ratios=[50]) - axes = {'bezier': grid[0, 0], - 'cm': grid[0, 1]} - - axes = dict([(key, fig.add_subplot(value)) - for (key, value) in axes.items()]) + grid = GridSpec(1, 2, width_ratios=[9, 1], height_ratios=[50]) + axes = {"bezier": grid[0, 0], "cm": grid[0, 1]} + + axes = {key: fig.add_subplot(value) for (key, value) in axes.items()} return axes -class viscm_editor(object): - def __init__(self, figure=None, uniform_space="CAM02-UCS", - min_Jp=15, max_Jp=95, xp=None, yp=None, cmtype="linear", filter_k=100, fixed=-1, name="new cm", method="CatmulClark"): - from .bezierbuilder import SingleBezierCurveModel, TwoBezierCurveModel, ControlPointBuilder, ControlPointModel +class viscm_editor: + def __init__( + self, + figure=None, + uniform_space="CAM02-UCS", + min_Jp=15, + max_Jp=95, + xp=None, + yp=None, + cmtype="linear", + filter_k=100, + fixed=-1, + name="new cm", + method="CatmulClark", + ): + from .bezierbuilder import ( + ControlPointBuilder, + ControlPointModel, + SingleBezierCurveModel, + TwoBezierCurveModel, + ) + if figure is None: figure = plt.figure() self.cmtype = cmtype @@ -523,80 +570,98 @@ def __init__(self, figure=None, uniform_space="CAM02-UCS", self.max_Jp = max_Jp self.fixed = fixed if self.cmtype in ["diverging", "diverging-continuous"] and xp is None: - self.fixed = 4; + self.fixed = 4 if xp is None or yp is None: if method == "Bezier": - xp = {"linear":[-2.0591553836234482, 59.377014829142524, - 43.552546744036135, 4.7670857511283202, - -9.5059638942617539], - "diverging":[-9, -15, 43, 30, 0, -20, -30, 20, 1], - "diverging-continuous":[-9, -15, 43, 30, 0, -20, -30, 20, 1], - }[cmtype] - yp = {"linear":[-25.664893617021221, -21.941489361702082, - 38.874113475177353, 20.567375886524871, - 32.047872340425585], - "diverging":[-5, 20, 20, -21, 0, 21, -38, -20, -5], - "diverging-continuous":[-5, 20, 20, -21, 0, 21, -38, -20, -5] - }[cmtype] + xp = { + "linear": [ + -2.0591553836234482, + 59.377014829142524, + 43.552546744036135, + 4.7670857511283202, + -9.5059638942617539, + ], + "diverging": [-9, -15, 43, 30, 0, -20, -30, 20, 1], + "diverging-continuous": [-9, -15, 43, 30, 0, -20, -30, 20, 1], + }[cmtype] + yp = { + "linear": [ + -25.664893617021221, + -21.941489361702082, + 38.874113475177353, + 20.567375886524871, + 32.047872340425585, + ], + "diverging": [-5, 20, 20, -21, 0, 21, -38, -20, -5], + "diverging-continuous": [-5, 20, 20, -21, 0, 21, -38, -20, -5], + }[cmtype] if method == "CatmulClark": - xp = {"linear":[-2, 20,23, 5, -9], - "diverging":[-9, -15, -10, 0, 0, 5, 10, 15, 2], - "diverging-continuous":[-9, -5, -1, 0, 0, 5, 10, 15, 2], - }[cmtype] - yp = {"linear":[-25, -21, 18, 10, 12], - "diverging":[-5, -8, -20, -10, 0, 2, 8, 15, 5], - "diverging-continuous":[-5, -8, -20, -10, 0, 2, 8, 15, 5] - }[cmtype] - xy_lim = {"Bezier" : (-100, 100), - "CatmulClark" : (-50, 50)}[self.method] - - BezierModel, startJp = {"linear":(SingleBezierCurveModel, 0.5), - "diverging":(TwoBezierCurveModel, 0.75), - "diverging-continuous":(TwoBezierCurveModel, 0.5), - }[cmtype] - + xp = { + "linear": [-2, 20, 23, 5, -9], + "diverging": [-9, -15, -10, 0, 0, 5, 10, 15, 2], + "diverging-continuous": [-9, -5, -1, 0, 0, 5, 10, 15, 2], + }[cmtype] + yp = { + "linear": [-25, -21, 18, 10, 12], + "diverging": [-5, -8, -20, -10, 0, 2, 8, 15, 5], + "diverging-continuous": [-5, -8, -20, -10, 0, 2, 8, 15, 5], + }[cmtype] + xy_lim = {"Bezier": (-100, 100), "CatmulClark": (-50, 50)}[self.method] + + BezierModel, startJp = { + "linear": (SingleBezierCurveModel, 0.5), + "diverging": (TwoBezierCurveModel, 0.75), + "diverging-continuous": (TwoBezierCurveModel, 0.5), + }[cmtype] + self.control_point_model = ControlPointModel(xp, yp, fixed=self.fixed) self.bezier_model = BezierModel(self.control_point_model, self.method) - axes['bezier'].add_line(self.bezier_model.bezier_curve) - self.cmap_model = BezierCMapModel(self.bezier_model, - self.min_Jp, - self.max_Jp, - uniform_space, - cmtype=cmtype, - filter_k=filter_k) - + axes["bezier"].add_line(self.bezier_model.bezier_curve) + self.cmap_model = BezierCMapModel( + self.bezier_model, + self.min_Jp, + self.max_Jp, + uniform_space, + cmtype=cmtype, + filter_k=filter_k, + ) self.highlight_point_model = HighlightPointModel(self.cmap_model, startJp) self.highlight_point_model1 = None - self.bezier_builder = ControlPointBuilder(axes['bezier'], - self.control_point_model) + self.bezier_builder = ControlPointBuilder( + axes["bezier"], self.control_point_model + ) - self.bezier_gamut_viewer = GamutViewer2D(axes['bezier'], - self.highlight_point_model, - uniform_space, - ) - - self.bezier_highlight_point_view = HighlightPoint2DView(axes['bezier'], - self.highlight_point_model) + self.bezier_gamut_viewer = GamutViewer2D( + axes["bezier"], + self.highlight_point_model, + uniform_space, + ) + + self.bezier_highlight_point_view = HighlightPoint2DView( + axes["bezier"], self.highlight_point_model + ) if cmtype == "diverging": - self.highlight_point_model1 = HighlightPointModel(self.cmap_model, 1 - startJp) - self.bezier_highlight_point_view1 = HighlightPoint2DView(axes['bezier'], - self.highlight_point_model1) + self.highlight_point_model1 = HighlightPointModel( + self.cmap_model, 1 - startJp + ) + self.bezier_highlight_point_view1 = HighlightPoint2DView( + axes["bezier"], self.highlight_point_model1 + ) # draw_pure_hue_angles(axes['bezier']) - axes['bezier'].set_xlim(*xy_lim) - axes['bezier'].set_ylim(*xy_lim) + axes["bezier"].set_xlim(*xy_lim) + axes["bezier"].set_ylim(*xy_lim) - self.cmap_view = CMapView(axes['cm'], self.cmap_model) + self.cmap_view = CMapView(axes["cm"], self.cmap_model) self.cmap_highlighter = HighlightPointBuilder( - axes['cm'], - self.highlight_point_model, - self.highlight_point_model1) + axes["cm"], self.highlight_point_model, self.highlight_point_model1 + ) self.axes = axes def save_colormap(self, filepath): - with open(filepath, 'w') as f: + with open(filepath, "w") as f: xp, yp, fixed = self.control_point_model.get_control_points() rgb, _ = self.cmap_model.get_sRGB(num=256) hex_blob = "" @@ -609,49 +674,59 @@ def save_colormap(self, filepath): elif self.cmtype == "linear": usage_hints.append("sequential") xp, yp, fixed = self.control_point_model.get_control_points() - extensions = {"min_Jp" : self.min_Jp, - "max_Jp" : self.max_Jp, - "xp" : xp, - "yp" : yp, - "fixed" : fixed, - "filter_k" : self.cmap_model.filter_k, - "cmtype" : self.cmtype, - "uniform_colorspace" : self._uniform_space, - "spline_method" : self.method + extensions = { + "min_Jp": self.min_Jp, + "max_Jp": self.max_Jp, + "xp": xp, + "yp": yp, + "fixed": fixed, + "filter_k": self.cmap_model.filter_k, + "cmtype": self.cmtype, + "uniform_colorspace": self._uniform_space, + "spline_method": self.method, } - json.dump({"content-type": "application/vnd.matplotlib.colormap-v1+json", - "name": self.name, - "license":"http://creativecommons.org/publicdomain/zero/1.0/", - "usage-hints": usage_hints, - "colorspace" : "sRGB", - "domain" : "continuous", - "colors" : hex_blob, - "extensions" : {"https://matplotlib.org/viscm" : extensions} - }, - f, indent=4) + json.dump( + { + "content-type": "application/vnd.matplotlib.colormap-v1+json", + "name": self.name, + "license": "http://creativecommons.org/publicdomain/zero/1.0/", + "usage-hints": usage_hints, + "colorspace": "sRGB", + "domain": "continuous", + "colors": hex_blob, + "extensions": {"https://matplotlib.org/viscm": extensions}, + }, + f, + indent=4, + ) print("Saved") def export_py(self, filepath): import textwrap - template = textwrap.dedent(''' + + template = textwrap.dedent( + """ from matplotlib.colors import ListedColormap cm_type = "{type}" cm_data = {array_list} test_cm = ListedColormap(cm_data, name="{name}") - ''') + """ + ) rgb, _ = self.cmap_model.get_sRGB(num=256) - array_list = np.array2string(rgb, max_line_width=78, - prefix='cm_data = ', - separator=',') - with open(filepath, 'w') as f: - f.write(template.format(**dict(array_list=array_list, type=self.cmtype, name=self.name))) - + array_list = np.array2string( + rgb, max_line_width=78, prefix="cm_data = ", separator="," + ) + with open(filepath, "w") as f: + f.write( + template.format( + **{"array_list": array_list, "type": self.cmtype, "name": self.name} + ) + ) def show_viscm(self): - cm = ListedColormap(self.cmap_model.get_sRGB(num=256)[0], - name=self.name) + cm = ListedColormap(self.cmap_model.get_sRGB(num=256)[0], name=self.name) return cm @@ -665,8 +740,10 @@ def _filter_k_update(self, filter_k): self.cmap_model.set_filter_k(filter_k) -class BezierCMapModel(object): - def __init__(self, bezier_model, min_Jp, max_Jp, uniform_space, filter_k=100, cmtype="linear"): +class BezierCMapModel: + def __init__( + self, bezier_model, min_Jp, max_Jp, uniform_space, filter_k=100, cmtype="linear" + ): self.bezier_model = bezier_model self.min_Jp = min_Jp self.max_Jp = max_Jp @@ -691,6 +768,7 @@ def set_filter_k(self, filter_k): def get_Jpapbp_at_point(self, point): from scipy.interpolate import interp1d + Jp, ap, bp = self.get_Jpapbp() Jp, ap, bp = interp1d(np.linspace(0, 1, Jp.size), np.array([Jp, ap, bp]))(point) return Jp, ap, bp @@ -700,6 +778,7 @@ def get_Jpapbp(self, num=200): at = np.linspace(0, 1, num) if self.cmtype == "diverging": from scipy.special import erf + at = 1 + 2 * np.cumsum(erf(self.filter_k * (at - 0.5))) / num Jp = (self.max_Jp - self.min_Jp) * at + self.min_Jp return Jp, ap, bp @@ -713,17 +792,16 @@ def get_sRGB(self, num=200): return sRGB, oog -class CMapView(object): +class CMapView: def __init__(self, ax, cmap_model): self.ax = ax self.cmap_model = cmap_model rgb_display, oog_display = self._drawable_arrays() - self.image = self.ax.imshow(rgb_display, extent=(0, 0.2, 0, 1), - origin="lower") - self.gamut_alert_image = self.ax.imshow(oog_display, - extent=(0.05, 0.15, 0, 1), - origin="lower") + self.image = self.ax.imshow(rgb_display, extent=(0, 0.2, 0, 1), origin="lower") + self.gamut_alert_image = self.ax.imshow( + oog_display, extent=(0.05, 0.15, 0, 1), origin="lower" + ) self.ax.set_xlim(0, 0.2) self.ax.set_ylim(0, 1) self.ax.get_xaxis().set_visible(False) @@ -744,7 +822,7 @@ def _refresh(self): self.gamut_alert_image.set_data(oog_display) -class HighlightPointModel(object): +class HighlightPointModel: def __init__(self, cmap_model, point): self._cmap_model = cmap_model self._point = point @@ -763,7 +841,7 @@ def get_Jpapbp(self): return self._cmap_model.get_Jpapbp_at_point(self._point) -class HighlightPointBuilder(object): +class HighlightPointBuilder: def __init__(self, ax, highlight_point_model_a, highlight_point_model_b): self.ax = ax self.highlight_point_model_b = highlight_point_model_b @@ -772,16 +850,17 @@ def __init__(self, ax, highlight_point_model_a, highlight_point_model_b): self.canvas = self.ax.figure.canvas self._in_drag = False - self.marker_line_a = self.ax.axhline(highlight_point_model_a.get_point(), - linewidth=3, color="r") + self.marker_line_a = self.ax.axhline( + highlight_point_model_a.get_point(), linewidth=3, color="r" + ) if self.highlight_point_model_b: - self.marker_line_b = self.ax.axhline(highlight_point_model_b.get_point(), - linewidth=3, color="r") + self.marker_line_b = self.ax.axhline( + highlight_point_model_b.get_point(), linewidth=3, color="r" + ) self.canvas.mpl_connect("button_press_event", self._on_button_press) self.canvas.mpl_connect("motion_notify_event", self._on_motion) - self.canvas.mpl_connect("button_release_event", - self._on_button_release) + self.canvas.mpl_connect("button_release_event", self._on_button_release) self.highlight_point_model_a.trigger.add_callback(self._refresh) if highlight_point_model_b: @@ -816,17 +895,22 @@ def _refresh(self): self.canvas.draw() -class GamutViewer2D(object): - def __init__(self, ax, highlight_point_model, uniform_space, - ap_lim=(-50, 50), bp_lim=(-50, 50)): +class GamutViewer2D: + def __init__( + self, + ax, + highlight_point_model, + uniform_space, + ap_lim=(-50, 50), + bp_lim=(-50, 50), + ): self.ax = ax self.highlight_point_model = highlight_point_model self.ap_lim = ap_lim self.bp_lim = bp_lim self.uniform_space = uniform_space - self.bgcolors = {"light": (0.9, 0.9, 0.9), - "dark": (0.1, 0.1, 0.1)} + self.bgcolors = {"light": (0.9, 0.9, 0.9), "dark": (0.1, 0.1, 0.1)} # We want some hysteresis, so that there's no point where wiggling the # line back and forth causes background flickering. self.bgcolor_ranges = {"light": (0, 60), "dark": (40, 100)} @@ -834,9 +918,9 @@ def __init__(self, ax, highlight_point_model, uniform_space, self.bg = "light" self.ax.set_facecolor(self.bgcolors[self.bg]) - self.image = self.ax.imshow([[[0, 0, 0]]], aspect="equal", - extent=ap_lim + bp_lim, - origin="lower") + self.image = self.ax.imshow( + [[[0, 0, 0]]], aspect="equal", extent=ap_lim + bp_lim, origin="lower" + ) self.highlight_point_model.trigger.add_callback(self._refresh) @@ -846,12 +930,11 @@ def _refresh(self): if not (low <= Jp <= high): self.bg = self.bg_opposites[self.bg] self.ax.set_facecolor(self.bgcolors[self.bg]) - sRGB = sRGB_gamut_Jp_slice(Jp, self.uniform_space, - self.ap_lim, self.bp_lim) + sRGB = sRGB_gamut_Jp_slice(Jp, self.uniform_space, self.ap_lim, self.bp_lim) self.image.set_data(sRGB) -class HighlightPoint2DView(object): +class HighlightPoint2DView: def __init__(self, ax, highlight_point_model): self.ax = ax self.highlight_point_model = highlight_point_model @@ -871,27 +954,33 @@ def loadpyfile(path): is_native = True cmtype = "linear" method = "Bezier" - ns = {'__name__': '', - '__file__': os.path.basename(path), - } - with open(args.colormap) as f: - code = compile(f.read(), - os.path.basename(args.colormap), - 'exec') + ns = { + "__name__": "", + "__file__": os.path.basename(path), + } + + # FIXME: Should be `args.colormap` should be `path`? + with open(args.colormap) as f: # noqa: F821 + code = compile( + f.read(), + os.path.basename(args.colormap), # noqa: F821 + "exec", + ) exec(code, globals(), ns) - params = ns.get('parameters', {}) + params = ns.get("parameters", {}) if "min_JK" in params: params["min_Jp"] = params.pop("min_JK") params["max_Jp"] = params.pop("max_JK") cmap = ns.get("test_cm", None) return params, cmtype, cmap.name, cmap, is_native, method -class Colormap(object): + +class Colormap: def __init__(self, cmtype, method, uniform_space): self.can_edit = True - self.params = {} - self.cmtype = cmtype + self.params = {} + self.cmtype = cmtype self.method = method self.name = None self.cmap = None @@ -907,15 +996,14 @@ def load(self, path): self.can_edit = True self.cmtype = "linear" self.method = "Bezier" - ns = {'__name__': '', - '__file__': os.path.basename(self.path), - } + ns = { + "__name__": "", + "__file__": os.path.basename(self.path), + } with open(self.path) as f: - code = compile(f.read(), - os.path.basename(self.path), - 'exec') + code = compile(f.read(), os.path.basename(self.path), "exec") exec(code, globals(), ns) - self.params = ns.get('parameters', {}) + self.params = ns.get("parameters", {}) if not self.params: self.can_edit = False if "min_JK" in self.params: @@ -929,30 +1017,59 @@ def load(self, path): data = json.loads(f.read()) self.name = data["name"] colors = data["colors"] - colors = [colors[i:i + 6] for i in range(0, len(colors), 6)] - colors = [[int(c[2 * i:2 * i + 2], 16) / 255 for i in range(3)] for c in colors] + colors = [colors[i : i + 6] for i in range(0, len(colors), 6)] + colors = [ + [int(c[2 * i : 2 * i + 2], 16) / 255 for i in range(3)] + for c in colors + ] self.cmap = matplotlib.colors.ListedColormap(colors, self.name) - if "extensions" in data and "https://matplotlib.org/viscm" in data["extensions"]: + if ( + "extensions" in data + and "https://matplotlib.org/viscm" in data["extensions"] + ): self.can_edit = True - self.params = {k:v for k,v in data["extensions"]["https://matplotlib.org/viscm"].items() - if k in {"xp", "yp", "min_Jp", "max_Jp", "fixed", "filter_k", "uniform_space"}} + self.params = { + k: v + for k, v in data["extensions"][ + "https://matplotlib.org/viscm" + ].items() + if k + in { + "xp", + "yp", + "min_Jp", + "max_Jp", + "fixed", + "filter_k", + "uniform_space", + } + } self.params["name"] = self.name - self.cmtype = data["extensions"]["https://matplotlib.org/viscm"]["cmtype"] - self.method = data["extensions"]["https://matplotlib.org/viscm"]["spline_method"] - self.uniform_space = data["extensions"]["https://matplotlib.org/viscm"]["uniform_colorspace"] + self.cmtype = data["extensions"][ + "https://matplotlib.org/viscm" + ]["cmtype"] + self.method = data["extensions"][ + "https://matplotlib.org/viscm" + ]["spline_method"] + self.uniform_space = data["extensions"][ + "https://matplotlib.org/viscm" + ]["uniform_colorspace"] else: sys.exit("Unsupported filetype") else: self.can_edit = False self.cmap = lookup_colormap_by_name(path) self.name = path - + def about(): - QtWidgets.QMessageBox.about(None, "VISCM", - "Copyright (C) 2015-2016 Nathaniel Smith\n" + - "Copyright (C) 2015-2016 Stéfan van der Walt\n" - "Copyright (C) 2016 Hankun Zhao") + QtWidgets.QMessageBox.about( + None, + "VISCM", + "Copyright (C) 2015-2016 Nathaniel Smith\n" + + "Copyright (C) 2015-2016 Stéfan van der Walt\n" + "Copyright (C) 2016 Hankun Zhao", + ) class ViewerWindow(QtWidgets.QMainWindow): @@ -962,18 +1079,17 @@ def __init__(self, figurecanvas, viscm, cmapname, parent=None): self.main_widget = QtWidgets.QWidget(self) self.cmapname = cmapname - file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu = QtWidgets.QMenu("&File", self) + file_menu.addAction("&Save", self.save, QtCore.Qt.CTRL + QtCore.Qt.Key_S) + file_menu.addAction("&Quit", self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) - options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Toggle Gamut', self.toggle_gamut, - QtCore.Qt.CTRL + QtCore.Qt.Key_G) + options_menu = QtWidgets.QMenu("&Options", self) + options_menu.addAction( + "&Toggle Gamut", self.toggle_gamut, QtCore.Qt.CTRL + QtCore.Qt.Key_G + ) - help_menu = QtWidgets.QMenu('&Help', self) - help_menu.addAction('&About', about) + help_menu = QtWidgets.QMenu("&Help", self) + help_menu.addAction("&About", about) self.menuBar().addMenu(file_menu) self.menuBar().addMenu(options_menu) @@ -1003,7 +1119,8 @@ def save(self): fileName, _ = _getSaveFileName( caption="Save file", directory=self.cmapname + ".png", - filter="Image Files (*.png *.jpg *.bmp)") + filter="Image Files (*.png *.jpg *.bmp)", + ) if fileName: self.viscm.save_figure(fileName) @@ -1014,19 +1131,18 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self.viscm_editor = viscm_editor - file_menu = QtWidgets.QMenu('&File', self) - file_menu.addAction('&Save', self.save, - QtCore.Qt.CTRL + QtCore.Qt.Key_S) + file_menu = QtWidgets.QMenu("&File", self) + file_menu.addAction("&Save", self.save, QtCore.Qt.CTRL + QtCore.Qt.Key_S) file_menu.addAction("&Export .py", self.export) - file_menu.addAction('&Quit', self.fileQuit, - QtCore.Qt.CTRL + QtCore.Qt.Key_Q) + file_menu.addAction("&Quit", self.fileQuit, QtCore.Qt.CTRL + QtCore.Qt.Key_Q) - options_menu = QtWidgets.QMenu('&Options', self) - options_menu.addAction('&Load in Viewer', self.loadviewer, - QtCore.Qt.CTRL + QtCore.Qt.Key_V) + options_menu = QtWidgets.QMenu("&Options", self) + options_menu.addAction( + "&Load in Viewer", self.loadviewer, QtCore.Qt.CTRL + QtCore.Qt.Key_V + ) - help_menu = QtWidgets.QMenu('&Help', self) - help_menu.addAction('&About', about) + help_menu = QtWidgets.QMenu("&Help", self) + help_menu.addAction("&About", about) self.menuBar().addMenu(file_menu) self.menuBar().addMenu(options_menu) @@ -1094,8 +1210,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.smoothness_slider_num = smoothness_slider_num smoothness_slider_layout = QtWidgets.QHBoxLayout() - smoothness_slider_layout.addWidget( - QtWidgets.QLabel("Transition sharpness")) + smoothness_slider_layout.addWidget(QtWidgets.QLabel("Transition sharpness")) smoothness_slider_layout.addWidget(smoothness_slider) smoothness_slider_layout.addWidget(smoothness_slider_num) @@ -1103,7 +1218,8 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.smoothness_slider = smoothness_slider viscm_editor.cmap_model.filter_k_trigger.add_callback( - self.update_smoothness_slider) + self.update_smoothness_slider + ) self.moveAction = QtWidgets.QAction("Drag points", self) self.moveAction.triggered.connect(self.set_move_mode) @@ -1122,11 +1238,10 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): renameAction = QtWidgets.QAction("Rename colormap", self) renameAction.triggered.connect(self.rename) - saveAction = QtWidgets.QAction('Save', self) + saveAction = QtWidgets.QAction("Save", self) saveAction.triggered.connect(self.save) - - self.toolbar = self.addToolBar('Tools') + self.toolbar = self.addToolBar("Tools") self.toolbar.addAction(self.moveAction) self.toolbar.addAction(self.addAction) self.toolbar.addAction(self.removeAction) @@ -1145,8 +1260,8 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): def rename(self): name, ok = QtWidgets.QInputDialog.getText( - self, "Rename your colormap", "Enter a name", - text=self.viscm_editor.name) + self, "Rename your colormap", "Enter a name", text=self.viscm_editor.name + ) self.viscm_editor.name = name self.setWindowTitle("VISCM Editing : " + self.viscm_editor.name) @@ -1157,9 +1272,8 @@ def smoothness_slider_moved(self): def update_smoothness_slider(self): filter_k = self.viscm_editor.cmap_model.filter_k - self.smoothness_slider_num.setText("{:0.2f}".format(filter_k)) - self.smoothness_slider.setValue( - int(round(np.log10(filter_k) * 1000))) + self.smoothness_slider_num.setText(f"{filter_k:0.2f}") + self.smoothness_slider.setValue(int(round(np.log10(filter_k) * 1000))) def swapjp(self): jp1, jp2 = self.min_slider.value(), self.max_slider.value() @@ -1193,7 +1307,8 @@ def export(self): fileName, _ = _getSaveFileName( caption="Export file", directory=self.viscm_editor.name + ".py", - filter=".py (*.py)") + filter=".py (*.py)", + ) if fileName: self.viscm_editor.export_py(fileName) @@ -1207,7 +1322,8 @@ def save(self): fileName, _ = _getSaveFileName( caption="Save file", directory=self.viscm_editor.name + ".jscm", - filter="JSCM Files (*.jscm)") + filter="JSCM Files (*.jscm)", + ) if fileName: self.viscm_editor.save_colormap(fileName) @@ -1217,8 +1333,9 @@ def loadviewer(self): cm = self.viscm_editor.show_viscm() v = viscm(cm, name=self.viscm_editor.name, figure=newfig) - newcanvas.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Expanding) + newcanvas.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding + ) newcanvas.updateGeometry() newwindow = ViewerWindow(newcanvas, v, self.viscm_editor.name, parent=self) diff --git a/viscm/minimvc.py b/viscm/minimvc.py index c10ab6f..9174993 100644 --- a/viscm/minimvc.py +++ b/viscm/minimvc.py @@ -3,7 +3,8 @@ # Copyright (C) 2015 Stefan van der Walt # See file LICENSE.txt for license information. -class Trigger(object): + +class Trigger: def __init__(self): self._callbacks = set() From 8aa7bb01440aeca6f8bbcefe0671c28f2ce284c6 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:06:23 -0600 Subject: [PATCH 13/47] Add git-blame-ignore-revs entry for ruff/black application --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..58cc7d8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Applied `ruff` and `black` (gh-64) +1fec42d0baf90e00d510efd76cb6006fa0c70dc4 From c51735ed56ab586bc6458989fcaa4a84bdef4453 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:09:33 -0600 Subject: [PATCH 14/47] Drop Python 3.7 support --- README.rst | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c6c76c7..b55db2d 100644 --- a/README.rst +++ b/README.rst @@ -24,7 +24,7 @@ Contact: Nathaniel J. Smith and Stéfan van der Walt Dependencies: - * Python 3.7+ + * Python 3.8+ * `colorspacious `_ * Matplotlib * NumPy diff --git a/pyproject.toml b/pyproject.toml index e5146ae..4871ebb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python :: 3", ] -requires-python = "~=3.7" +requires-python = "~=3.8" dependencies = [ "numpy", "matplotlib", @@ -46,10 +46,10 @@ package-data = {viscm = ["examples/*"]} [tool.black] -target-version = ["py37", "py38", "py39", "py310", "py311"] +target-version = ["py38", "py39", "py310", "py311"] [tool.ruff] -target-version = "py37" +target-version = "py38" select = ["F", "E", "W", "C90", "I", "UP", "YTT", "B", "A", "C4", "T10", "RUF"] [tool.ruff.mccabe] From ccf11014cf8e763978dc56a198a67e718ffb9f2f Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:27:37 -0600 Subject: [PATCH 15/47] Run pre-commit checks in CI --- .github/workflows/check.yml | 26 ++++++++++++++++++++++++++ .pre-commit-config.yaml | 2 ++ 2 files changed, 28 insertions(+) create mode 100644 .github/workflows/check.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..4c7a9d5 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,26 @@ +name: "Check with pre-commit" +on: + - "push" + - "pull_request" + +jobs: + + check-with-pre-commit: + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: ["3.11"] + + steps: + - uses: "actions/checkout@v3" + + - name: "Set up Python ${{ matrix.python-version }}" + uses: "actions/setup-python@v4" + with: + python-version: "${{ matrix.python-version }}" + + - name: "Install pre-commit" + run: "pip install pre-commit" + + - name: "Run checks with pre-commit" + run: "pre-commit run --all-files --show-diff-on-failure --color always" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d58682..cbacefd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,8 @@ repos: rev: "v0.0.269" hooks: - id: "ruff" + # NOTE: "--exit-non-zero-on-fix" is important for CI to function + # correctly! args: ["--fix", "--exit-non-zero-on-fix"] - repo: "https://github.com/psf/black" From 2c63ce5f2eb679e13d1284c5d64857ddb9d1b4c6 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 21 May 2023 13:47:03 -0600 Subject: [PATCH 16/47] Add example commits for our formatting best practice --- doc/contributing.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/contributing.md b/doc/contributing.md index a7de17e..45cc0df 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -34,4 +34,6 @@ trigger this manually with `pre-commit run --all-files`. Thanks to pre-commit, all commits should be formatted. In cases where formatting needs to be fixed (e.g. changing config of a linter), a format-only commit should be created, and then another commit should immediately follow which updates -`.git-blame-ignore-revs`. +`.git-blame-ignore-revs`. For example: +[1fec42d](https://github.com/matplotlib/viscm/pull/64/commits/1fec42d0baf90e00d510efd76cb6006fa0c70dc4), +[8aa7bb0](https://github.com/matplotlib/viscm/pull/64/commits/8aa7bb01440aeca6f8bbcefe0671c28f2ce284c6). From fef7e7cb79edd31e7fcdcf380a821ff675e00022 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Thu, 1 Jun 2023 13:21:03 -0600 Subject: [PATCH 17/47] Add pre-commit checks for common mistakes --- .pre-commit-config.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbacefd..d99fe97 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,13 @@ repos: + - repo: "https://github.com/pre-commit/pre-commit-hooks" + rev: "v4.4.0" + hooks: + - id: "check-added-large-files" + - id: "check-vcs-permalinks" + - id: "no-commit-to-branch" + # TODO: Apply this fixer and add an entry to .git-blame-ignore-revs + # - id: "end-of-file-fixer" + - repo: "https://github.com/charliermarsh/ruff-pre-commit" rev: "v0.0.269" hooks: From 1a8779b0cc189a94902e1caccbcd99f2fed557be Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Thu, 1 Jun 2023 13:26:34 -0600 Subject: [PATCH 18/47] Put pre-commit command in a Makefile --- .github/workflows/check.yml | 4 ++-- Makefile | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 Makefile diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4c7a9d5..e3caf78 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -22,5 +22,5 @@ jobs: - name: "Install pre-commit" run: "pip install pre-commit" - - name: "Run checks with pre-commit" - run: "pre-commit run --all-files --show-diff-on-failure --color always" + - name: "Run lint and format checks" + run: "make lint" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3ea713f --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +.PHONY: lint +lint: + pre-commit run --all-files --show-diff-on-failure --color always From 5177edc0ac1767d738f6dc80ab4350c7cc5fd477 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Thu, 1 Jun 2023 19:03:25 -0600 Subject: [PATCH 19/47] Fixup unit test The unit test appears to have fallen out of sync with the code long ago (e.g. it seems to previously expect the values from `get_sRGB` to range from 0-255). I did my best to restore it, but at this point I'm not 100% sure why the colormaps only match approximately after applying these fixes. --- .github/workflows/test.yml | 35 ++++++ .travis.yml | 20 --- Makefile | 11 ++ environment.yml | 4 + tests.py => test/test_editor_loads_native.py | 123 +++++++++++-------- 5 files changed, 121 insertions(+), 72 deletions(-) create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml rename tests.py => test/test_editor_loads_native.py (56%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..131562e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: "Unit test" +on: + - "push" + - "pull_request" + +jobs: + + unit-test-with-pytest: + runs-on: "ubuntu-latest" + strategy: + matrix: + python-version: + - "3.11" + - "3.10" + - "3.9" + - "3.8" + pyqt-dependency: + - "PyQt5" + # - "PyQt6" + + steps: + - uses: "actions/checkout@v3" + + - name: "Set up Python ${{ matrix.python-version }}" + uses: "actions/setup-python@v4" + with: + python-version: "${{ matrix.python-version }}" + + - name: "Install dependencies in pyproject.toml" + run: | + pip install . + pip install pytest pytest-cov ${{ matrix.pyqt-dependency }} + + - name: "Run tests" + run: "make test" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index df3b66d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,20 +0,0 @@ -language: python -python: - - "2.7" - - "3.4" - - "3.5" -addon: - apt: - packages: - - python-qt4 - -before_install: - - pip install --no-index --trusted-host travis-wheels.scikit-image.org --find-links=http://travis-wheels.scikit-image.org numpy scipy matplotlib PySide - - pip install colorspacious - - pip install pytest pytest-cov - - python $(dirname $(which python))/pyside_postinstall.py -install - - sh -e /etc/init.d/xvfb start - - export DISPLAY=:99 -script: - - "py.test tests.py" -sudo: false \ No newline at end of file diff --git a/Makefile b/Makefile index 3ea713f..77aaa6c 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,14 @@ +.PHONY: test +test: + python -m pytest --version + python -m pytest test/ + + .PHONY: lint lint: + pre-commit --version pre-commit run --all-files --show-diff-on-failure --color always + + +.PHONY: ci +ci: lint test diff --git a/environment.yml b/environment.yml index 21689cc..e198e5d 100644 --- a/environment.yml +++ b/environment.yml @@ -12,6 +12,10 @@ dependencies: # Development - "pre-commit" + - "pytest" + - "pytest-cov" + - "pytest-qt" + - "pytest-xvfb" - pip: - "build ~=0.10" diff --git a/tests.py b/test/test_editor_loads_native.py similarity index 56% rename from tests.py rename to test/test_editor_loads_native.py index ac8d37f..03e44e9 100644 --- a/tests.py +++ b/test/test_editor_loads_native.py @@ -1,57 +1,80 @@ -import numpy as np +import json + +import pytest -from viscm.bezierbuilder import json from viscm.gui import Colormap, viscm_editor -cms = { - "viscm/examples/sample_linear.jscm", - "viscm/examples/sample_diverging.jscm", - "viscm/examples/sample_diverging_continuous.jscm", -} - - -def test_editor_loads_native(): - for k in cms: - with open(k) as f: - data = json.loads(f.read()) - cm = Colormap(None, "CatmulClark", "CAM02-UCS") - cm.load(k) - viscm = viscm_editor( - uniform_space=cm.uniform_space, - cmtype=cm.cmtype, - method=cm.method, - **cm.params, - ) - assert viscm.name == data["name"] - - extensions = data["extensions"]["https://matplotlib.org/viscm"] - xp, yp, fixed = viscm.control_point_model.get_control_points() - - assert extensions["fixed"] == fixed - assert len(extensions["xp"]) == len(xp) - assert len(extensions["yp"]) == len(yp) - assert len(xp) == len(yp) - for i in range(len(xp)): - assert extensions["xp"][i] == xp[i] - assert extensions["yp"][i] == yp[i] - assert extensions["min_Jp"] == viscm.min_Jp - assert extensions["max_Jp"] == viscm.max_Jp - assert extensions["filter_k"] == viscm.filter_k - assert extensions["cmtype"] == viscm.cmtype - - colors = data["colors"] - colors = [ - [int(c[i : i + 2], 16) / 256 for i in range(0, 6, 2)] - for c in [colors[i : i + 6] for i in range(0, len(colors), 6)] - ] - editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() - for i in range(len(colors)): - for z in range(3): - assert colors[i][z] == np.rint(editor_colors[i][z] / 256) + +def approxeq(x, y, *, err=0.0001): + return abs(y - x) < err + + +@pytest.mark.parametrize( + "colormap_file", + [ + "viscm/examples/sample_linear.jscm", + "viscm/examples/sample_diverging.jscm", + "viscm/examples/sample_diverging_continuous.jscm", + ], +) +def test_editor_loads_native(colormap_file): + with open(colormap_file) as f: + data = json.loads(f.read()) + cm = Colormap(None, "CatmulClark", "CAM02-UCS") + cm.load(colormap_file) + viscm = viscm_editor( + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) + assert viscm.name == data["name"] + + extensions = data["extensions"]["https://matplotlib.org/viscm"] + xp, yp, fixed = viscm.control_point_model.get_control_points() + + assert extensions["fixed"] == fixed + assert len(extensions["xp"]) == len(xp) + assert len(extensions["yp"]) == len(yp) + assert len(xp) == len(yp) + for i in range(len(xp)): + assert extensions["xp"][i] == xp[i] + assert extensions["yp"][i] == yp[i] + assert extensions["min_Jp"] == viscm.min_Jp + assert extensions["max_Jp"] == viscm.max_Jp + assert extensions["filter_k"] == viscm.cmap_model.filter_k + assert extensions["cmtype"] == viscm.cmtype + + # Decode hexadecimal-encoded colormap string (grouped in units of 3 pairs of + # two-character (0-255) values) to 3-tuples of floats (0-1). + colors_hex = data["colors"] + colors_hex = [colors_hex[i : i + 6] for i in range(0, len(colors_hex), 6)] + colors = [ + # TODO: Should we divide by 255 here instead of 256? The tests pass with a + # lower value for `err` if we do. + [int(c[i : i + 2], 16) / 256 for i in range(0, len(c), 2)] + for c in colors_hex + ] + + editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() + + for i in range(len(colors)): + for z in range(3): + assert approxeq(colors[i][z], editor_colors[i][z], err=0.01) # import matplotlib as mpl -# from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +# try: +# from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas +# except ImportError: +# try: +# from matplotlib.backends.backend_qt5agg import ( +# FigureCanvasQTAgg as FigureCanvas +# ) +# except ImportError: +# from matplotlib.backends.backend_qt4agg import ( +# FigureCanvasQTAgg as FigureCanvas +# ) # from matplotlib.backends.qt_compat import QtCore, QtGui # # def test_editor_add_point(): @@ -144,7 +167,3 @@ def test_editor_loads_native(): # print(linear.control_point_model.get_control_points()) # # print(linear.cmap_model.get_Jpapbp(3)) - - -def approxeq(x, y, err=0.0001): - return abs(y - x) < err From 70b4918434689b052aa6061c75281b1f12a376d6 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 18:54:00 -0600 Subject: [PATCH 20/47] Fix hex decoding logic --- test/test_editor_loads_native.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/test_editor_loads_native.py b/test/test_editor_loads_native.py index 03e44e9..2f45ca8 100644 --- a/test/test_editor_loads_native.py +++ b/test/test_editor_loads_native.py @@ -50,10 +50,7 @@ def test_editor_loads_native(colormap_file): colors_hex = data["colors"] colors_hex = [colors_hex[i : i + 6] for i in range(0, len(colors_hex), 6)] colors = [ - # TODO: Should we divide by 255 here instead of 256? The tests pass with a - # lower value for `err` if we do. - [int(c[i : i + 2], 16) / 256 for i in range(0, len(c), 2)] - for c in colors_hex + [int(c[i : i + 2], 16) / 255 for i in range(0, len(c), 2)] for c in colors_hex ] editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() From beb1a97f8b252b0a1cfdff3756b843b08b96d1e3 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 18:54:11 -0600 Subject: [PATCH 21/47] Restore old assertion and mark xfail --- test/test_editor_loads_native.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_editor_loads_native.py b/test/test_editor_loads_native.py index 2f45ca8..f360b04 100644 --- a/test/test_editor_loads_native.py +++ b/test/test_editor_loads_native.py @@ -1,5 +1,6 @@ import json +import numpy as np import pytest from viscm.gui import Colormap, viscm_editor @@ -17,6 +18,7 @@ def approxeq(x, y, *, err=0.0001): "viscm/examples/sample_diverging_continuous.jscm", ], ) +@pytest.mark.xfail(reason="Test very old; intent unclear") def test_editor_loads_native(colormap_file): with open(colormap_file) as f: data = json.loads(f.read()) @@ -57,7 +59,11 @@ def test_editor_loads_native(colormap_file): for i in range(len(colors)): for z in range(3): - assert approxeq(colors[i][z], editor_colors[i][z], err=0.01) + # FIXME: The right-hand side of this comparison will always be 0. + # https://github.com/matplotlib/viscm/pull/66#discussion_r1213818015 + assert colors[i][z] == np.rint(editor_colors[i][z] / 256) + # Should the test look more like this? + # assert approxeq(colors[i][z], editor_colors[i][z], err=0.005) # import matplotlib as mpl From 355688f47d112378468ae8514277bffddaca8a31 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 11:31:17 -0600 Subject: [PATCH 22/47] Refactor unit tests to test smaller units at a time --- Makefile | 2 +- ...or_loads_native.py => test_editor_load.py} | 114 +++++++++++------- 2 files changed, 69 insertions(+), 47 deletions(-) rename test/{test_editor_loads_native.py => test_editor_load.py} (58%) diff --git a/Makefile b/Makefile index 77aaa6c..b8cc123 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test test: python -m pytest --version - python -m pytest test/ + python -m pytest -v test/ .PHONY: lint diff --git a/test/test_editor_loads_native.py b/test/test_editor_load.py similarity index 58% rename from test/test_editor_loads_native.py rename to test/test_editor_load.py index f360b04..c974b61 100644 --- a/test/test_editor_loads_native.py +++ b/test/test_editor_load.py @@ -18,52 +18,74 @@ def approxeq(x, y, *, err=0.0001): "viscm/examples/sample_diverging_continuous.jscm", ], ) -@pytest.mark.xfail(reason="Test very old; intent unclear") -def test_editor_loads_native(colormap_file): - with open(colormap_file) as f: - data = json.loads(f.read()) - cm = Colormap(None, "CatmulClark", "CAM02-UCS") - cm.load(colormap_file) - viscm = viscm_editor( - uniform_space=cm.uniform_space, - cmtype=cm.cmtype, - method=cm.method, - **cm.params, - ) - assert viscm.name == data["name"] - - extensions = data["extensions"]["https://matplotlib.org/viscm"] - xp, yp, fixed = viscm.control_point_model.get_control_points() - - assert extensions["fixed"] == fixed - assert len(extensions["xp"]) == len(xp) - assert len(extensions["yp"]) == len(yp) - assert len(xp) == len(yp) - for i in range(len(xp)): - assert extensions["xp"][i] == xp[i] - assert extensions["yp"][i] == yp[i] - assert extensions["min_Jp"] == viscm.min_Jp - assert extensions["max_Jp"] == viscm.max_Jp - assert extensions["filter_k"] == viscm.cmap_model.filter_k - assert extensions["cmtype"] == viscm.cmtype - - # Decode hexadecimal-encoded colormap string (grouped in units of 3 pairs of - # two-character (0-255) values) to 3-tuples of floats (0-1). - colors_hex = data["colors"] - colors_hex = [colors_hex[i : i + 6] for i in range(0, len(colors_hex), 6)] - colors = [ - [int(c[i : i + 2], 16) / 255 for i in range(0, len(c), 2)] for c in colors_hex - ] - - editor_colors = viscm.cmap_model.get_sRGB(num=256)[0].tolist() - - for i in range(len(colors)): - for z in range(3): - # FIXME: The right-hand side of this comparison will always be 0. - # https://github.com/matplotlib/viscm/pull/66#discussion_r1213818015 - assert colors[i][z] == np.rint(editor_colors[i][z] / 256) - # Should the test look more like this? - # assert approxeq(colors[i][z], editor_colors[i][z], err=0.005) +class TestEditorLoad: + def expected(self, colormap_file): + with open(colormap_file) as f: + exp = json.loads(f.read()) + return exp + + def actual(self, colormap_file): + cm = Colormap(None, "CatmulClark", "CAM02-UCS") + cm.load(colormap_file) + act = viscm_editor( + uniform_space=cm.uniform_space, + cmtype=cm.cmtype, + method=cm.method, + **cm.params, + ) + return act + + def test_editor_loads_jscm_parameters_match(self, colormap_file): + expected = self.expected(colormap_file) + actual = self.actual(colormap_file) + + assert actual.name == expected["name"] + + extensions = expected["extensions"]["https://matplotlib.org/viscm"] + xp, yp, fixed = actual.control_point_model.get_control_points() + + assert extensions["fixed"] == fixed + assert len(extensions["xp"]) == len(xp) + assert len(extensions["yp"]) == len(yp) + assert len(xp) == len(yp) + for i in range(len(xp)): + assert extensions["xp"][i] == xp[i] + assert extensions["yp"][i] == yp[i] + assert extensions["min_Jp"] == actual.min_Jp + assert extensions["max_Jp"] == actual.max_Jp + assert extensions["filter_k"] == actual.cmap_model.filter_k + assert extensions["cmtype"] == actual.cmtype + + @pytest.mark.xfail(reason="Test very old; intent unclear") + def test_editor_loads_jscm_data_match(self, colormap_file): + expected = self.expected(colormap_file) + actual = self.actual(colormap_file) + + # Decode hexadecimal-encoded colormap string (grouped in units of 3 pairs of + # two-character [00-ff / 0-255] values) to 3-tuples of floats (0-1). + expected_colors_hex = expected["colors"] + expected_colors_hex = [ + expected_colors_hex[i : i + 6] + for i in range(0, len(expected_colors_hex), 6) + ] + expected_colors = [ + [int(c[i : i + 2], 16) / 255 for i in range(0, len(c), 2)] + for c in expected_colors_hex + ] + + actual_colors = actual.cmap_model.get_sRGB(num=256)[0].tolist() + + for i in range(len(expected_colors)): + for z in range(3): + # FIXME: The right-hand side of this comparison will always be 0. + # https://github.com/matplotlib/viscm/pull/66#discussion_r1213818015 + assert actual_colors[i][z] == np.rint(expected_colors[i][z] / 256) + # Should the test look more like this? + # assert approxeq( + # expected_colors[i][z], + # actual_colors[i][z], + # err=0.005, + # ) # import matplotlib as mpl From 64fea8193efe8c506d63f81fbb6b45b2cfb9523c Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 11:34:25 -0600 Subject: [PATCH 23/47] Require newline at end of file --- .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d99fe97..9672b73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,8 +5,7 @@ repos: - id: "check-added-large-files" - id: "check-vcs-permalinks" - id: "no-commit-to-branch" - # TODO: Apply this fixer and add an entry to .git-blame-ignore-revs - # - id: "end-of-file-fixer" + - id: "end-of-file-fixer" - repo: "https://github.com/charliermarsh/ruff-pre-commit" rev: "v0.0.269" From e9104b3616899f54257bb38959d6e1c0acc70f6a Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 11:34:57 -0600 Subject: [PATCH 24/47] Apply end-of-file-fixer --- .gitignore | 1 - viscm/examples/sample_diverging.jscm | 2 +- viscm/examples/sample_diverging_continuous.jscm | 2 +- viscm/examples/sample_linear.jscm | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2b5b1d3..eea21e2 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,3 @@ coverage.xml # Sphinx documentation docs/_build/ - diff --git a/viscm/examples/sample_diverging.jscm b/viscm/examples/sample_diverging.jscm index b78bc23..c491b71 100644 --- a/viscm/examples/sample_diverging.jscm +++ b/viscm/examples/sample_diverging.jscm @@ -37,4 +37,4 @@ "domain": "continuous", "content-type": "application/vnd.matplotlib.colormap-v1+json", "license": "http://creativecommons.org/publicdomain/zero/1.0/" -} \ No newline at end of file +} diff --git a/viscm/examples/sample_diverging_continuous.jscm b/viscm/examples/sample_diverging_continuous.jscm index 6f3eaf6..055a1de 100644 --- a/viscm/examples/sample_diverging_continuous.jscm +++ b/viscm/examples/sample_diverging_continuous.jscm @@ -34,4 +34,4 @@ "colorspace": "sRGB", "name": "sample_diverging_continuous", "domain": "continuous" -} \ No newline at end of file +} diff --git a/viscm/examples/sample_linear.jscm b/viscm/examples/sample_linear.jscm index b8142de..e4a09c2 100644 --- a/viscm/examples/sample_linear.jscm +++ b/viscm/examples/sample_linear.jscm @@ -35,4 +35,4 @@ "cmtype": "linear" } } -} \ No newline at end of file +} From e7fcd3033d444438754e22043f1584e1129e2ff9 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 13:25:54 -0600 Subject: [PATCH 25/47] Refactor bezierbuilder to avoid `eval` calls. This will help with pep8 naming convention compliance without breaking the user-facing API. Specifically, we can lowercase the names of the Bezier and CatmulClark functions without having to lowercase the string argument values to the CLI. --- .../__init__.py} | 108 +++++------------- viscm/bezierbuilder/curve.py | 59 ++++++++++ 2 files changed, 86 insertions(+), 81 deletions(-) rename viscm/{bezierbuilder.py => bezierbuilder/__init__.py} (74%) create mode 100644 viscm/bezierbuilder/curve.py diff --git a/viscm/bezierbuilder.py b/viscm/bezierbuilder/__init__.py similarity index 74% rename from viscm/bezierbuilder.py rename to viscm/bezierbuilder/__init__.py index cbffbeb..0c7c199 100644 --- a/viscm/bezierbuilder.py +++ b/viscm/bezierbuilder/__init__.py @@ -1,44 +1,36 @@ -# BézierBuilder -# -# Copyright (c) 2013, Juan Luis Cano Rodríguez -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * 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. -# -# 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. - """BézierBuilder, an interactive Bézier curve explorer. -Just run it with - -$ python bezier_builder.py - +Copyright (c) 2013, Juan Luis Cano Rodríguez +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * 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. + +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. """ -from math import factorial - import numpy as np from matplotlib.backends.qt_compat import QtCore from matplotlib.lines import Line2D -from .minimvc import Trigger +from viscm.bezierbuilder.curve import curve_method +from viscm.minimvc import Trigger class ControlPointModel: @@ -193,7 +185,7 @@ def compute_arc_length(xp, yp, method, t=None, grid=256): class SingleBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): - self.method = eval(method) + self.method = curve_method[method] self.control_point_model = control_point_model x, y = self.get_bezier_points() self.bezier_curve = Line2D(x, y) @@ -215,7 +207,7 @@ def _refresh(self): class TwoBezierCurveModel: def __init__(self, control_point_model, method="CatmulClark"): - self.method = eval(method) + self.method = curve_method[method] self.control_point_model = control_point_model x, y = self.get_bezier_points() self.bezier_curve = Line2D(x, y) @@ -286,49 +278,3 @@ def _refresh(self): x, y = self.bezier_curve_model.get_bezier_points() self.bezier_curve.set_data(x, y) self.canvas.draw() - - -# We used to use scipy.special.binom here, -# but reimplementing it ourself lets us avoid pulling in a dependency -# scipy just for that one function. -def binom(n, k): - return factorial(n) * 1.0 / (factorial(k) * factorial(n - k)) - - -def Bernstein(n, k): - """Bernstein polynomial.""" - coeff = binom(n, k) - - def _bpoly(x): - return coeff * x**k * (1 - x) ** (n - k) - - return _bpoly - - -def Bezier(points, at): - """Build Bézier curve from points. - Deprecated. CatmulClark builds nicer splines - """ - at = np.asarray(at) - at_flat = at.ravel() - N = len(points) - curve = np.zeros((at_flat.shape[0], 2)) - for ii in range(N): - curve += np.outer(Bernstein(N - 1, ii)(at_flat), points[ii]) - return curve.reshape((*at.shape, 2)) - - -def CatmulClark(points, at): - points = np.asarray(points) - - while len(points) < len(at): - new_p = np.zeros((2 * len(points), 2)) - new_p[0] = points[0] - new_p[-1] = points[-1] - new_p[1:-2:2] = 3 / 4.0 * points[:-1] + 1 / 4.0 * points[1:] - new_p[2:-1:2] = 1 / 4.0 * points[:-1] + 3 / 4.0 * points[1:] - points = new_p - xp, yp = zip(*points) - xp = np.interp(at, np.linspace(0, 1, len(xp)), xp) - yp = np.interp(at, np.linspace(0, 1, len(yp)), yp) - return np.asarray(list(zip(xp, yp))) diff --git a/viscm/bezierbuilder/curve.py b/viscm/bezierbuilder/curve.py new file mode 100644 index 0000000..e11bf76 --- /dev/null +++ b/viscm/bezierbuilder/curve.py @@ -0,0 +1,59 @@ +from math import factorial + +import numpy as np + + +def binom(n, k): + """Re-implement `scipy.special.binom`. + + Reimplementing it ourself lets us avoid pulling in a dependency scipy just for that + one function. + + FIXME: `scipy` is already a dependency. Delete this? + """ + return factorial(n) * 1.0 / (factorial(k) * factorial(n - k)) + + +def Bernstein(n, k): + """Bernstein polynomial.""" + coeff = binom(n, k) + + def _bpoly(x): + return coeff * x**k * (1 - x) ** (n - k) + + return _bpoly + + +def Bezier(points, at): + """Build Bézier curve from points. + Deprecated. CatmulClark builds nicer splines + """ + at = np.asarray(at) + at_flat = at.ravel() + N = len(points) + curve = np.zeros((at_flat.shape[0], 2)) + for ii in range(N): + curve += np.outer(Bernstein(N - 1, ii)(at_flat), points[ii]) + return curve.reshape((*at.shape, 2)) + + +def CatmulClark(points, at): + points = np.asarray(points) + + while len(points) < len(at): + new_p = np.zeros((2 * len(points), 2)) + new_p[0] = points[0] + new_p[-1] = points[-1] + new_p[1:-2:2] = 3 / 4.0 * points[:-1] + 1 / 4.0 * points[1:] + new_p[2:-1:2] = 1 / 4.0 * points[:-1] + 3 / 4.0 * points[1:] + points = new_p + xp, yp = zip(*points) + xp = np.interp(at, np.linspace(0, 1, len(xp)), xp) + yp = np.interp(at, np.linspace(0, 1, len(yp)), yp) + return np.asarray(list(zip(xp, yp))) + + +curve_method = { + "Bezier": Bezier, + "CatmulClark": CatmulClark, +} From 41d26e68b0dbfe46b2b2e212d599d4e815e36a20 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 17:24:07 -0600 Subject: [PATCH 26/47] Ignore autoformatting in GitHub blame view --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 58cc7d8..9fdd13e 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,5 @@ # Applied `ruff` and `black` (gh-64) 1fec42d0baf90e00d510efd76cb6006fa0c70dc4 + +# Applied `pre-commit` `end-of-file-fixer` +e9104b3616899f54257bb38959d6e1c0acc70f6a From bbd66aad25c919103402d1039617b898509d7e91 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 17:26:09 -0600 Subject: [PATCH 27/47] Remove no-commit-to-branch pre-commit rule This rule was causing GitHub Actions to fail on merge commit. --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9672b73..b7cc1e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,6 @@ repos: hooks: - id: "check-added-large-files" - id: "check-vcs-permalinks" - - id: "no-commit-to-branch" - id: "end-of-file-fixer" - repo: "https://github.com/charliermarsh/ruff-pre-commit" From 205e26dda046be7d25dc2631097978b7dddabba4 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 19:25:46 -0600 Subject: [PATCH 28/47] Remove re-implemented `scipy.special.binom` function --- viscm/bezierbuilder/curve.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/viscm/bezierbuilder/curve.py b/viscm/bezierbuilder/curve.py index e11bf76..5c55feb 100644 --- a/viscm/bezierbuilder/curve.py +++ b/viscm/bezierbuilder/curve.py @@ -1,17 +1,5 @@ -from math import factorial - import numpy as np - - -def binom(n, k): - """Re-implement `scipy.special.binom`. - - Reimplementing it ourself lets us avoid pulling in a dependency scipy just for that - one function. - - FIXME: `scipy` is already a dependency. Delete this? - """ - return factorial(n) * 1.0 / (factorial(k) * factorial(n - k)) +from scipy.special import binom def Bernstein(n, k): From ea93bc2a2bbf5147ae0e39f91d50bd890e4af99e Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 19:31:38 -0600 Subject: [PATCH 29/47] Display a warning when using deprecated Bezier function --- viscm/bezierbuilder/curve.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/viscm/bezierbuilder/curve.py b/viscm/bezierbuilder/curve.py index 5c55feb..95e031f 100644 --- a/viscm/bezierbuilder/curve.py +++ b/viscm/bezierbuilder/curve.py @@ -1,3 +1,5 @@ +import warnings + import numpy as np from scipy.special import binom @@ -13,9 +15,13 @@ def _bpoly(x): def Bezier(points, at): - """Build Bézier curve from points. - Deprecated. CatmulClark builds nicer splines - """ + """Build Bézier curve from points.""" + warnings.warn( + message="Deprecated. CatmulClark builds nicer splines.", + category=FutureWarning, + stacklevel=1, + ) + at = np.asarray(at) at_flat = at.ravel() N = len(points) From c389db54e8168a9862c48697398db34fa121c2fc Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 17:03:20 -0600 Subject: [PATCH 30/47] Add `pep8-naming` rule to `ruff` Ignore the `gui.py` file for now. There's lots of pep8-naming rules being thrown for this, and some of them could result in API changes. Too much to tackle at the moment. --- pyproject.toml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4871ebb..7870101 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,24 @@ target-version = ["py38", "py39", "py310", "py311"] [tool.ruff] target-version = "py38" -select = ["F", "E", "W", "C90", "I", "UP", "YTT", "B", "A", "C4", "T10", "RUF"] +select = [ + "F", + "E", + "W", + "C90", + "I", + "N", + "UP", + "YTT", + "B", + "A", + "C4", + "T10", + "RUF", +] + +[tool.ruff.per-file-ignores] +"viscm/gui.py" = ["N8"] [tool.ruff.mccabe] max-complexity = 11 From 1eb35bbb81a8f3f72cf5c2cb44d3dd94d4c15c32 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 17:05:52 -0600 Subject: [PATCH 31/47] Fix pep8-naming violations --- viscm/bezierbuilder/curve.py | 16 ++++++++-------- viscm/cli.py | 12 ++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/viscm/bezierbuilder/curve.py b/viscm/bezierbuilder/curve.py index 95e031f..06e4a82 100644 --- a/viscm/bezierbuilder/curve.py +++ b/viscm/bezierbuilder/curve.py @@ -4,7 +4,7 @@ from scipy.special import binom -def Bernstein(n, k): +def bernstein(n, k): """Bernstein polynomial.""" coeff = binom(n, k) @@ -14,7 +14,7 @@ def _bpoly(x): return _bpoly -def Bezier(points, at): +def bezier(points, at): """Build Bézier curve from points.""" warnings.warn( message="Deprecated. CatmulClark builds nicer splines.", @@ -24,14 +24,14 @@ def Bezier(points, at): at = np.asarray(at) at_flat = at.ravel() - N = len(points) + n = len(points) curve = np.zeros((at_flat.shape[0], 2)) - for ii in range(N): - curve += np.outer(Bernstein(N - 1, ii)(at_flat), points[ii]) + for ii in range(n): + curve += np.outer(bernstein(n - 1, ii)(at_flat), points[ii]) return curve.reshape((*at.shape, 2)) -def CatmulClark(points, at): +def catmul_clark(points, at): points = np.asarray(points) while len(points) < len(at): @@ -48,6 +48,6 @@ def CatmulClark(points, at): curve_method = { - "Bezier": Bezier, - "CatmulClark": CatmulClark, + "Bezier": bezier, + "CatmulClark": catmul_clark, } diff --git a/viscm/cli.py b/viscm/cli.py index f7f80cc..3b349a1 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -92,9 +92,9 @@ def cli(): if cm is None: sys.exit("Please specify a colormap") fig = plt.figure() - figureCanvas = gui.FigureCanvas(fig) + figure_canvas = gui.FigureCanvas(fig) v = gui.viscm(cm.cmap, name=cm.name, figure=fig, uniform_space=cm.uniform_space) - mainwindow = gui.ViewerWindow(figureCanvas, v, cm.name) + mainwindow = gui.ViewerWindow(figure_canvas, v, cm.name) if args.save is not None: v.figure.set_size_inches(20, 12) v.figure.savefig(args.save) @@ -103,7 +103,7 @@ def cli(): sys.exit("Sorry, I don't know how to edit the specified colormap") # Hold a reference so it doesn't get GC'ed fig = plt.figure() - figureCanvas = gui.FigureCanvas(fig) + figure_canvas = gui.FigureCanvas(fig) v = gui.viscm_editor( figure=fig, uniform_space=cm.uniform_space, @@ -111,17 +111,17 @@ def cli(): method=cm.method, **cm.params, ) - mainwindow = gui.EditorWindow(figureCanvas, v) + mainwindow = gui.EditorWindow(figure_canvas, v) else: raise RuntimeError("can't happen") if args.quit: sys.exit() - figureCanvas.setSizePolicy( + figure_canvas.setSizePolicy( gui.QtWidgets.QSizePolicy.Expanding, gui.QtWidgets.QSizePolicy.Expanding ) - figureCanvas.updateGeometry() + figure_canvas.updateGeometry() mainwindow.resize(800, 600) mainwindow.show() From 7abd2207b3b0cdcb7a4ea6a52db08d0cdd4ae252 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Fri, 2 Jun 2023 17:07:24 -0600 Subject: [PATCH 32/47] Ignore lint fixes in GitHub's blame view --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 9fdd13e..912614e 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -3,3 +3,6 @@ # Applied `pre-commit` `end-of-file-fixer` e9104b3616899f54257bb38959d6e1c0acc70f6a + +# Applied `ruff` `pep8-naming` rules +1eb35bbb81a8f3f72cf5c2cb44d3dd94d4c15c32 From 3e767837e671e09ecbb8192c15eb8fd54e0aa252 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 4 Jun 2023 17:29:18 -0600 Subject: [PATCH 33/47] Add basic GUI tests --- .github/workflows/test.yml | 13 +- Makefile | 2 +- doc/contributing.md | 32 +++- test/conftest.py | 10 ++ test/data/option_d.py | 311 +++++++++++++++++++++++++++++++++++++ test/test_gui.py | 44 ++++++ viscm/cli.py | 99 ++++++++---- viscm/gui.py | 2 +- 8 files changed, 471 insertions(+), 42 deletions(-) create mode 100644 test/conftest.py create mode 100644 test/data/option_d.py create mode 100644 test/test_gui.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 131562e..9c7d542 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,10 +26,19 @@ jobs: with: python-version: "${{ matrix.python-version }}" - - name: "Install dependencies in pyproject.toml" + - name: "Install test and project dependencies" run: | + # Project dependencies from pyproject.toml + # NOTE: Also builds viscm. How do we avoid this? pip install . - pip install pytest pytest-cov ${{ matrix.pyqt-dependency }} + + # Test dependencies + pip install pytest pytest-cov pytest-qt pytest-xvfb ${{ matrix.pyqt-dependency }} + # pytest-qt CI dependencies: https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions + sudo apt update + sudo apt install -y \ + xvfb \ + libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils - name: "Run tests" run: "make test" diff --git a/Makefile b/Makefile index b8cc123..80f7393 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test test: python -m pytest --version - python -m pytest -v test/ + python -m pytest --xvfb-backend=xvfb -v test/ .PHONY: lint diff --git a/doc/contributing.md b/doc/contributing.md index 45cc0df..95fe327 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -2,28 +2,37 @@ Install development dependencies: -``` +```bash conda env create # or `mamba env create` ``` ## Development install -``` +```bash pip install -e . ``` ## Testing the build -``` +```bash rm -rf dist python -m build pip install dist/*.whl # or `dist/*.tar.gz` ``` -## Code formatting and linting +## Tests + +See `Makefile` for convenience commands. + + +### Code linting (and formatting) + +```bash +make lint +``` This codebase uses [black](https://black.readthedocs.io/en/stable/) and [ruff](https://github.com/charliermarsh/ruff) to automatically format and lint the code. @@ -37,3 +46,18 @@ and then another commit should immediately follow which updates `.git-blame-ignore-revs`. For example: [1fec42d](https://github.com/matplotlib/viscm/pull/64/commits/1fec42d0baf90e00d510efd76cb6006fa0c70dc4), [8aa7bb0](https://github.com/matplotlib/viscm/pull/64/commits/8aa7bb01440aeca6f8bbcefe0671c28f2ce284c6). + + +### Unit tests + +```bash +make test +``` + +Unit tests require `xvfb` (X Virtual Framebuffer) to test the GUI. If `xvfb` is not +installed, you'll receive `ERROR: xvfb backend xvfb requested but not installed`. +Install with: + +```bash +sudo apt install xvfb +``` diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..0c1de49 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,10 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def tests_data_dir() -> Path: + tests_dir = Path(__file__).parent.resolve() + tests_data_dir = tests_dir / "data" + return tests_data_dir diff --git a/test/data/option_d.py b/test/data/option_d.py new file mode 100644 index 0000000..f2868b9 --- /dev/null +++ b/test/data/option_d.py @@ -0,0 +1,311 @@ +"""Viridis / Option D colormap from mpl-colormaps + +https://github.com/BIDS/colormap/blob/bc549477db0c12b54a5928087552ad2cf274980f/option_d.py + +mpl-colormaps by Nathaniel Smith & Stefan van der Walt + +To the extent possible under law, the persons who associated CC0 with +mpl-colormaps have waived all copyright and related or neighboring rights +to mpl-colormaps. + +You should have received a copy of the CC0 legalcode along with this +work. If not, see . +""" +from matplotlib.colors import LinearSegmentedColormap + +# Used to reconstruct the colormap in viscm +parameters = { + "xp": [ + 22.674387857633945, + 11.221508276482126, + -14.356589454756971, + -47.18817758739222, + -34.59001004812521, + -6.0516291196352654, + ], + "yp": [ + -20.102530541012214, + -33.08246073298429, + -42.24476439790574, + -5.595549738219887, + 42.5065445026178, + 40.13395157135497, + ], + "min_JK": 18.8671875, + "max_JK": 92.5, +} + +cm_data = [ + [0.26700401, 0.00487433, 0.32941519], + [0.26851048, 0.00960483, 0.33542652], + [0.26994384, 0.01462494, 0.34137895], + [0.27130489, 0.01994186, 0.34726862], + [0.27259384, 0.02556309, 0.35309303], + [0.27380934, 0.03149748, 0.35885256], + [0.27495242, 0.03775181, 0.36454323], + [0.27602238, 0.04416723, 0.37016418], + [0.2770184, 0.05034437, 0.37571452], + [0.27794143, 0.05632444, 0.38119074], + [0.27879067, 0.06214536, 0.38659204], + [0.2795655, 0.06783587, 0.39191723], + [0.28026658, 0.07341724, 0.39716349], + [0.28089358, 0.07890703, 0.40232944], + [0.28144581, 0.0843197, 0.40741404], + [0.28192358, 0.08966622, 0.41241521], + [0.28232739, 0.09495545, 0.41733086], + [0.28265633, 0.10019576, 0.42216032], + [0.28291049, 0.10539345, 0.42690202], + [0.28309095, 0.11055307, 0.43155375], + [0.28319704, 0.11567966, 0.43611482], + [0.28322882, 0.12077701, 0.44058404], + [0.28318684, 0.12584799, 0.44496], + [0.283072, 0.13089477, 0.44924127], + [0.28288389, 0.13592005, 0.45342734], + [0.28262297, 0.14092556, 0.45751726], + [0.28229037, 0.14591233, 0.46150995], + [0.28188676, 0.15088147, 0.46540474], + [0.28141228, 0.15583425, 0.46920128], + [0.28086773, 0.16077132, 0.47289909], + [0.28025468, 0.16569272, 0.47649762], + [0.27957399, 0.17059884, 0.47999675], + [0.27882618, 0.1754902, 0.48339654], + [0.27801236, 0.18036684, 0.48669702], + [0.27713437, 0.18522836, 0.48989831], + [0.27619376, 0.19007447, 0.49300074], + [0.27519116, 0.1949054, 0.49600488], + [0.27412802, 0.19972086, 0.49891131], + [0.27300596, 0.20452049, 0.50172076], + [0.27182812, 0.20930306, 0.50443413], + [0.27059473, 0.21406899, 0.50705243], + [0.26930756, 0.21881782, 0.50957678], + [0.26796846, 0.22354911, 0.5120084], + [0.26657984, 0.2282621, 0.5143487], + [0.2651445, 0.23295593, 0.5165993], + [0.2636632, 0.23763078, 0.51876163], + [0.26213801, 0.24228619, 0.52083736], + [0.26057103, 0.2469217, 0.52282822], + [0.25896451, 0.25153685, 0.52473609], + [0.25732244, 0.2561304, 0.52656332], + [0.25564519, 0.26070284, 0.52831152], + [0.25393498, 0.26525384, 0.52998273], + [0.25219404, 0.26978306, 0.53157905], + [0.25042462, 0.27429024, 0.53310261], + [0.24862899, 0.27877509, 0.53455561], + [0.2468114, 0.28323662, 0.53594093], + [0.24497208, 0.28767547, 0.53726018], + [0.24311324, 0.29209154, 0.53851561], + [0.24123708, 0.29648471, 0.53970946], + [0.23934575, 0.30085494, 0.54084398], + [0.23744138, 0.30520222, 0.5419214], + [0.23552606, 0.30952657, 0.54294396], + [0.23360277, 0.31382773, 0.54391424], + [0.2316735, 0.3181058, 0.54483444], + [0.22973926, 0.32236127, 0.54570633], + [0.22780192, 0.32659432, 0.546532], + [0.2258633, 0.33080515, 0.54731353], + [0.22392515, 0.334994, 0.54805291], + [0.22198915, 0.33916114, 0.54875211], + [0.22005691, 0.34330688, 0.54941304], + [0.21812995, 0.34743154, 0.55003755], + [0.21620971, 0.35153548, 0.55062743], + [0.21429757, 0.35561907, 0.5511844], + [0.21239477, 0.35968273, 0.55171011], + [0.2105031, 0.36372671, 0.55220646], + [0.20862342, 0.36775151, 0.55267486], + [0.20675628, 0.37175775, 0.55311653], + [0.20490257, 0.37574589, 0.55353282], + [0.20306309, 0.37971644, 0.55392505], + [0.20123854, 0.38366989, 0.55429441], + [0.1994295, 0.38760678, 0.55464205], + [0.1976365, 0.39152762, 0.55496905], + [0.19585993, 0.39543297, 0.55527637], + [0.19410009, 0.39932336, 0.55556494], + [0.19235719, 0.40319934, 0.55583559], + [0.19063135, 0.40706148, 0.55608907], + [0.18892259, 0.41091033, 0.55632606], + [0.18723083, 0.41474645, 0.55654717], + [0.18555593, 0.4185704, 0.55675292], + [0.18389763, 0.42238275, 0.55694377], + [0.18225561, 0.42618405, 0.5571201], + [0.18062949, 0.42997486, 0.55728221], + [0.17901879, 0.43375572, 0.55743035], + [0.17742298, 0.4375272, 0.55756466], + [0.17584148, 0.44128981, 0.55768526], + [0.17427363, 0.4450441, 0.55779216], + [0.17271876, 0.4487906, 0.55788532], + [0.17117615, 0.4525298, 0.55796464], + [0.16964573, 0.45626209, 0.55803034], + [0.16812641, 0.45998802, 0.55808199], + [0.1666171, 0.46370813, 0.55811913], + [0.16511703, 0.4674229, 0.55814141], + [0.16362543, 0.47113278, 0.55814842], + [0.16214155, 0.47483821, 0.55813967], + [0.16066467, 0.47853961, 0.55811466], + [0.15919413, 0.4822374, 0.5580728], + [0.15772933, 0.48593197, 0.55801347], + [0.15626973, 0.4896237, 0.557936], + [0.15481488, 0.49331293, 0.55783967], + [0.15336445, 0.49700003, 0.55772371], + [0.1519182, 0.50068529, 0.55758733], + [0.15047605, 0.50436904, 0.55742968], + [0.14903918, 0.50805136, 0.5572505], + [0.14760731, 0.51173263, 0.55704861], + [0.14618026, 0.51541316, 0.55682271], + [0.14475863, 0.51909319, 0.55657181], + [0.14334327, 0.52277292, 0.55629491], + [0.14193527, 0.52645254, 0.55599097], + [0.14053599, 0.53013219, 0.55565893], + [0.13914708, 0.53381201, 0.55529773], + [0.13777048, 0.53749213, 0.55490625], + [0.1364085, 0.54117264, 0.55448339], + [0.13506561, 0.54485335, 0.55402906], + [0.13374299, 0.54853458, 0.55354108], + [0.13244401, 0.55221637, 0.55301828], + [0.13117249, 0.55589872, 0.55245948], + [0.1299327, 0.55958162, 0.55186354], + [0.12872938, 0.56326503, 0.55122927], + [0.12756771, 0.56694891, 0.55055551], + [0.12645338, 0.57063316, 0.5498411], + [0.12539383, 0.57431754, 0.54908564], + [0.12439474, 0.57800205, 0.5482874], + [0.12346281, 0.58168661, 0.54744498], + [0.12260562, 0.58537105, 0.54655722], + [0.12183122, 0.58905521, 0.54562298], + [0.12114807, 0.59273889, 0.54464114], + [0.12056501, 0.59642187, 0.54361058], + [0.12009154, 0.60010387, 0.54253043], + [0.11973756, 0.60378459, 0.54139999], + [0.11951163, 0.60746388, 0.54021751], + [0.11942341, 0.61114146, 0.53898192], + [0.11948255, 0.61481702, 0.53769219], + [0.11969858, 0.61849025, 0.53634733], + [0.12008079, 0.62216081, 0.53494633], + [0.12063824, 0.62582833, 0.53348834], + [0.12137972, 0.62949242, 0.53197275], + [0.12231244, 0.63315277, 0.53039808], + [0.12344358, 0.63680899, 0.52876343], + [0.12477953, 0.64046069, 0.52706792], + [0.12632581, 0.64410744, 0.52531069], + [0.12808703, 0.64774881, 0.52349092], + [0.13006688, 0.65138436, 0.52160791], + [0.13226797, 0.65501363, 0.51966086], + [0.13469183, 0.65863619, 0.5176488], + [0.13733921, 0.66225157, 0.51557101], + [0.14020991, 0.66585927, 0.5134268], + [0.14330291, 0.66945881, 0.51121549], + [0.1466164, 0.67304968, 0.50893644], + [0.15014782, 0.67663139, 0.5065889], + [0.15389405, 0.68020343, 0.50417217], + [0.15785146, 0.68376525, 0.50168574], + [0.16201598, 0.68731632, 0.49912906], + [0.1663832, 0.69085611, 0.49650163], + [0.1709484, 0.69438405, 0.49380294], + [0.17570671, 0.6978996, 0.49103252], + [0.18065314, 0.70140222, 0.48818938], + [0.18578266, 0.70489133, 0.48527326], + [0.19109018, 0.70836635, 0.48228395], + [0.19657063, 0.71182668, 0.47922108], + [0.20221902, 0.71527175, 0.47608431], + [0.20803045, 0.71870095, 0.4728733], + [0.21400015, 0.72211371, 0.46958774], + [0.22012381, 0.72550945, 0.46622638], + [0.2263969, 0.72888753, 0.46278934], + [0.23281498, 0.73224735, 0.45927675], + [0.2393739, 0.73558828, 0.45568838], + [0.24606968, 0.73890972, 0.45202405], + [0.25289851, 0.74221104, 0.44828355], + [0.25985676, 0.74549162, 0.44446673], + [0.26694127, 0.74875084, 0.44057284], + [0.27414922, 0.75198807, 0.4366009], + [0.28147681, 0.75520266, 0.43255207], + [0.28892102, 0.75839399, 0.42842626], + [0.29647899, 0.76156142, 0.42422341], + [0.30414796, 0.76470433, 0.41994346], + [0.31192534, 0.76782207, 0.41558638], + [0.3198086, 0.77091403, 0.41115215], + [0.3277958, 0.77397953, 0.40664011], + [0.33588539, 0.7770179, 0.40204917], + [0.34407411, 0.78002855, 0.39738103], + [0.35235985, 0.78301086, 0.39263579], + [0.36074053, 0.78596419, 0.38781353], + [0.3692142, 0.78888793, 0.38291438], + [0.37777892, 0.79178146, 0.3779385], + [0.38643282, 0.79464415, 0.37288606], + [0.39517408, 0.79747541, 0.36775726], + [0.40400101, 0.80027461, 0.36255223], + [0.4129135, 0.80304099, 0.35726893], + [0.42190813, 0.80577412, 0.35191009], + [0.43098317, 0.80847343, 0.34647607], + [0.44013691, 0.81113836, 0.3409673], + [0.44936763, 0.81376835, 0.33538426], + [0.45867362, 0.81636288, 0.32972749], + [0.46805314, 0.81892143, 0.32399761], + [0.47750446, 0.82144351, 0.31819529], + [0.4870258, 0.82392862, 0.31232133], + [0.49661536, 0.82637633, 0.30637661], + [0.5062713, 0.82878621, 0.30036211], + [0.51599182, 0.83115784, 0.29427888], + [0.52577622, 0.83349064, 0.2881265], + [0.5356211, 0.83578452, 0.28190832], + [0.5455244, 0.83803918, 0.27562602], + [0.55548397, 0.84025437, 0.26928147], + [0.5654976, 0.8424299, 0.26287683], + [0.57556297, 0.84456561, 0.25641457], + [0.58567772, 0.84666139, 0.24989748], + [0.59583934, 0.84871722, 0.24332878], + [0.60604528, 0.8507331, 0.23671214], + [0.61629283, 0.85270912, 0.23005179], + [0.62657923, 0.85464543, 0.22335258], + [0.63690157, 0.85654226, 0.21662012], + [0.64725685, 0.85839991, 0.20986086], + [0.65764197, 0.86021878, 0.20308229], + [0.66805369, 0.86199932, 0.19629307], + [0.67848868, 0.86374211, 0.18950326], + [0.68894351, 0.86544779, 0.18272455], + [0.69941463, 0.86711711, 0.17597055], + [0.70989842, 0.86875092, 0.16925712], + [0.72039115, 0.87035015, 0.16260273], + [0.73088902, 0.87191584, 0.15602894], + [0.74138803, 0.87344918, 0.14956101], + [0.75188414, 0.87495143, 0.14322828], + [0.76237342, 0.87642392, 0.13706449], + [0.77285183, 0.87786808, 0.13110864], + [0.78331535, 0.87928545, 0.12540538], + [0.79375994, 0.88067763, 0.12000532], + [0.80418159, 0.88204632, 0.11496505], + [0.81457634, 0.88339329, 0.11034678], + [0.82494028, 0.88472036, 0.10621724], + [0.83526959, 0.88602943, 0.1026459], + [0.84556056, 0.88732243, 0.09970219], + [0.8558096, 0.88860134, 0.09745186], + [0.86601325, 0.88986815, 0.09595277], + [0.87616824, 0.89112487, 0.09525046], + [0.88627146, 0.89237353, 0.09537439], + [0.89632002, 0.89361614, 0.09633538], + [0.90631121, 0.89485467, 0.09812496], + [0.91624212, 0.89609127, 0.1007168], + [0.92610579, 0.89732977, 0.10407067], + [0.93590444, 0.8985704, 0.10813094], + [0.94563626, 0.899815, 0.11283773], + [0.95529972, 0.90106534, 0.11812832], + [0.96489353, 0.90232311, 0.12394051], + [0.97441665, 0.90358991, 0.13021494], + [0.98386829, 0.90486726, 0.13689671], + [0.99324789, 0.90615657, 0.1439362], +] + +test_cm = LinearSegmentedColormap.from_list(__file__, cm_data) + + +if __name__ == "__main__": + import matplotlib.pyplot as plt + import numpy as np + + try: + from viscm import viscm + + viscm(test_cm) + except ImportError: + print("viscm not found, falling back on simple display") + plt.imshow(np.linspace(0, 100, 256)[None, :], aspect="auto", cmap=test_cm) + plt.show() diff --git a/test/test_gui.py b/test/test_gui.py new file mode 100644 index 0000000..6b06ee8 --- /dev/null +++ b/test/test_gui.py @@ -0,0 +1,44 @@ +from pathlib import Path + +import pytest + +from viscm.cli import _make_window + + +class TestGui: + def test_gui_view_opens(self, qtbot): + window = _make_window( + action="view", + cmap="viridis", + cmap_type="linear", + cmap_spline_method="CatmulClark", + cmap_uniform_space="CAM02-UCS", + save=None, + quit_immediately=False, + ) + window.show() + qtbot.addWidget(window) + + assert window.isVisible() + + @pytest.mark.xfail( + reason="Unknown. See https://github.com/matplotlib/viscm/issues/71", + ) + def test_gui_edit_pyfile_opens(self, tests_data_dir: Path, qtbot): + """Reproduce viridis from README instructions. + + https://github.com/matplotlib/viscm/pull/58 + """ + window = _make_window( + action="edit", + cmap=str(tests_data_dir / "option_d.py"), + cmap_type="linear", + cmap_spline_method="Bezier", + cmap_uniform_space="buggy-CAM02-UCS", + save=None, + quit_immediately=False, + ) + window.show() + qtbot.addWidget(window) + + assert window.isVisible() diff --git a/viscm/cli.py b/viscm/cli.py index 3b349a1..6625693 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -1,4 +1,6 @@ import sys +from pathlib import Path +from typing import Union import matplotlib.pyplot as plt @@ -71,39 +73,79 @@ def cli(): "--save", metavar="FILE", default=None, - help="Immediately save visualization to a file " "(view-mode only).", + help="Immediately save visualization to a file (view-mode only).", ) parser.add_argument( "--quit", default=False, action="store_true", - help="Quit immediately after starting " "(useful with --save).", + help="Quit immediately after starting (useful with --save).", ) args = parser.parse_args(argv) - cm = gui.Colormap(args.type, args.method, args.uniform_space) app = gui.QtWidgets.QApplication([]) - if args.colormap: - cm.load(args.colormap) + try: + mainwindow = _make_window( + action=args.action, + cmap=args.colormap, + cmap_type=args.type, + cmap_spline_method=args.method, + cmap_uniform_space=args.uniform_space, + save=Path(args.save) if args.save else None, + quit_immediately=args.quit, + ) + except Exception as e: + sys.exit(str(e)) + + mainwindow.show() + + # PyQt messes up signal handling by default. Python signal handlers (e.g., + # the default handler for SIGINT that raises KeyboardInterrupt) can only + # run when we enter the Python interpreter, which doesn't happen while + # idling in the Qt mainloop. (Unless we register a timer to poll + # explicitly.) So here we unregister Python's default signal handler and + # replace it with... the *operating system's* default signal handler, so + # instead of a KeyboardInterrupt our process just exits. + import signal + + signal.signal(signal.SIGINT, signal.SIG_DFL) + + app.exec_() + + +def _make_window( + *, + action: str, + cmap: Union[str, None], + cmap_type: str, + cmap_spline_method: str, + cmap_uniform_space: str, + save: Union[Path, None], + quit_immediately: bool, +): + # Hold a reference so it doesn't get GC'ed + fig = plt.figure() + figure_canvas = gui.FigureCanvas(fig) + + cm = gui.Colormap(cmap_type, cmap_spline_method, cmap_uniform_space) + if cmap: + cm.load(cmap) # Easter egg! I keep typing 'show' instead of 'view' so accept both - if args.action in ("view", "show"): + if action in ("view", "show"): if cm is None: - sys.exit("Please specify a colormap") - fig = plt.figure() - figure_canvas = gui.FigureCanvas(fig) + raise RuntimeError("Please specify a colormap") + v = gui.viscm(cm.cmap, name=cm.name, figure=fig, uniform_space=cm.uniform_space) - mainwindow = gui.ViewerWindow(figure_canvas, v, cm.name) - if args.save is not None: + window = gui.ViewerWindow(figure_canvas, v, cm.name) + if save is not None: v.figure.set_size_inches(20, 12) - v.figure.savefig(args.save) - elif args.action == "edit": + v.figure.savefig(str(save)) + elif action == "edit": if not cm.can_edit: sys.exit("Sorry, I don't know how to edit the specified colormap") - # Hold a reference so it doesn't get GC'ed - fig = plt.figure() - figure_canvas = gui.FigureCanvas(fig) + v = gui.viscm_editor( figure=fig, uniform_space=cm.uniform_space, @@ -111,11 +153,13 @@ def cli(): method=cm.method, **cm.params, ) - mainwindow = gui.EditorWindow(figure_canvas, v) + window = gui.EditorWindow(figure_canvas, v) else: - raise RuntimeError("can't happen") + raise RuntimeError( + "Action must be 'edit', 'view', or 'show'. This should never happen.", + ) - if args.quit: + if quit_immediately: sys.exit() figure_canvas.setSizePolicy( @@ -123,21 +167,8 @@ def cli(): ) figure_canvas.updateGeometry() - mainwindow.resize(800, 600) - mainwindow.show() - - # PyQt messes up signal handling by default. Python signal handlers (e.g., - # the default handler for SIGINT that raises KeyboardInterrupt) can only - # run when we enter the Python interpreter, which doesn't happen while - # idling in the Qt mainloop. (Unless we register a timer to poll - # explicitly.) So here we unregister Python's default signal handler and - # replace it with... the *operating system's* default signal handler, so - # instead of a KeyboardInterrupt our process just exits. - import signal - - signal.signal(signal.SIGINT, signal.SIG_DFL) - - app.exec_() + window.resize(800, 600) + return window if __name__ == "__main__": diff --git a/viscm/gui.py b/viscm/gui.py index b3a1a2e..927943e 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -959,7 +959,7 @@ def loadpyfile(path): "__file__": os.path.basename(path), } - # FIXME: Should be `args.colormap` should be `path`? + # FIXME: `args.colormap` should be `path`? with open(args.colormap) as f: # noqa: F821 code = compile( f.read(), From 52758b93f25fa1de68642ff6eac182ee430c4c83 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 4 Jun 2023 18:14:18 -0600 Subject: [PATCH 34/47] Fix failing viscm editor test The old behavior was an implicit conversion to `int`. This warning was being thrown in Python 3.9: ``` DeprecationWarning: an integer is required (got type float). Implicit conversion to integers using __int__ is deprecated, and may be removed in a future version of Python. ``` And in Python 3.10, this turned in to an error, as seen in : ``` TypeError: setValue(self, a0: int): argument 1 has unexpected type 'float' ``` The new version of this code simply makes the old behavior (conversion to int) explicit. I'm unsure whether this was the original intent. --- .github/workflows/test.yml | 4 ++++ test/test_gui.py | 5 ----- viscm/gui.py | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c7d542..4a37ef9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,3 +42,7 @@ jobs: - name: "Run tests" run: "make test" + env: + # In Pythons >= 3.10, tests fail with `RuntimeError: Invalid DISPLAY + # variable`, unless this variable is set: + MPLBACKEND: "Agg" diff --git a/test/test_gui.py b/test/test_gui.py index 6b06ee8..7a364f2 100644 --- a/test/test_gui.py +++ b/test/test_gui.py @@ -1,7 +1,5 @@ from pathlib import Path -import pytest - from viscm.cli import _make_window @@ -21,9 +19,6 @@ def test_gui_view_opens(self, qtbot): assert window.isVisible() - @pytest.mark.xfail( - reason="Unknown. See https://github.com/matplotlib/viscm/issues/71", - ) def test_gui_edit_pyfile_opens(self, tests_data_dir: Path, qtbot): """Reproduce viridis from README instructions. diff --git a/viscm/gui.py b/viscm/gui.py index 927943e..0fba1fa 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -1154,7 +1154,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.max_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.max_slider.setMinimum(0) self.max_slider.setMaximum(100) - self.max_slider.setValue(viscm_editor.max_Jp) + self.max_slider.setValue(int(viscm_editor.max_Jp)) self.max_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) self.max_slider.setTickInterval(10) self.max_slider.valueChanged.connect(self.updatejp) @@ -1164,7 +1164,7 @@ def __init__(self, figurecanvas, viscm_editor, parent=None): self.min_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal) self.min_slider.setMinimum(0) self.min_slider.setMaximum(100) - self.min_slider.setValue(viscm_editor.min_Jp) + self.min_slider.setValue(int(viscm_editor.min_Jp)) self.min_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) self.min_slider.setTickInterval(10) self.min_slider.valueChanged.connect(self.updatejp) @@ -1277,8 +1277,8 @@ def update_smoothness_slider(self): def swapjp(self): jp1, jp2 = self.min_slider.value(), self.max_slider.value() - self.min_slider.setValue(jp2) - self.max_slider.setValue(jp1) + self.min_slider.setValue(int(jp2)) + self.max_slider.setValue(int(jp1)) self.updatejp() def updatejp(self): From b86bf1af644b353e18bd813a66ae15f81beec675 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Mon, 5 Jun 2023 21:00:59 -0600 Subject: [PATCH 35/47] Skip GUI tests if xvfb not installed --- Makefile | 2 +- doc/contributing.md | 3 +-- test/test_gui.py | 9 +++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 80f7393..b8cc123 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test test: python -m pytest --version - python -m pytest --xvfb-backend=xvfb -v test/ + python -m pytest -v test/ .PHONY: lint diff --git a/doc/contributing.md b/doc/contributing.md index 95fe327..ad445ba 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -55,8 +55,7 @@ make test ``` Unit tests require `xvfb` (X Virtual Framebuffer) to test the GUI. If `xvfb` is not -installed, you'll receive `ERROR: xvfb backend xvfb requested but not installed`. -Install with: +installed, GUI tests will be skipped. Install with: ```bash sudo apt install xvfb diff --git a/test/test_gui.py b/test/test_gui.py index 7a364f2..6f1ca1c 100644 --- a/test/test_gui.py +++ b/test/test_gui.py @@ -1,8 +1,17 @@ from pathlib import Path +import pytest +import pytest_xvfb + from viscm.cli import _make_window +xvfb_installed = pytest_xvfb.xvfb_instance is not None + +@pytest.mark.skipif( + not xvfb_installed, + reason="Xvfb must be installed for this test.", +) class TestGui: def test_gui_view_opens(self, qtbot): window = _make_window( From a271a0e97be4ec184bd582115d6f99465608935b Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 4 Jun 2023 20:14:04 -0600 Subject: [PATCH 36/47] Add mypy configuration --- .github/workflows/test.yml | 8 +++++++- Makefile | 8 +++++++- doc/contributing.md | 9 +++++++++ environment.yml | 1 + pyproject.toml | 16 ++++++++++++++++ viscm/cli.py | 3 ++- 6 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4a37ef9..c8ab78d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: jobs: - unit-test-with-pytest: + unit-test-and-typecheck: runs-on: "ubuntu-latest" strategy: matrix: @@ -46,3 +46,9 @@ jobs: # In Pythons >= 3.10, tests fail with `RuntimeError: Invalid DISPLAY # variable`, unless this variable is set: MPLBACKEND: "Agg" + + - name: "Install mypy" + run: "pip install mypy>=1.3" + + - name: "Run typechecker" + run: "make typecheck" diff --git a/Makefile b/Makefile index b8cc123..344a63e 100644 --- a/Makefile +++ b/Makefile @@ -10,5 +10,11 @@ lint: pre-commit run --all-files --show-diff-on-failure --color always +.PHONY: typecheck +typecheck: + mypy --version + mypy viscm + + .PHONY: ci -ci: lint test +ci: lint typecheck test diff --git a/doc/contributing.md b/doc/contributing.md index ad445ba..1729689 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -60,3 +60,12 @@ installed, GUI tests will be skipped. Install with: ```bash sudo apt install xvfb ``` + + +### Type check + +```bash +make typecheck +``` + +Type checking requires `mypy`. diff --git a/environment.yml b/environment.yml index e198e5d..599ba24 100644 --- a/environment.yml +++ b/environment.yml @@ -11,6 +11,7 @@ dependencies: - "scipy ~=1.10" # Development + - "mypy ~=1.3.0" - "pre-commit" - "pytest" - "pytest-cov" diff --git a/pyproject.toml b/pyproject.toml index 7870101..e572469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,22 @@ packages = {find = {}} package-data = {viscm = ["examples/*"]} +[tool.mypy] +python_version = "3.8" + +# These libraries don't have type stubs. Mypy will see them as `Any` and not +# throw an [import] error. +[[tool.mypy.overrides]] +module = [ + "colorspacious", + "matplotlib.*", + "mpl_toolkits.*", + "scipy.*", +] +ignore_missing_imports = true + + + [tool.black] target-version = ["py38", "py39", "py310", "py311"] diff --git a/viscm/cli.py b/viscm/cli.py index 6625693..c32f77d 100644 --- a/viscm/cli.py +++ b/viscm/cli.py @@ -123,7 +123,7 @@ def _make_window( cmap_uniform_space: str, save: Union[Path, None], quit_immediately: bool, -): +) -> Union[gui.ViewerWindow, gui.EditorWindow]: # Hold a reference so it doesn't get GC'ed fig = plt.figure() figure_canvas = gui.FigureCanvas(fig) @@ -132,6 +132,7 @@ def _make_window( if cmap: cm.load(cmap) + v: Union[gui.viscm, gui.viscm_editor] # Easter egg! I keep typing 'show' instead of 'view' so accept both if action in ("view", "show"): if cm is None: From b9c78a03fc3536dfac94b8c0c66a8de38571d2a9 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 6 Jun 2023 11:46:42 -0600 Subject: [PATCH 37/47] Fix newline issue in README --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 852c37c..020b6a7 100644 --- a/README.rst +++ b/README.rst @@ -43,4 +43,4 @@ python -m viscm --uniform-space buggy-CAM02-UCS -m Bezier edit /tmp/option_d.py Note that there was a small bug in the assumed sRGB viewing conditions while designing viridis. It does not affect the outcome by much. Also -see `python -m viscm --help`. \ No newline at end of file +see `python -m viscm --help`. From 6ceabb637da956ee629e713882d4456993aa6e97 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 10 Jun 2023 16:36:33 -0600 Subject: [PATCH 38/47] Set dependency lower bounds --- .github/workflows/test.yml | 3 +-- README.rst | 48 ++++++++++++++++++++++++++------------ doc/contributing.md | 2 ++ pyproject.toml | 20 +++++++++------- 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8ab78d..367e8a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,10 +13,9 @@ jobs: - "3.11" - "3.10" - "3.9" - - "3.8" pyqt-dependency: - "PyQt5" - # - "PyQt6" + - "PySide2" steps: - uses: "actions/checkout@v3" diff --git a/README.rst b/README.rst index 020b6a7..aeb2037 100644 --- a/README.rst +++ b/README.rst @@ -3,16 +3,6 @@ viscm This is a little tool for analyzing colormaps and creating new colormaps. -Try:: - - $ pip install viscm - $ python -m viscm view jet - $ python -m viscm edit - -There is some information available about how to interpret the -resulting visualizations and use the editor tool `on this website -`_. - Downloads: * https://pypi.python.org/pypi/viscm/ * https://anaconda.org/conda-forge/viscm/ @@ -24,16 +14,44 @@ Contact: Nathaniel J. Smith and Stéfan van der Walt Dependencies: - * Python 3.8+ - * `colorspacious `_ - * Matplotlib - * NumPy + * Python 3.9+ + * `colorspacious `_ 1.1+ + * Matplotlib 3.5+ + * NumPy 1.22+ + * SciPy 1.8+ License: MIT, see `LICENSE `__ for details. + +Installation +------------ + +This is a GUI application, and requires Qt Python bindings. +They can be provided by PyQt (GPL) or PySide (LGPL):: + + $ pip install viscm[PySide] + +...or:: + + $ pip install viscm[PyQt] + + +Usage +----- + +:: + + $ viscm view jet + $ viscm edit + +There is some information available about how to interpret the +resulting visualizations and use the editor tool `on this website +`_. + + Reproducing viridis -------------------- +^^^^^^^^^^^^^^^^^^^ Load [viridis AKA option_d.py](https://github.com/BIDS/colormap/) using: diff --git a/doc/contributing.md b/doc/contributing.md index 1729689..6955eab 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -22,6 +22,8 @@ python -m build pip install dist/*.whl # or `dist/*.tar.gz` ``` +To automatically install a Qt dependency, try `pip install dist/[PyQt]`. + ## Tests diff --git a/pyproject.toml b/pyproject.toml index e572469..35fad24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,14 +15,18 @@ classifiers = [ "Programming Language :: Python :: 3", ] -requires-python = "~=3.8" +requires-python = "~=3.9" dependencies = [ - "numpy", - "matplotlib", - "colorspacious", - "scipy", + "numpy ~=1.22", + "matplotlib ~=3.5", + "colorspacious ~=1.1", + "scipy ~=1.8", ] +[project.optional-dependencies] +PySide = ["PySide2 ~=5.15"] +PyQt = ["PyQt5 ~=5.15"] + [project.urls] repository = "https://github.com/matplotlib/viscm" # documentation = "https://viscm.readthedocs.io" @@ -46,7 +50,7 @@ package-data = {viscm = ["examples/*"]} [tool.mypy] -python_version = "3.8" +python_version = "3.9" # These libraries don't have type stubs. Mypy will see them as `Any` and not # throw an [import] error. @@ -62,10 +66,10 @@ ignore_missing_imports = true [tool.black] -target-version = ["py38", "py39", "py310", "py311"] +target-version = ["py39", "py310", "py311"] [tool.ruff] -target-version = "py38" +target-version = "py39" select = [ "F", "E", From d22fce1ca45671689288d8b1d7e7ca6ab35ab39c Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sat, 10 Jun 2023 17:22:34 -0600 Subject: [PATCH 39/47] README fix: markdown -> rst --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index aeb2037..73a0dfa 100644 --- a/README.rst +++ b/README.rst @@ -53,12 +53,10 @@ resulting visualizations and use the editor tool `on this website Reproducing viridis ^^^^^^^^^^^^^^^^^^^ -Load [viridis AKA option_d.py](https://github.com/BIDS/colormap/) using: +Load `viridis AKA option_d.py `__ using:: -``` -python -m viscm --uniform-space buggy-CAM02-UCS -m Bezier edit /tmp/option_d.py -``` + viscm --uniform-space buggy-CAM02-UCS -m Bezier edit /tmp/option_d.py Note that there was a small bug in the assumed sRGB viewing conditions while designing viridis. It does not affect the outcome by much. Also -see `python -m viscm --help`. +see :code:`viscm --help`. From bd2bb6c47a2b92a2fcc9a60226a2205aa84ba406 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 11 Jun 2023 10:28:04 -0600 Subject: [PATCH 40/47] Drop Qt5 support in package metadata and GHActions config --- .github/workflows/test.yml | 6 +++--- environment.yml | 1 + pyproject.toml | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 367e8a9..7e7cf48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,8 +14,8 @@ jobs: - "3.10" - "3.9" pyqt-dependency: - - "PyQt5" - - "PySide2" + - "PyQt6" + - "PySide6" steps: - uses: "actions/checkout@v3" @@ -36,7 +36,7 @@ jobs: # pytest-qt CI dependencies: https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions sudo apt update sudo apt install -y \ - xvfb \ + xvfb libegl1 \ libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils - name: "Run tests" diff --git a/environment.yml b/environment.yml index 599ba24..bf96902 100644 --- a/environment.yml +++ b/environment.yml @@ -9,6 +9,7 @@ dependencies: - "matplotlib ~=3.7" - "colorspacious ~=1.1" - "scipy ~=1.10" + - "PySide6" # Development - "mypy ~=1.3.0" diff --git a/pyproject.toml b/pyproject.toml index 35fad24..f5cb284 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,9 @@ dependencies = [ ] [project.optional-dependencies] -PySide = ["PySide2 ~=5.15"] -PyQt = ["PyQt5 ~=5.15"] +# Qt6 was released 2020.08.12 +PySide = ["PySide6"] +PyQt = ["PyQt6"] [project.urls] repository = "https://github.com/matplotlib/viscm" From af040f04637dc899ff6db9208e2917120c232213 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 11 Jun 2023 16:40:49 -0600 Subject: [PATCH 41/47] Drop qt4/qt5 compatibility fallbacks --- viscm/gui.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/viscm/gui.py b/viscm/gui.py index e6145b8..1cc4fbf 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -10,35 +10,27 @@ import os.path import sys -import numpy as np - -# matplotlib.rcParams['backend'] = "QT4AGG" -# Do this first before any other matplotlib imports, to force matplotlib to -# use a Qt backend -from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets - -try: - from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas -except ImportError: - try: - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas - except ImportError: - from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas - import matplotlib import matplotlib.colors import matplotlib.pyplot as plt import mpl_toolkits.mplot3d +import numpy as np from colorspacious import ( CIECAM02Space, CIECAM02Surround, cspace_convert, cspace_converter, ) +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas + +# matplotlib.rcParams['backend'] = "QtAgg" +# Do this first before any other matplotlib imports, to force matplotlib to +# use a Qt backend +from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets from matplotlib.colors import ListedColormap from matplotlib.gridspec import GridSpec -from .minimvc import Trigger +from viscm.minimvc import Trigger Qt = QtCore.Qt From adf73db13ab0825a7299838c6a5e2c13d4481b68 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 11 Jun 2023 18:15:55 -0600 Subject: [PATCH 42/47] Add more context to comment --- viscm/gui.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/viscm/gui.py b/viscm/gui.py index 1cc4fbf..e527ae3 100644 --- a/viscm/gui.py +++ b/viscm/gui.py @@ -109,7 +109,11 @@ def _setup_Jpapbp_axis(ax): # Adapt a matplotlib colormap to a linearly transformed version -- useful for # visualizing how colormaps look given color deficiency. # Kinda a hack, b/c we inherit from Colormap (this is required), but then -# ignore its implementation entirely. +# ignore its implementation entirely. This results in errors at runtime: +# File "//site-packages/matplotlib/artist.py", line 1343, in format_cursor_data # noqa: E501 +# n = self.cmap.N +# ^^^^^^^^^^^ +# AttributeError: 'TransformedCMap' object has no attribute 'N' class TransformedCMap(matplotlib.colors.Colormap): def __init__(self, transform, base_cmap): self.transform = transform From 0f36651baf2bdcca31da085c7fb4ce52a2914dda Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Sun, 11 Jun 2023 19:53:21 -0600 Subject: [PATCH 43/47] Fixup GitHub Actions dependencies for tests with Qt6 --- .github/workflows/test.yml | 3 ++- Makefile | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e7cf48..2174dc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,8 @@ jobs: sudo apt update sudo apt install -y \ xvfb libegl1 \ - libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils + libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils \ + libxcb-cursor0 - name: "Run tests" run: "make test" diff --git a/Makefile b/Makefile index 344a63e..c1cfef0 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test test: python -m pytest --version - python -m pytest -v test/ + python -m pytest -sv test/ .PHONY: lint From d513594c3adac746148d8d98abe3f202689b2c7c Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Mon, 12 Jun 2023 16:31:04 -0600 Subject: [PATCH 44/47] Publish to PyPI automatically when publishing a GitHub release --- .github/workflows/check.yml | 5 +++-- .github/workflows/release.yml | 29 +++++++++++++++++++++++++++++ .github/workflows/test.yml | 7 ++++--- doc/contributing.md | 10 ++++++++++ 4 files changed, 46 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e3caf78..1336e17 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -1,7 +1,8 @@ name: "Check with pre-commit" on: - - "push" - - "pull_request" + push: + branches: ["main"] + pull_request: jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..39b0c8f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,29 @@ +name: "Release" +on: + release: + types: + - "published" + +jobs: + + build-and-release: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v3" + + - name: "Set up Python" + uses: "actions/setup-python@v4" + with: + python-version: "3.9" + + - name: "Install build tool" + run: "pip install --user build" + + - name: "Build binary dist (wheel) and source dist" + run: "python -m build" + + - name: "Publish to PyPI" + uses: "pypa/gh-action-pypi-publish@release/v1" + with: + password: "${{ secrets.PYPI_TOKEN }}" + repository-url: "https://pypi.org/project/viscm" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8ab78d..73a8514 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,8 @@ -name: "Unit test" +name: "Test" on: - - "push" - - "pull_request" + push: + branches: ["main"] + pull_request: jobs: diff --git a/doc/contributing.md b/doc/contributing.md index 1729689..38921f0 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -69,3 +69,13 @@ make typecheck ``` Type checking requires `mypy`. + + +## Making a release and publishing to PyPI + +Create a release using the GitHub user interface. Tag the version following +[semver](https://semver.org). *NOTE: As this software is pre-1.x, always increment the +minor version only.* + +Once the release is published in GitHub, a GitHub Action will be triggered to build and +upload to PyPI. From 2887ec147eb437ec98e59f7f9cc6deee1f448743 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 13 Jun 2023 10:00:31 -0600 Subject: [PATCH 45/47] Enable auto-generated release notes to be categorized by PR label --- .github/release.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..245f114 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,23 @@ +changelog: + exclude: + labels: + - "releasenotes-ignore" + categories: + - title: "New features" + labels: + - "enhancement" + - title: "Bug fixes" + labels: + - "bug" + - title: "CI" + labels: + - "ci" + - title: "Documentation" + labels: + - "documentation" + - title: "Dependency updates" + labels: + - "dependencies" + - title: "Other changes" + labels: + - "*" From bb2575cdb9022b1be786e049b008a7233f0171ae Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 13 Jun 2023 10:42:40 -0600 Subject: [PATCH 46/47] Fixup PyPI publish URL ``` 25hERROR RedirectDetected: https://pypi.org/project/viscm attempted to redirect to https://pypi.org/project/viscm/. Your repository URL is missing a trailing slash. Please add it and try again. ``` --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 39b0c8f..f30f504 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,4 +26,4 @@ jobs: uses: "pypa/gh-action-pypi-publish@release/v1" with: password: "${{ secrets.PYPI_TOKEN }}" - repository-url: "https://pypi.org/project/viscm" + repository-url: "https://pypi.org/project/viscm/" From 7e19769f0c472023da41f940f9755cdfe9fd69f9 Mon Sep 17 00:00:00 2001 From: Matt Fisher Date: Tue, 13 Jun 2023 11:10:13 -0600 Subject: [PATCH 47/47] Remove `repository-url` parameter from PyPI publish step ``` 25hERROR InvalidPyPIUploadURL: It appears you're trying to upload to pypi.org but have an invalid URL. You probably want one of these two URLs: https://upload.pypi.org/legacy/ or https://test.pypi.org/legacy/. Check your --repository-url value. ``` --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f30f504..8e13e01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,4 +26,3 @@ jobs: uses: "pypa/gh-action-pypi-publish@release/v1" with: password: "${{ secrets.PYPI_TOKEN }}" - repository-url: "https://pypi.org/project/viscm/"