Source code for pyqtgraph.widgets.ColorMapMenu

import collections
import importlib.util
import re

from .. import colormap
from ..graphicsItems.GradientPresets import Gradients
from ..Qt import QtCore, QtGui, QtWidgets

__all__ = ['ColorMapMenu']


# from https://matplotlib.org/stable/gallery/color/colormap_reference.html
MATPLOTLIB_CMAPS = [
         ('Perceptually Uniform Sequential', [
            'viridis', 'plasma', 'inferno', 'magma', 'cividis']),
         ('Sequential', [
            'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds',
            'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu',
            'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']),
         ('Sequential (2)', [
            'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink',
            'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia',
            'hot', 'afmhot', 'gist_heat', 'copper']),
         ('Diverging', [
            'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu',
            'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']),
         ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']),
         ('Qualitative', [
            'Pastel1', 'Pastel2', 'Paired', 'Accent',
            'Dark2', 'Set1', 'Set2', 'Set3',
            'tab10', 'tab20', 'tab20b', 'tab20c']),
         ('Miscellaneous', [
            'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern',
            'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg',
            'gist_rainbow', 'rainbow', 'jet', 'turbo', 'nipy_spectral',
            'gist_ncar'])
    ]


PrivateActionData = collections.namedtuple("ColorMapMenuPrivateActionData", ["name", "source"])


def buildMenuEntryWidget(cmap, text):
    lut = cmap.getLookupTable(nPts=32, alpha=True)
    qimg = QtGui.QImage(lut, len(lut), 1, QtGui.QImage.Format.Format_RGBA8888)
    pixmap = QtGui.QPixmap.fromImage(qimg)

    widget = QtWidgets.QWidget()
    layout = QtWidgets.QHBoxLayout(widget)
    layout.setContentsMargins(1,1,1,1)
    label1 = QtWidgets.QLabel()
    label1.setScaledContents(True)
    label1.setPixmap(pixmap)
    label2 = QtWidgets.QLabel(text)
    layout.addWidget(label1, 0)
    layout.addWidget(label2, 1)

    return widget

def buildMenuEntryAction(menu, name, source):
    if isinstance(source, colormap.ColorMap):
        cmap = source
    elif source == 'preset-gradient':
        cmap = preset_gradient_to_colormap(name)
    else:
        cmap = colormap.get(name, source=source)
    act = QtWidgets.QWidgetAction(menu)
    act.setData(PrivateActionData(name, source))
    act.setDefaultWidget(buildMenuEntryWidget(cmap, name))
    menu.addAction(act)

def sorted_filenames(names):
    pattern = re.compile(r'(\d+)')
    key = lambda x: [int(c) if c.isdigit() else c for c in pattern.split(x)]
    return sorted(names, key=key)

def find_mpl_leftovers():
    names = colormap.listMaps(source="matplotlib")
    # remove entries registered by colorcet
    names = [x for x in names if not x.startswith('cet_')]
    # remove the reversed colormaps
    names = [x for x in names if not x.endswith('_r')]
    # remove entries that are already categorised
    known_names = set()
    for item in MATPLOTLIB_CMAPS:
        known_names.update(item[1])
    names = [x for x in names if x not in known_names]
    return names

def buildCetSubMenu(menu, source, cet_type):
    names = colormap.listMaps(source=source)
    names = [x for x in names if x.startswith("CET")]

    if cet_type.endswith("Blind"):
        names = [x for x in names if x[4:6] == "CB"]
    else:
        names = [x for x in names if x[4] == cet_type[0] and x[5].isdigit()]

    for name in sorted_filenames(names):
        buildMenuEntryAction(menu, name, source)

def buildUserSubMenu(menu, userList):
    for item in userList:
        if isinstance(item, colormap.ColorMap):
            name, source = item.name, item
        elif isinstance(item, str):
            name, source = item, None
        elif isinstance(item, tuple):
            name, source = item
        else:
            raise ValueError("userList items must be ColorMap, str or tuple")

        buildMenuEntryAction(menu, name, source)

def preset_gradient_to_colormap(name):
    # generate the hsv two gradients using makeHslCycle
    if name == 'spectrum':
        # steps=30 for 300 degrees gives the same density as
        # the default steps=36 for 360 degrees
        cmap = colormap.makeHslCycle((0, 300/360), steps=30)
    elif name == 'cyclic':
        cmap = colormap.makeHslCycle((1, 0))
    else:
        cmap = colormap.ColorMap(*zip(*Gradients[name]["ticks"]), name=name)
    return cmap


