Skip to content

add ColorMapMenu to ColorBarItem#2955

Merged
j9ac9k merged 3 commits intopyqtgraph:masterfrom
pijyoi:cbi-colormenu
Apr 17, 2024
Merged

add ColorMapMenu to ColorBarItem#2955
j9ac9k merged 3 commits intopyqtgraph:masterfrom
pijyoi:cbi-colormenu

Conversation

@pijyoi
Copy link
Copy Markdown
Contributor

@pijyoi pijyoi commented Mar 9, 2024

#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?

@NilsNemitz
Copy link
Copy Markdown
Contributor

(( 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:

  • No menu
  • Menu populated by all available maps
  • Menu populated only by a limited list (with a good default)

((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.))

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 10, 2024

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.

I probably wouldn't want to overload an end-user with the full list.

The overload is mitigated by putting the colormaps into submenus. Currently:

  1. local (what you termed the "non-terrible" colormaps)
  2. colorcet (local) or colorcet (external)
  3. matplotlib
  4. colorcet (external) for those that have alternate names

I think the colorcet could perhaps be broken down further into (C, CB, D, I, L, R) submenus?
Should CB be distributed into the others? e.g. CBL and CBTL just go into L.
However, if we go down this path, the code needs to "know" more about the file naming conventions; any new categories would need to be manually coded in.

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).
That is to say, matplotlib submenu will just remain with its large selection.

Do you see a good way for the developer to supply a shortlist of colormaps that will populate the menu?

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?

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 10, 2024

An example of the current implemented API.
User passes in an instance of ColorMapMenu if they want anything other than the default.

At the moment, a list of names from the local bundled colormaps can be provided.
This does raise the question of why it wasn't made even more general by allowing the user to provide a list of ColorMap instances. As it is, ColorMapMenu entries are intended to be lightweight, and don't store the colormap itself. Hence the current implementation only accepts colormap names.

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()

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 10, 2024

How it looks like:

  1. A "curated" submenu
  2. CET colormaps are split into further submenus
    Screenshot 2024-03-10 111824

@NilsNemitz
Copy link
Copy Markdown
Contributor

NilsNemitz commented Mar 10, 2024

(I had not seen the updates above when I wrote the text below. This is in response to the earlier response.)

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.

I think that would be good, for "blacklist" = "do not automatically add anything starting with PAL- to the menu"

The overload is mitigated by putting the colormaps into submenus.

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.

Do you see a good way for the developer to supply a shortlist of colormaps that will populate the menu?

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?

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.

I think it shouldn't be difficult to implement this, provided we stick only to the bundled colormap names.

If the menu goes through the colormap.get() function, then it should be able to load anything sufficiently specified by the passed string, including imported maps, bundled maps, and developer-provided "user" maps in the local folder, I think.
The responsibility to check the list falls to the developer. I am guessing that the implementation might be as simple as passing a list of such strings. Or a list of ('display name','internal name') tuples if you want to be fancy about it.


Sorry for the long text. Back to the original point:

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?

Yes. That would be a great addition!
And if there's a not overly complex way to add the third option of a developer-curated list: That would add significant functionality.

@NilsNemitz
Copy link
Copy Markdown
Contributor

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?
But it's awesome already :)

@NilsNemitz
Copy link
Copy Markdown
Contributor

At the moment, a list of names from the local bundled colormaps can be provided.
This does raise the question of why it wasn't made even more general by allowing the user to provide a list of ColorMap instances.

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 export() method to ColorMap, making it easy to create such colormap files.

@pijyoi pijyoi marked this pull request as draft March 10, 2024 09:12
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 10, 2024

Updated API:

  1. curatedList renamed to userList
  2. added option showOnlyUser to display only user provided list
  3. external colormaps from "colorcet" and "matplotlib" can be specified in userList
  4. ColorMap instance can be specified in userList

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()

@NilsNemitz
Copy link
Copy Markdown
Contributor

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:
Maybe the user list should always be shown in the top level of the menu? (That's where it is in showOnlyUser mode; it currently moves into a user folder when that is false.) I would expect that the manually selected maps should be the most convenient to apply, maybe without clicking into a further menu.

Here's the code I have been playing with, maybe that could help out as example code?

import importlib.util
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore
from pyqtgraph.widgets.ColorMapButton import ColorMapMenu
import numpy as np

data = np.zeros(256)
data[::2] = 1.0
cmap_zebra = pg.colormap.ColorMap(pos=None, color=data)

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(("zebra", 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 = ColorMapMenu(userList=userList, showOnlyUser=True)
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 = ColorMapMenu(userList=userList, showOnlyUser=False)
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()

app.exec()

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 11, 2024

Updates:

  1. userList always in top level
  2. ColorMap(s) can be specified directly instead of ("name", ColorMap). ColorMap.name will be used as the name.

So when creating a custom ColorMap, name should be provided like so:

data = np.zeros(256)
data[::2] = 1.0
cmap_zebra = pg.colormap.ColorMap(pos=None, color=data, name="zebra")

Specifying as ("name", ColorMap) still works but should be considered an implementation detail. (Seems like setting ourselves up for Hyrum's Law https://www.hyrumslaw.com/)

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 11, 2024

Because ColorMapMenu is becoming an exposed API, it might be good to try to get the API right.

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):
  1. if userList is not supplied, then autoPopulate defaults to True
  2. if userList is supplied, then autoPopulate defaults to False
  3. showGradientSubMenu is still odd, but is there to support GradientEditorItem.
    • it does work even outside of GradientEditorItem, but we want to nudge people away from the legacy Gradients.
  4. a better name for autoPopulate?

@NilsNemitz
Copy link
Copy Markdown
Contributor

NilsNemitz commented Mar 11, 2024

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 autoPopulate:

  1. autoPopulate is perfect, because it can grow to be a parameter that specifies the auto-population more precisely. Only Matplotlib. Only ColorCET. Some obscure online resource. Not sure what the use case might be, but the API would handle it if someone feels the need to add that later.
  2. By default, userList and autoPopulate now compete for who gets to define the menu. That might be more obvious if the naming included a common topic. From that point of view, autoPopulate --> defaultList or standardList might work.

And a general thought from when I was trying to explain use cases before:
There's three different layers of working on or with pyqtgraph.

  1. You are a library developer.
  2. I take the library to write a lab tool.
  3. The Unfortunate Postdoc has to use that tool to do stuff.

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, userList is not really correct here, because it is specified by (2.) (the 'application developer'?!?). Maybe picking a name that dodges this distinction would be more future-proof?

userList --> customList, maybe?

@pijyoi pijyoi marked this pull request as ready for review March 11, 2024 10:07
@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Mar 11, 2024

I'm personally fine with userList as it sort of lines up with the UserRole the Qt namespace enum for application specific configuration for ItemDataRoles which feels close enough to me.

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 :) ).

@NilsNemitz
Copy link
Copy Markdown
Contributor

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 :)

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 11, 2024

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 showColorMapSubMenus=True.
From the "user" perspective, it has made it less verbose for the case where they only want to use their own subset of colormaps. i.e. it becomes "additive", you only ask to add entries. You don't ask to remove entries.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 11, 2024

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?

I have been using the HistogramLUTItem mostly as having the histogram really helps. Recently, I ran out of screen estate and tried putting in a ColorBarItem and thought it would be nice to port over the ColorMapMenu functionality. So there isn't a larger branch.

Oh, the rayleigh noise simulated data is really neat, especially with the green-screen color map :)

