{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n# mplcvd -- an example of figure hook\n\nTo use this hook, ensure that this module is in your ``PYTHONPATH``, and set\n``rcParams[\"figure.hooks\"] = [\"mplcvd:setup\"]``. This hook depends on\nthe ``colorspacious`` third-party module.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import functools\nfrom pathlib import Path\n\nimport colorspacious\n\nimport numpy as np\n\n_BUTTON_NAME = \"Filter\"\n_BUTTON_HELP = \"Simulate color vision deficiencies\"\n_MENU_ENTRIES = {\n \"None\": None,\n \"Greyscale\": \"greyscale\",\n \"Deuteranopia\": \"deuteranomaly\",\n \"Protanopia\": \"protanomaly\",\n \"Tritanopia\": \"tritanomaly\",\n}\n\n\ndef _get_color_filter(name):\n \"\"\"\n Given a color filter name, create a color filter function.\n\n Parameters\n ----------\n name : str\n The color filter name, one of the following:\n\n - ``\"none\"``: ...\n - ``\"greyscale\"``: Convert the input to luminosity.\n - ``\"deuteranopia\"``: Simulate the most common form of red-green\n colorblindness.\n - ``\"protanopia\"``: Simulate a rarer form of red-green colorblindness.\n - ``\"tritanopia\"``: Simulate the rare form of blue-yellow\n colorblindness.\n\n Color conversions use `colorspacious`_.\n\n Returns\n -------\n callable\n A color filter function that has the form:\n\n def filter(input: np.ndarray[M, N, D])-> np.ndarray[M, N, D]\n\n where (M, N) are the image dimensions, and D is the color depth (3 for\n RGB, 4 for RGBA). Alpha is passed through unchanged and otherwise\n ignored.\n \"\"\"\n if name not in _MENU_ENTRIES:\n raise ValueError(f\"Unsupported filter name: {name!r}\")\n name = _MENU_ENTRIES[name]\n\n if name is None:\n return None\n\n elif name == \"greyscale\":\n rgb_to_jch = colorspacious.cspace_converter(\"sRGB1\", \"JCh\")\n jch_to_rgb = colorspacious.cspace_converter(\"JCh\", \"sRGB1\")\n\n def convert(im):\n greyscale_JCh = rgb_to_jch(im)\n greyscale_JCh[..., 1] = 0\n im = jch_to_rgb(greyscale_JCh)\n return im\n\n else:\n cvd_space = {\"name\": \"sRGB1+CVD\", \"cvd_type\": name, \"severity\": 100}\n convert = colorspacious.cspace_converter(cvd_space, \"sRGB1\")\n\n def filter_func(im, dpi):\n alpha = None\n if im.shape[-1] == 4:\n im, alpha = im[..., :3], im[..., 3]\n im = convert(im)\n if alpha is not None:\n im = np.dstack((im, alpha))\n return np.clip(im, 0, 1), 0, 0\n\n return filter_func\n\n\ndef _set_menu_entry(tb, name):\n tb.canvas.figure.set_agg_filter(_get_color_filter(name))\n tb.canvas.draw_idle()\n\n\ndef setup(figure):\n tb = figure.canvas.toolbar\n if tb is None:\n return\n for cls in type(tb).__mro__:\n pkg = cls.__module__.split(\".\")[0]\n if pkg != \"matplotlib\":\n break\n if pkg == \"gi\":\n _setup_gtk(tb)\n elif pkg in (\"PyQt5\", \"PySide2\", \"PyQt6\", \"PySide6\"):\n _setup_qt(tb)\n elif pkg == \"tkinter\":\n _setup_tk(tb)\n elif pkg == \"wx\":\n _setup_wx(tb)\n else:\n raise NotImplementedError(\"The current backend is not supported\")\n\n\ndef _setup_gtk(tb):\n from gi.repository import Gio, GLib, Gtk\n\n for idx in range(tb.get_n_items()):\n children = tb.get_nth_item(idx).get_children()\n if children and isinstance(children[0], Gtk.Label):\n break\n\n toolitem = Gtk.SeparatorToolItem()\n tb.insert(toolitem, idx)\n\n image = Gtk.Image.new_from_gicon(\n Gio.Icon.new_for_string(\n str(Path(__file__).parent / \"images/eye-symbolic.svg\")),\n Gtk.IconSize.LARGE_TOOLBAR)\n\n # The type of menu is progressively downgraded depending on GTK version.\n if Gtk.check_version(3, 6, 0) is None:\n\n group = Gio.SimpleActionGroup.new()\n action = Gio.SimpleAction.new_stateful(\"cvdsim\",\n GLib.VariantType(\"s\"),\n GLib.Variant(\"s\", \"none\"))\n group.add_action(action)\n\n @functools.partial(action.connect, \"activate\")\n def set_filter(action, parameter):\n _set_menu_entry(tb, parameter.get_string())\n action.set_state(parameter)\n\n menu = Gio.Menu()\n for name in _MENU_ENTRIES:\n menu.append(name, f\"local.cvdsim::{name}\")\n\n button = Gtk.MenuButton.new()\n button.remove(button.get_children()[0])\n button.add(image)\n button.insert_action_group(\"local\", group)\n button.set_menu_model(menu)\n button.get_style_context().add_class(\"flat\")\n\n item = Gtk.ToolItem()\n item.add(button)\n tb.insert(item, idx + 1)\n\n else:\n\n menu = Gtk.Menu()\n group = []\n for name in _MENU_ENTRIES:\n item = Gtk.RadioMenuItem.new_with_label(group, name)\n item.set_active(name == \"None\")\n item.connect(\n \"activate\", lambda item: _set_menu_entry(tb, item.get_label()))\n group.append(item)\n menu.append(item)\n menu.show_all()\n\n tbutton = Gtk.MenuToolButton.new(image, _BUTTON_NAME)\n tbutton.set_menu(menu)\n tb.insert(tbutton, idx + 1)\n\n tb.show_all()\n\n\ndef _setup_qt(tb):\n from matplotlib.backends.qt_compat import QtGui, QtWidgets\n\n menu = QtWidgets.QMenu()\n try:\n QActionGroup = QtGui.QActionGroup # Qt6\n except AttributeError:\n QActionGroup = QtWidgets.QActionGroup # Qt5\n group = QActionGroup(menu)\n group.triggered.connect(lambda action: _set_menu_entry(tb, action.text()))\n\n for name in _MENU_ENTRIES:\n action = menu.addAction(name)\n action.setCheckable(True)\n action.setActionGroup(group)\n action.setChecked(name == \"None\")\n\n actions = tb.actions()\n before = next(\n (action for action in actions\n if isinstance(tb.widgetForAction(action), QtWidgets.QLabel)), None)\n\n tb.insertSeparator(before)\n button = QtWidgets.QToolButton()\n # FIXME: _icon needs public API.\n button.setIcon(tb._icon(str(Path(__file__).parent / \"images/eye.png\")))\n button.setText(_BUTTON_NAME)\n button.setToolTip(_BUTTON_HELP)\n button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup)\n button.setMenu(menu)\n tb.insertWidget(before, button)\n\n\ndef _setup_tk(tb):\n import tkinter as tk\n\n tb._Spacer() # FIXME: _Spacer needs public API.\n\n button = tk.Menubutton(master=tb, relief=\"raised\")\n button._image_file = str(Path(__file__).parent / \"images/eye.png\")\n # FIXME: _set_image_for_button needs public API (perhaps like _icon).\n tb._set_image_for_button(button)\n button.pack(side=tk.LEFT)\n\n menu = tk.Menu(master=button, tearoff=False)\n for name in _MENU_ENTRIES:\n menu.add(\"radiobutton\", label=name,\n command=lambda _name=name: _set_menu_entry(tb, _name))\n menu.invoke(0)\n button.config(menu=menu)\n\n\ndef _setup_wx(tb):\n import wx\n\n idx = next(idx for idx in range(tb.ToolsCount)\n if tb.GetToolByPos(idx).IsStretchableSpace())\n tb.InsertSeparator(idx)\n tool = tb.InsertTool(\n idx + 1, -1, _BUTTON_NAME,\n # FIXME: _icon needs public API.\n tb._icon(str(Path(__file__).parent / \"images/eye.png\")),\n # FIXME: ITEM_DROPDOWN is not supported on macOS.\n kind=wx.ITEM_DROPDOWN, shortHelp=_BUTTON_HELP)\n\n menu = wx.Menu()\n for name in _MENU_ENTRIES:\n item = menu.AppendRadioItem(-1, name)\n menu.Bind(\n wx.EVT_MENU,\n lambda event, _name=name: _set_menu_entry(tb, _name),\n id=item.Id,\n )\n tb.SetDropdownMenu(tool.Id, menu)\n\n\nif __name__ == '__main__':\n import matplotlib.pyplot as plt\n\n from matplotlib import cbook\n\n plt.rcParams['figure.hooks'].append('mplcvd:setup')\n\n fig, axd = plt.subplot_mosaic(\n [\n ['viridis', 'turbo'],\n ['photo', 'lines']\n ]\n )\n\n delta = 0.025\n x = y = np.arange(-3.0, 3.0, delta)\n X, Y = np.meshgrid(x, y)\n Z1 = np.exp(-X**2 - Y**2)\n Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)\n Z = (Z1 - Z2) * 2\n\n imv = axd['viridis'].imshow(\n Z, interpolation='bilinear',\n origin='lower', extent=[-3, 3, -3, 3],\n vmax=abs(Z).max(), vmin=-abs(Z).max()\n )\n fig.colorbar(imv)\n imt = axd['turbo'].imshow(\n Z, interpolation='bilinear', cmap='turbo',\n origin='lower', extent=[-3, 3, -3, 3],\n vmax=abs(Z).max(), vmin=-abs(Z).max()\n )\n fig.colorbar(imt)\n\n # A sample image\n with cbook.get_sample_data('grace_hopper.jpg') as image_file:\n photo = plt.imread(image_file)\n axd['photo'].imshow(photo)\n\n th = np.linspace(0, 2*np.pi, 1024)\n for j in [1, 2, 4, 6]:\n axd['lines'].plot(th, np.sin(th * j), label=f'$\\\\omega={j}$')\n axd['lines'].legend(ncols=2, loc='upper right')\n plt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 0 }