[docs] class ColorMapMenu(QtWidgets.QMenu): sigColorMapTriggered = QtCore.Signal(object)
[docs] def __init__(self, *, userList=None, showGradientSubMenu=False, showColorMapSubMenus=False): """ Creates a new ColorMapMenu. Parameters ---------- userList : list of ColorMapSpecifier, optional Supported values for ColorMapSpecifier are ``str``, ``(str, str)``, :class:`~pyqtgraph.ColorMap` Example: ``["viridis", ("glasbey", "colorcet"), ("rainbow", "matplotlib")]`` showGradientSubMenu : bool, default=False Adds legacy gradients in a submenu. showColorMapSubMenus : bool, default=False Adds bundled colormaps and external (colorcet, matplotlib) colormaps in submenus. """ super().__init__() self.setTitle("ColorMaps") self.triggered.connect(self.onTriggered) topmenu = self act = topmenu.addAction('None') act.setData(PrivateActionData(None, None)) if userList is not None: buildUserSubMenu(topmenu, userList) if any([showGradientSubMenu, showColorMapSubMenus]): topmenu.addSeparator() # render the submenus only if the user actually clicks on it if showGradientSubMenu: submenu = topmenu.addMenu('preset gradient') submenu.aboutToShow.connect(self.buildGradientSubMenu) if not showColorMapSubMenus: return submenu = topmenu.addMenu('local') submenu.aboutToShow.connect(self.buildLocalSubMenu) have_colorcet = importlib.util.find_spec('colorcet') is not None # arranged in the order listed in https://colorcet.com/ cet_types = ["Linear", "Divergent", "Rainbow", "Cyclic", "Isoluminant", "Color Blind"] # the local cet files are a subset of the colorcet module. # expose just one of them. if not have_colorcet: submenu = topmenu.addMenu('cet (local)') for cet_type in cet_types: sub2menu = submenu.addMenu(cet_type) sub2menu.aboutToShow.connect(self.buildCetLocalSubMenu) else: submenu = topmenu.addMenu('cet (external)') for cet_type in cet_types: sub2menu = submenu.addMenu(cet_type) sub2menu.aboutToShow.connect(self.buildCetExternalSubMenu) if importlib.util.find_spec('matplotlib') is not None: submenu = topmenu.addMenu('matplotlib') # skip 1st entry which is "Perceptually Uniform Sequential" # since pyqtgraph has those already for category, _ in MATPLOTLIB_CMAPS[1:]: sub2menu = submenu.addMenu(category) sub2menu.aboutToShow.connect(self.buildMplCategorySubMenu) if find_mpl_leftovers(): sub2menu = submenu.addMenu("Others") sub2menu.aboutToShow.connect(self.buildMplOthersSubMenu) if have_colorcet: submenu = topmenu.addMenu('colorcet') submenu.aboutToShow.connect(self.buildColorcetSubMenu)
def onTriggered(self, action): if not isinstance(data := action.data(), PrivateActionData): return cmap = self.actionDataToColorMap(data) self.sigColorMapTriggered.emit(cmap) def buildGradientSubMenu(self): source = 'preset-gradient' names = list(Gradients.keys()) self.buildSubMenu(names, source, sort=False) def buildLocalSubMenu(self): source = None names = colormap.listMaps(source=source) names = [x for x in names if not x.startswith('CET')] names = [x for x in names if not x.startswith('PAL-relaxed')] self.buildSubMenu(names, source) def buildCetLocalSubMenu(self): # in Qt6 we could have used Qt.ConnectionType.SingleShotConnection menu = self.sender() menu.aboutToShow.disconnect() source = None cet_type = menu.title() buildCetSubMenu(menu, source, cet_type) def buildCetExternalSubMenu(self): # in Qt6 we could have used Qt.ConnectionType.SingleShotConnection menu = self.sender() menu.aboutToShow.disconnect() source = 'colorcet' cet_type = menu.title() buildCetSubMenu(menu, source, cet_type) def buildMplCategorySubMenu(self): # in Qt6 we could have used Qt.ConnectionType.SingleShotConnection menu = self.sender() menu.aboutToShow.disconnect() source = 'matplotlib' category = menu.title() categories = [x[0] for x in MATPLOTLIB_CMAPS] names = MATPLOTLIB_CMAPS[categories.index(category)][1] for name in names: try: buildMenuEntryAction(menu, name, source) except ValueError: # the names are not programmatically discovered, # so to be safe, we wrap around try except pass def buildMplOthersSubMenu(self): self.buildSubMenu(find_mpl_leftovers(), "matplotlib") def buildColorcetSubMenu(self): # colorcet colormaps with shorter/simpler aliases source = 'colorcet' import colorcet names = list(colorcet.palette_n.keys()) self.buildSubMenu(names, source) def buildSubMenu(self, names, source, sort=True): # in Qt6 we could have used Qt.ConnectionType.SingleShotConnection menu = self.sender() menu.aboutToShow.disconnect() if sort: names = sorted_filenames(names) for name in names: buildMenuEntryAction(menu, name, source) @staticmethod def actionDataToColorMap(data): name, source = data if isinstance(source, colormap.ColorMap): cmap = source elif name is None: cmap = colormap.ColorMap(None, [0.0, 1.0]) elif source == 'preset-gradient': cmap = preset_gradient_to_colormap(name) cmap.name = f"{source}:{name}" # for GradientEditorItem else: # colormap module maintains a cache keyed by name only. # thus if a colormap has the same name in two different sources, # we will end up getting whatever was already cached. cmap = colormap.get(name, source=source) return cmap