pyqtgraph / pyqtgraph

Fast data visualization and GUI tools for scientific / engineering applications
https://www.pyqtgraph.org
Other
3.9k stars 1.1k forks source link

Bug in colormap parameter #2787

Open MrBeee opened 1 year ago

MrBeee commented 1 year ago

Short description

When loading a colormap parameter, it defaults to a colormap with a gradient between black (left) and red (right) This default 'looks good' but may not be the colormap you would like to use as a default value'

You can provide a default colormap by giving a colormap as a starting value

Code to reproduce

import pyqtgraph as pg
import numpy as np
from pyqtgraph.parametertree import (Parameter, ParameterTree, registerParameterType)

        ...
        cmap = pg.colormap.get("viridis")
        colorParams = [
            dict(name='Color Settings', type='group', children=[
                dict(name='ColorMap', type='colormap', value=cmap),
            ]),
        ]

        self.parameters = pg.parametertree.Parameter.create(name='Analysis Settings', type='group', children=colorParams)

Expected behavior

The viridis colorscheme should be loaded with 5 tickmarks, and a white background behind the tickmarks image

Real behavior

The viridis colorscheme is loaded with 5 tickmarks, and a black background behind the tickmarks image

Tested environment(s)

Additional context

None

pijyoi commented 1 year ago

The colormap loaded by pg.colormap.get("viridis") actually has 256 stops.

It is the squashed over-painting of these 256 ticks that results in the appearance of a black background.

MrBeee commented 1 year ago

@pijyoi

Thanks for your reply, but I'm afraid it is only a half-answer.

Colormap implementation in GradientEditorItem.py

I checked the file viridis.csv in the pyqtgraph\pyqtgraph\colors\maps\ folder. Indeed it contains 256 ticks in lines 15 to 270. So you are right in that respect.

However, viridis is defined again in the file GradientEditorItem.py where the definition is as follows:

('viridis', {'ticks': [(0.0, (68, 1, 84, 255)), (0.25, (58, 82, 139, 255)), (0.5, (32, 144, 140, 255)), (0.75, (94, 201, 97, 255)), (1.0, (253, 231, 36, 255))], 'mode': 'rgb'})

In my view you should not have two different colormap definitions that carry the same name in the same software package. This is utterly confusing for the end user.

In my view, GradientEditorItem.py should also pull its resources from the pyqtgraph\pyqtgraph\colors\maps\ folder, using cmap = pg.colormap.get(item) .

To create a list with colormaps to chose from, it may have a built-in default list using some default 'limits'

E.g. self.limits = ['thermal', 'flame', 'yellowy', 'bipolar', 'spectrum', 'cyclic', 'greyclip', 'grey', 'viridis', 'inferno', 'plasma', 'magma']

This way, the list can be overruled as a 'limits' option by the end user. But for consistency, the colormaps should always be read from pyqtgraph\pyqtgraph\colors\maps\ .

Inconsistency in colormap selection

There are three pyqtgraph examples that define the colormap using cmap = colormap.get('viridis'). These are MultiDataPlot.py, PColorMeshItem.py, and PColorMeshItem.py.

In these cases there's no 'black background' in the tickmark area, so they (somehow) get their colormap directly from GradientEditorItem.py

The bug (feature?) that I reported only occurs when you define a colormap in a parameter list as shown earlier in the 'Code to reproduce'

Finally

Another reason to avoid the use of GradientEditorItem.py is the occurence of a NotImplementedError when editing hsv colormaps. I don't want end users to be confronted with this error. To me it seems this file needs a bit of extra work...

pijyoi commented 1 year ago

The gradients in GradientEditorItem pre-date the colormaps stored in pyqtgraph/colors/maps. The former date back to 2012, while the latter are a fairly recent addition (2021/06). Except for GradientEditorItem, most GraphicsItems make use of the pyqtgraph/colors/maps.

I don't understand your comment about MultiDataPlot.py. It doesn't show a colorbar to begin with. As for PColorMeshItem.py, it shows a colorbar using ColorBarItem, which doesn't display ticks to begin with.

import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets

pg.mkQApp()

cmap = pg.colormap.get('viridis')

main = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
main.setLayout(layout)

