add ColorMapMenu to ColorBarItem#2955
Conversation
|
(( It's been suggested to me to comment on this. I sadly still don't have much time to contribute more actively. Apologies! )) That sounds great to me! A built-in way to swap between different pre-made false color modes could be very helpful. Not least it would add the option to switch to a colorblind-friendly color map on the fly. Looking at the example in #2779: In addition to being able to turn the menu off entirely, it does seem important to be able to reduce the selection displayed in the menu. I probably wouldn't want to overload an end-user with the full list. I probably wouldn't want to overload myself with the full list if I am really only going to consider three or so different maps. Do you see a good way for the developer to supply a shortlist of colormaps that will populate the menu? Maybe with a default list that would include a selection of the non-terrible maps (e.g. viridis, magma, plasme, inferno, turbo, cividis)? However, for other applications, it might be more helpful to have a different list to switch between, so it should be possible to specify a custom list. (Example: The various diverging maps included in CET for data with a significant zero point.) That would be three modes:
((And a very small extra comment, because I didn't notice in #2779: The "PAL-relaxed(_bright)" set of colors was intended as a palette of plotting colors, not a color map. They probably should not be in the "default" list for a color map selection - assuming that list is manually specified somewhere.)) |
The existing implementation of ColorMapMenu splits the bundled colormaps into those starting with "CET" and those not. I did notice that the "PAL" colormaps didn't "belong". But at the time, the focus was more on getting the mechanism of showing the colormaps. So one way is to blacklist them.
The overload is mitigated by putting the colormaps into submenus. Currently:
I think the colorcet could perhaps be broken down further into (C, CB, D, I, L, R) submenus? For the matplotlib colormaps, I don't think we should "know" too much about its naming conventions or categories, otherwise we are potentially setting ourselves up for breakage if new colormaps are added that don't fit existing conventions. Currently the code removes "cet*" and "_r" (reversed).
I think it shouldn't be difficult to implement this, provided we stick only to the bundled colormap names. But what is the use-case for this? The developer has a dataset and believes that only a restricted subset of colormaps is useful for that dataset? |
|
An example of the current implemented API. At the moment, a list of names from the local bundled colormaps can be provided. import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
from pyqtgraph.widgets.ColorMapButton import ColorMapMenu
import numpy as np
rng = np.random.default_rng(0)
app = pg.mkQApp()
win = pg.PlotWidget()
win.show()
plot = win.getPlotItem()
img = pg.ImageItem(axisOrder='row-major')
plot.addItem(img)
img.setImage(rng.rayleigh(size=(100,100)))
colorMapMenu = False
colorMapMenu = ColorMapMenu(curatedList=["viridis", "CET-L5", "cividis"])
cbi = pg.ColorBarItem(colorMap='CET-L5', colorMapMenu=colorMapMenu)
cbi.setImageItem(img, insert_in=plot)
def update():
noise = rng.rayleigh(size=(100,100))
XY = np.sinc(np.linspace(-10, 10, 100))
s = rng.random(1)
data = s * 10 * np.abs(XY[:, np.newaxis] * XY)**2 + noise
img.setImage(data, autoLevels=False)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(100)
app.exec() |
|
(I had not seen the updates above when I wrote the text below. This is in response to the earlier response.)
I think that would be good, for "blacklist" = "do not automatically add anything starting with
The current menu is wonderful for the developer to initially explore options. Or for a special-function tool where the developer and user are the same. Sorting into further categories would be nice, but -same as you do- I suspect that it would create more effort of maintenance than it is worth. I think the menu now strikes a good balance for what it does. I would vote to keep it as it is.
Yes. I think it is rare that you are making a program that might be showing anything, ever in the same plotting window. If the program is showing a camera image, then you'll want a linear map. If you are making a tool that shows differences, you might want a divergent map. If you are somehow plotting angles, then you may have a use for a circular map. Of course it would be great to specify the category and make pyqtgraph do the work of selection... But we'll never catch all selection criteria and even if we do, it will be a lot of effort to maintain that. The second part is a concern about inducing selection paralysis in the users. A professionally made program will typically only offer a handful of curated options to keep things simple, and I think that's good design. On the other end of the spectrum, I have some lab tools that are operated by colleagues. There's no help files, no tooltips, and there are options that may be unwise to select at certain times. Any extra option is a source of fear. I would prefer offering a list that is e.g. "Grayscale, Viridis, Cvidis" rather than a screen-filling extravaganza :) Let's add an option that moves the pre-selection to the developer. They know what they want the plot to communicate, and can play with the full menu until they have their own list of what works.
If the menu goes through the Sorry for the long text. Back to the original point:
Yes. That would be a great addition! |
|
You've programmed this faster than I can think about it. Thank you very much. This looks very good to me, maybe consider the option of turning off everything except the curated list? |
I like the current implementation. It adds minimal clutter to the developer's code, just the list of curated names. We can wait to see if there's demand for supplying customized maps. And there's a workaround: Your code should already search the program folder for the specified name. Maybe the stupidly simple solution would be to add an |
|
Updated API:
There are more combinations to test now. import importlib.util
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np
data = np.zeros(256)
data[::2] = 1.0
cmap_zebra = pg.colormap.ColorMap(pos=None, color=data, name="zebra")
rng = np.random.default_rng(0)
app = pg.mkQApp()
win = pg.PlotWidget()
win.show()
plot = win.getPlotItem()
img = pg.ImageItem(axisOrder='row-major')
plot.addItem(img)
img.setImage(rng.rayleigh(size=(100,100)))
colorMapMenu = False
userList = ["viridis", "CET-L5", "cividis"]
# userList.append(("zebra", cmap_zebra))
userList.append(cmap_zebra)
if importlib.util.find_spec("colorcet") is not None:
userList.append(("glasbey", "colorcet"))
if importlib.util.find_spec("matplotlib") is not None:
userList.append(("prism", "matplotlib"))
colorMapMenu = pg.ColorMapMenu(userList=userList)
cbi = pg.ColorBarItem(colorMap='CET-L5', colorMapMenu=colorMapMenu)
cbi.setImageItem(img, insert_in=plot)
def update():
noise = rng.rayleigh(size=(100,100))
XY = np.sinc(np.linspace(-10, 10, 100))
s = rng.random(1)
data = s * 10 * np.abs(XY[:, np.newaxis] * XY)**2 + noise
img.setImage(data, autoLevels=False)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(100)
app.exec() |
|
I 've played with the code now, and it is very nice. I really have only one comment, and I am not entirely sure about that one: Here's the code I have been playing with, maybe that could help out as example code? |
|
Updates:
So when creating a custom data = np.zeros(256)
data[::2] = 1.0
cmap_zebra = pg.colormap.ColorMap(pos=None, color=data, name="zebra")Specifying as ("name", |
|
Because Something feels "off" about the existing API. (It had grown organically after all) class ColorMapMenu(QtWidgets.QMenu):
def __init__(self, *, userList=None, showOnlyUser=False, showGradientSubMenu=False):Something more natural might be: class ColorMapMenu(QtWidgets.QMenu):
def __init__(self, *, userList=None, autoPopulate=None, showGradientSubMenu=False):
|
|
I have no real opinions on the API. What you have already seems quite workable and didn't generate tons of boiler-plate code in the example I played with. I am quite happy about that. Maybe @j9ac9k can chime in here? I have some thoughts, but they are just that, random thoughts :) Two opposite impressions on
And a general thought from when I was trying to explain use cases before:
On the library developer (1.) side, we tend to mash (2.) and (3.) together, but maybe we shouldn't. if we ever need a proper convention to refer to these layers, then I have no good idea what (1.) and (2.) should be, but at least to me, (3.) is clearly the "user". From that point of view,
|
|
I'm personally fine with https://doc.qt.io/qt-6/qt.html#ItemDataRole-enum I'm generally not great at contributing to API related discussions, and I'll go with something resembling a consensus unless I have very strong opinions, which I generally don't (it's so much easier to complain after it's in the public API :) ). |
|
To be clear, I have no objections to the API. This obviously still needs some documentation, but if I am I seeing this right then this PR is only part of a larger branch that you are putting together? Oh, the rayleigh noise simulated data is really neat, especially with the green-screen color map :) |
|
What would you think of the following API: class ColorMapMenu(QtWidgets.QMenu):
def __init__(self, *, userList=None, showGradientSubMenu=False, showColorMapSubMenus=False):Internal library usage has no problem being explicit/verbose about specifying |
I have been using the
I only recently "discovered" that CET-L5 is a good fit for my data. |
|
API change: updated example from #2955 (comment) import importlib.util
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np
data = np.zeros(256)
data[::2] = 1.0
cmap_zebra = pg.colormap.ColorMap(pos=None, color=data, name="zebra")
banded_gradient = np.array( [
[ int( bands*(x/127)+0.5 ) / bands for x in range(0, 127) ]
for bands in range(8, 50, 3)
] )
rng = np.random.default_rng(0)
app = pg.mkQApp()
win = pg.GraphicsLayoutWidget(show=True, title="ColorMap menu")
win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: ColorMap menu')
# Dynamically updating main plot with colorbar context menu limited to user-provided list
plot = win.addPlot(title='Color bar context menu: Fixed user-provided list', row=0, col=0, rowspan=3)
img = pg.ImageItem(axisOrder='row-major')
plot.addItem(img)
img.setImage(rng.rayleigh(size=(100,100)))
# Add elements to user-specified list of color maps:
userList = ["viridis", "CET-L5", "cividis"]
userList.append(cmap_zebra)
# Select from imported ColorCET list if available:
if importlib.util.find_spec("colorcet") is not None:
userList.append(("glasbey", "colorcet"))
# Select from imported Matplotlib list if available:
if importlib.util.find_spec("matplotlib") is not None:
userList.append(("prism", "matplotlib"))
# Create customized color map menu, showing only selected items, then add it to image:
colorMapMenu = pg.ColorMapMenu(userList=userList)
cbi = pg.ColorBarItem(colorMap='CET-L5', colorMapMenu=colorMapMenu)
cbi.setImageItem(img, insert_in=plot)
# Top plot: No extra code is required to enable default menu:
plotA = win.addPlot(row=0, col=2, title='Color bar context menu: Default full selection')
imgA = pg.ImageItem(banded_gradient, axisOrder='row-major')
plotA.addItem(imgA)
cbiA = pg.ColorBarItem(colorMap='plasma')
cbiA.setImageItem(imgA, insert_in=plotA)
# Middle plot: The user specified list can be shown together with the default list:
plotB = win.addPlot(row=1, col=2, title='Color bar context menu: Expanded by user list')
imgB = pg.ImageItem(banded_gradient, axisOrder='row-major')
plotB.addItem(imgB)
colorMapMenuB = pg.ColorMapMenu(userList=userList, showColorMapSubMenus=True)
cbiB = pg.ColorBarItem(colorMap='viridis', colorMapMenu=colorMapMenuB)
cbiB.setImageItem(imgB, insert_in=plotB)
# Bottom plot: If it is not needed, the color map menu can be disabled:
plotC = win.addPlot(row=2, col=2, title='Color bar context menu: Disabled')
imgC = pg.ImageItem(banded_gradient, axisOrder='row-major')
plotC.addItem(imgC)
cbiC = pg.ColorBarItem(colorMap='cividis', colorMapMenu=False)
cbiC.setImageItem(imgC, insert_in=plotC)
# Animate the primary plot:
def update():
noise = rng.rayleigh(size=(100,100))
XY = np.sinc(np.linspace(-10, 10, 100))
s = rng.random(1)
data = s * 10 * np.abs(XY[:, np.newaxis] * XY)**2 + noise
img.setImage(data, autoLevels=False)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(100)
win.show()
pg.exec() |
|
If a
|
|
@pijyoi I know we haven't been enforcing this much, but I would try and modify the docstring to be more inline with numpydoc format; see here: https://numpydoc.readthedocs.io/en/latest/format.html#parameters I'd be happy to make the change to your commit if you'd rather I do it. |
Please do. I have no idea what I am doing wrong and was just glad it passed the docs CI. |
I'll add a commit and let you squash as you see fit in a few hours, have to get back to my job that pays 😆 The CI docs are green if sphinx is able to build them without warnings. There is no check on docstring style guide, and we don't have "nit picky" mode enabled to even verify all the links work (although I should probably get on that). A good chunk of the library is still in google/sphinx style docstrings, only the newer stuff I've tried to steer towards numpy style ... maybe I should start chipping away at the legacy stuff when I need something small to do. |
Matplotlib's docs do categorize its colormaps. Perhaps it would be useful to break up Matplotlib's colormaps into further submenus. |
- move to ColorMapMenu.py - split colorcet into further submenus - split matplotlib cmaps into submenus - allow user to supply their own list of colormaps - wrap our action data in a namedtuple for identification
|
Some other examples of usage of Adding a import pyqtgraph as pg
import numpy as np
rng = np.random.default_rng(0)
pg.mkQApp()
win = pg.GraphicsLayoutWidget(show=True)
plot = win.addViewBox()
# must set removable=True, otherwise ImageItem doesn't get a menu
img = pg.ImageItem(axisOrder='row-major', removable=True)
plot.addItem(img)
img.setImage(rng.rayleigh(size=(100,100)))
submenu = pg.ColorMapMenu(showColorMapSubMenus=True)
submenu.sigColorMapTriggered.connect(img.setColorMap)
img.getMenu().addMenu(submenu)
pg.exec()Setting a custom import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
import numpy as np
rng = np.random.default_rng(0)
pg.mkQApp()
win = pg.GraphicsLayoutWidget()
win.show()
plot = win.addPlot()
hlut = pg.HistogramLUTItem()
win.addItem(hlut)
img = pg.ImageItem(axisOrder='row-major')
plot.addItem(img)
img.setImage(rng.rayleigh(size=(100,100)))
hlut.setImageItem(img)
hlut.setHistogramRange(0, 10)
def setColorMap(cmap):
hlut.gradient.setColorMap(cmap)
hlut.gradient.showTicks(False)
userList = ["viridis", "CET-L5", "cividis"]
menu = pg.ColorMapMenu(userList=userList)
menu.sigColorMapTriggered.connect(setColorMap)
hlut.gradient.menu = menu
setColorMap(pg.colormap.get("CET-L5"))
def update():
noise = rng.rayleigh(size=(100,100))
XY = np.sinc(np.linspace(-10, 10, 100))
s = rng.random(1)
data = s * 10 * np.abs(XY[:, np.newaxis] * XY)**2 + noise
img.setImage(data, autoLevels=False)
timer = QtCore.QTimer()
timer.timeout.connect(update)
timer.start(100)
pg.exec() |
|
@pijyoi sorry it took me so long to get to this. Take a look at the rendered documentation output and the docstring yourself. If you think it looks good, let me know and I'll merge, but I'm very open to suggested changes. |
|
looks good to me |
|
Thanks again for your PR @pijyoi again, my apologies it took so long to follow up here. |

#2779 added functionality to allow selection of all the colormaps to
GradientEditorItem.While that same functionality can be easily added to
ColorBarItem, the question is whether it should?Or add an option (defaulted to False) to enable it?