{
  "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\nfrom PIL import Image\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    photo = Image.open(cbook.get_sample_data(\"grace_hopper.jpg\"))\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.14.6"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}