layout.addWidget(QtWidgets.QLabel('GradientWidget loadPreset'))
gw1 = pg.GradientWidget()
gw1.loadPreset('viridis')
layout.addWidget(gw1)

layout.addWidget(QtWidgets.QLabel('GradientWidget setColorMap'))
gw2 = pg.GradientWidget()
gw2.setColorMap(cmap)
layout.addWidget(gw2)

layout.addWidget(QtWidgets.QLabel('ColorBarItem'))
gv = pg.GraphicsView()
cbi = pg.ColorBarItem(colorMap=cmap, orientation='horizontal')
gv.setCentralItem(cbi)
layout.addWidget(gv)

main.resize(200, 200)
main.show()
pg.exec()
MrBeee commented 1 year ago

Thanks for explaining the history about GradientEditorItem. And looking a bit more into examples that use that widget, I think I mixed up using cmap = pg.colormap.get('viridis') to define a value in dict(name='ColorMap', type='colormap', value=cmap), with the use of loadPreset(), as demonstrated in the example GradientWidget.py:

w4 = pg.GradientWidget(orientation='left')
w4.loadPreset('spectrum')

But that still begs the questions;

  1. How do you get the list of 12 available gradient names you can choose from ?
  2. How do you set one of these as a colormap value in the code I showed earlier above:
        colorParams = [
            dict(name='Color Settings', type='group', children=[
                dict(name='ColorMap', type='colormap', value=cmap),
            ]),
        ]

Bottom line, the 70 available colormaps in the folder pyqtgraph\pyqtgraph\colors\maps\ and the 12 colormaps in the GradientEditorItem.py are out of sync.

Presently there is no insightful widget (apart from a plain 'list') that allows for selecting one of these 70 colormaps.

That's why I suggested a solution in discussion issue 2788

Cheers.

PS: thanks for the example code above. If you stretch the main window horizontally, you'll start to notice the individual 256 ticks. At the same time, you see there's no difference in the color gradient between the bar based upon 5 ticks (top) and the one based upon 256 ticks (bottom). So there's no value in applying 256 points. Maybe this was the easiest way to copy stuff from matplotlib, but it certainly wasn't the most clever way...

pijyoi commented 1 year ago

How do you get the list of 12 available gradient names you can chose from ? How do you set one of these as a colormap value in the code I showed earlier above:

import pyqtgraph as pg
import pyqtgraph.parametertree as ptree

from pyqtgraph.graphicsItems.GradientEditorItem import Gradients

print(list(Gradients.keys()))
cmap = pg.ColorMap(*zip(*Gradients["viridis"]["ticks"]))

pg.mkQApp()

params = ptree.Parameter.create(name='Parameters', type='group', children=[
    dict(name='ColorMap', type='colormap', value=cmap),
])
pt = ptree.ParameterTree(showHeader=False)
pt.setParameters(params)
pt.show()

pg.exec()
MrBeee commented 1 year ago

Thanks very much for going through the effort of creating this example. Much appreciated !

For folk new to the parameter tree (well for me at least) this isn't very obvious (Apart from the import statement, but in order to use this, you need to delve into the source code)

I believe from the discussion on this forum that GradientEditorItem (the widget underpinning colormap) is being replaced by another widget, so this may become less of an issue.

Nevertheless, it would be appreciated if class GradientEditorItem (and/or it successor) would have two more methods:

  1. getGradientList()
  2. getColormap(colormapName)

Whereby in GradientEditorItem, these methods could be defined as follows::

    getGradientList():
        return list(Gradients.keys())

    getColormap(colormapName):
        return pg.ColorMap(*zip(*Gradients[colormapName]["ticks"]))

Alternatively, your code could be included in any of the ParameterTree examples.

Cheers

PS: What is also needed is a manner to limit the nr of colormaps the user can choose from in the context menu. See the following example:


cmapLimits = ['thermal', flame', 'yellow']

params = ptree.Parameter.create(name='Parameters', type='group', limits=cmapLimits, children=[
    dict(name='ColorMap', type='colormap', value=cmap),
])
dx-momo commented 6 months ago

Excuse me, do you know how to set the background color of gradients in GLViewWidget? I only found the function setBackgroundColor(), but it can only set fixed color values, not gradients.