I only recently "discovered" that CET-L5 is a good fit for my data.
The script was meant to demonstrate why auto levels is not appropriate as we want to keep the intensity level of noise the same across frames.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 11, 2024

API change: showOnlyUser replaced with showColorMapSubMenus

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()

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 12, 2024

If a ColorMapMenu.sigColorMapTriggered(cmap) signal was provided, then the library code could just connect it directly to their setColorMap(cmap) methods instead of manually calling ColorMapMenu.actionDataToColorMap(action.data()) in a separate trampoline method.

GradientEditorItem would still need to connect to QMenu.triggered(action) signal to maintain the existing behavior of gradients having ticks.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Mar 12, 2024

@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.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 12, 2024

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.

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Mar 12, 2024

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.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 14, 2024

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.

Matplotlib's docs do categorize its colormaps.
https://matplotlib.org/stable/gallery/color/colormap_reference.html

Perhaps it would be useful to break up Matplotlib's colormaps into further submenus.
We could add a "Others" category to catch any new ones.

pijyoi added 2 commits March 17, 2024 17:06
- 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
@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Mar 17, 2024

Some other examples of usage of ColorMapMenu.

Adding a ColorMapMenu into an ImageItem.

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 ColorMapMenu to a HistogramLUTItem by monkey patching.
According to #2959, GradientEditorItem.menu attribute is part of the public API.

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()

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Apr 17, 2024

@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.

@pijyoi
Copy link
Copy Markdown
Contributor Author

pijyoi commented Apr 17, 2024

looks good to me

@j9ac9k
Copy link
Copy Markdown
Member

j9ac9k commented Apr 17, 2024

Thanks again for your PR @pijyoi again, my apologies it took so long to follow up here.

@j9ac9k j9ac9k merged commit ed3c30f into pyqtgraph:master Apr 17, 2024
@pijyoi pijyoi deleted the cbi-colormenu branch April 17, 2024 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants