Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add style sheet in pyqtgraph #2664

Open
wants to merge 32 commits into
base: master
Choose a base branch
from
Open

Add style sheet in pyqtgraph #2664

wants to merge 32 commits into from

Conversation

edumur
Copy link
Contributor

@edumur edumur commented Mar 26, 2023

Dear all,

I thought it will be a nice feature to have style sheet, in the "same" way as matplotlib.
Here, I have just implemented a very simple version of that feature in hope to have feedback before going further.
Currently nothing is changed (and normally nothing is crashing) except all style options are now on a dedicated default style sheet.

How it works

When pyqtgraph is loaded, the default style is loaded from the new core.style file.
All the style options are stored in configStyle which is a dictionnary.
Throughout the lib, default style options are taken from that dictionnary.

To use other styles, use core.use and load another style file.

Some remarks

  • There is currently two options dict, the config and the style one. There are not access the same way. We should decide one one way and use that everywhere.
  • Is it possible to have different item with and without aliasing?
  • I am not at all a python or qt wizards so if something looks weirds it could be because I messed up somewhere. Please tell me if that's the case!
  • current extension is pstyle... Do we keep it?

Tasks

Item to go through and transfer to the new styling paradigms:

  • LabelItem
  • LegendlItem
  • AxisItem
  • GridItem
  • GraphItem
  • PlotCurveItem
  • PlotDataItem
  • BarItem
  • ErrorBarItem
  • ScaleBarItem
  • ScatterPlotItem

What I plan to do if we decide to move forwards

  • Add more style options in the stylesheet (size, color, font, ...).
  • Make an official "white" version of pyqtgraph through a second style sheet.
  • Make an example to demonstrate the use of the feature.
  • Add test

To be checked:

  • Working with pyplotter
  • Working with orange

Example

White official style
image

Breaking change

Currently None.

Other Tasks

Bump Dependency Versions

Files that need updates

I confirm that I didn't touch that yet.

  • README.md
  • setup.py
  • tox.ini
  • .github/workflows/main.yml and associated requirements.txt and conda environemt.yml files
  • pyproject.toml
  • binder/requirements.txt
Pre-Release Checklist

Pre Release Checklist

I confirm that I didn't touch that yet.

  • Update version info in __init__.py
  • Update CHANGELOG primarily using contents from automated changelog generation in GitHub release page
  • Have git tag in the format of pyqtgraph-
Post-Release Checklist

Steps To Complete

I confirm that I didn't touch that yet.

  • Append .dev0 to __version__ in __init__.py
  • Announce on mail list
  • Announce on Twitter

pyqtgraph/graphicsItems/LabelItem.py Fixed Show fixed Hide fixed
pyqtgraph/graphicsItems/LabelItem.py Fixed Show fixed Hide fixed
pyqtgraph/graphicsItems/LabelItem.py Fixed Show fixed Hide fixed
@j9ac9k
Copy link
Member

j9ac9k commented Mar 28, 2023

Feel free to use #1625 for inspiration

That PR never got merged in large part due to the size/scope and my reluctance to merge something with such a big diff... I do regret not reviewing that more thoroughly in a timely fashion.

### Set style options
## All options related to style are set here
## Init default style
from .style.core import loadDefautStyle
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should that be "DefaultStyle" throughout, maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling: "Defaut" != "Default"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol that's a very French mistake to do ^^ .

Thanks.

@NilsNemitz
Copy link
Contributor

Hi @edumur ,

It is great to see someone working on this, I hope you can get this merged!
My own attempts (there's also #1921 in addition to #1621 ) stalled since I couldn't figure out a scope that wouldn't break the entire library at the same time.

But there's some things in that earlier attempt that might be worth looking at. Two things come to mind:

It would be great to expose some of the style colors through the existing color dictionary. For example, so that user generated graphs (drawn with a "foreground color") can function in both light and dark mode.

I had a working method to send redraw requests to all pyqtgraph items. That made it possible to apply changes to existing graphs. I had been expecting to find something built in on the Qt level, but somehow that all failed to work.

There's also a number of color palettes for styling already :)

Best of luck!

Unfortunately, I looks like I'll be in zero-free-time mode until at least end of May.

@edumur
Copy link
Contributor Author

edumur commented Mar 29, 2023

Thanks for sharing the previous request!

@edumur
Copy link
Contributor Author

edumur commented Mar 29, 2023

Is there any docstring style recommanded?

@j9ac9k
Copy link
Member

j9ac9k commented Mar 29, 2023

Is there any docstring style recommanded?

numpy docstyle for sure, ... maybe one evening if I'm too tired to do any coding, I can start converting existing docstrings over...

@edumur
Copy link
Contributor Author

edumur commented Mar 30, 2023

Is there any docstring style recommanded?

numpy docstyle for sure, ... maybe one evening if I'm too tired to do any coding, I can start converting existing docstrings over...

Of course I started with google style...
I will do it in numpy then.

@edumur
Copy link
Contributor Author

edumur commented Apr 8, 2023

Is there a reason why some objects are defined with an Item in there name such as "LabelItem" and others without such as "ScaleBar"?

@j9ac9k
Copy link
Member

j9ac9k commented Apr 8, 2023

Is there a reason why some objects are defined with an Item in there name such as "LabelItem" and others without such as "ScaleBar"?

So first I want to acknowledge that our API consistency isn't as good as it should be.

That said, PyQtGraph classes that end in Item typically inherit from QGraphicsItem. I'm on mobile so not sure about ScaleBar specifically but classes that don't end in Item are typically QWidgets.

We recently added this diagram to help with understanding the relationships between PyQtGraph and parent Qt classes.

https://pyqtgraph.readthedocs.io/en/latest/api_reference/uml_overview.html

@edumur
Copy link
Contributor Author

edumur commented Apr 8, 2023

Thanks, I am still confused with "item", "object", "view" and, "widget".
The disgram is nice thought!

@j9ac9k
Copy link
Member

j9ac9k commented Apr 8, 2023

Thanks, I am still confused with "item", "object", "view" and, "widget". The disgram is nice thought!

SomethingObject inherits from QGraphicsObject, and SomethingView from QGraphicsView, same with QGraphicsWidget for being SomethingWidget. What complicates things is that QGraphicsObject inherits from QGraphicsItem, and QGraphicsWidget inherits from QGraphicsObject 😆

Also what can complicate matters is that I don't believe you can do multiple inheritances of QObject sublcasses

All that said, we do have inconsistencies in our naming scheme which definitely does not help.

@edumur
Copy link
Contributor Author

edumur commented Apr 12, 2023

Hi all,

Some items have their style option stored in an attribute self.opts and others in self.styles.
I would like to make it consistante across the lib.
Any preference?

@j9ac9k
Copy link
Member

j9ac9k commented Apr 12, 2023

Hi all,

Some items have their style option stored in an attribute self.opts and others in self.styles. I would like to make it consistante across the lib. Any preference?

I low-key dislike self.opts ... I tend to fall back towards whatever the upstream API is for this sort of thing, in this case, where Qt objects store style information, ...but for this sort of thing I'm not seeing something that's analogous in the Qt API; so I would lean towards self.styles myself; but I'll go with whatever you would prefer (given you're the one actually doing the work :) )

@ntjess
Copy link
Contributor

ntjess commented Apr 12, 2023

Can you link a few locations where styles is used? I found some with opts in the existing codebase but not with styles

Also -- Is there a reason to define a style.core with a new file format instead of relying on python's builtin ConfigParser? It seems the syntax is largely compatible and we wouldn't need to be responsible for maintaining the parser. This is one of the reasons pyqtgraph/configfile.py was discouraged for use in earlier PRs (there was a discussion in #1953 (comment) about custom file formats)

@edumur
Copy link
Contributor Author

edumur commented Apr 12, 2023

Sure: here.

style.core is made to host any general style related code.
However I would love to use a standard file format instead of the current *.pstyle !
However configparser can only store string, see here and currently the *.pstyle file is storing various type such as bool, float, list of tuple, ...
Maybe a python dict? a yaml file? something else?
I also do not like parsing the fle myself.

@j9ac9k
Copy link
Member

j9ac9k commented Apr 12, 2023

Sure: here.

style.core is made to host any general style related code. However I would love to use a standard file format instead of the current *.pstyle ! However configparser can only store string, see here and currently the *.pstyle file is storing various type such as bool, float, list of tuple, ... Maybe a python dict? a yaml file? something else? I also do not like parsing the fle myself.

I'm reluctant towards yaml as that would involve an extra dependency. Python 3.11 introduced tomllib; if we don't want to do json, I would be ok with a toml file and require end users to provide some dependency that can read it in if using python < 3.11.

@ntjess
Copy link
Contributor

ntjess commented Apr 12, 2023

Sure: here.

Thanks, I was doing a search for styles which is why I missed it

However configparser can only store string, see here

ConfigParser is easily customizable:

from configparser import ConfigParser, Interpolation
import ast

class TypedInterpolation(Interpolation):
    def before_get(self, parser, section, option, value, defaults):
        try:
            return ast.literal_eval(value)
        except (ValueError, SyntaxError):
            return super().before_get(parser, section, option, value, defaults)
typed_config = ConfigParser(interpolation=TypedInterpolation())

ini_file = """
[linePlot]
string = 'hello'
tuple = (1, 2, 3)
list = [1, 2, 3]
dict = {'a': 1, 'b': 2, 'c': 3}
int = 1
float = 1.0
bool = True
none = None
"""

typed_config.read_string(ini_file)
for key, value in typed_config.items('linePlot'):
    print(key, value, type(value))

# string hello <class 'str'>
# tuple (1, 2, 3) <class 'tuple'>
# list [1, 2, 3] <class 'list'>
# dict {'a': 1, 'b': 2, 'c': 3} <class 'dict'>
# int 1 <class 'int'>
# float 1.0 <class 'float'>
# bool True <class 'bool'>
# none None <class 'NoneType'>

This implementation essentially allows any builtin python type, defaulting to a string otherwise

@edumur
Copy link
Contributor Author

edumur commented Apr 13, 2023

@ntjess thanks, I will try this!

@j9ac9k I just realized that replacing opts by styles was a breaking change anyway so I will leave both as it is.

@@ -1268,7 +1707,7 @@
else:
return lv.mouseDragEvent(event, axis=0)

def mouseClickEvent(self, event):
def mouseClickEvent(self, event: MouseClickEvent) -> None:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.
Comment on lines +329 to +330
# if not isinstance(labelColor, ConfigColorHint):
# raise ValueError('The given labelColor type is "{}", must a "ConfigColorHint"'.format(type(labelColor).__name__))

Check notice

Code scanning / CodeQL

Commented-out code Note

This comment appears to contain commented-out code.
from math import ceil, floor, isfinite, log, log10
from typing import Any, List, Optional, Tuple, TypedDict, Union

Check notice

Code scanning / CodeQL

Unused import Note

Import of 'Any' is not used.
@@ -1254,7 +1693,7 @@
lv.wheelEvent(event, axis=0)
event.accept()

def mouseDragEvent(self, event):
def mouseDragEvent(self, event: MouseDragEvent) -> None:

Check notice

Code scanning / CodeQL

Explicit returns mixed with implicit (fall through) returns Note

Mixing implicit and explicit returns may indicate an error as implicit returns always return None.
@edumur
Copy link
Contributor Author

edumur commented Apr 17, 2023

Dear all,

To keep consistency across items, I think to rename setStyles method of AxisItem as setStyle.
This will be a breaking change.
The consequences are the following.
Before

ax.setStyle(showValues=show_value)

After

ax.setStyle('showValues', show_value)
ax.setStyles(showValues=show_value)

We could maybe do differently, it is not set in stone.

@ntjess
Copy link
Contributor

ntjess commented Apr 17, 2023

It is better to avoid breaking changes immediately; instead, consider a deprecation warning while keeping old behavior:

def setStyle(...):
    warnings.warn("`setStyle` is deprecated in favor of `setStyles`, see XXX", FutureWarning)
    ...

Also, for pieces of the library that are as old as this, deprecation may not even be an option (884 results on github alone). We might just want to leave a comment in the docstring that new behavior is preferred, but keep everything else.

Curious about others' thoughts, though

@edumur
Copy link
Contributor Author

edumur commented Apr 19, 2023

That's fine, I think I can keep the old name by renaming my new method.

I already put warnings around like this one in LabelItem:

        warnings.warn('Argument "bold" is deprecated, "fontweight" should be used instead.',
                       DeprecationWarning,
                       stacklevel=2)

@edumur
Copy link
Contributor Author

edumur commented Jun 19, 2023

Hi all,

Just push the last update.
Not done yet.

  • Most of item have the new styles implementation.
  • All test are passed
  • I added a default white style and so far, it looks good.

@j9ac9k
Copy link
Member

j9ac9k commented Jun 19, 2023

Hi there @edumur

Looks like read the docs doesn't like some of the enum namespacing;

List[Tuple[QtCore.QRectF, QtCore.Qt.Alignment, str]]]]:

I think this should be QtCore.Qt.AlignmentFlag

@j9ac9k
Copy link
Member

j9ac9k commented Jun 20, 2023

I need to go through this diff more closely; but at first glance, I would steer away from def getAttribute(self) as method names, and just stick with def attribute(). That's more in-line with the Qt API (Qt has very few methods that start with the word "get"). I know pyqtgraph has some of those, but in time I suspect we'll phase them out.

@j9ac9k
Copy link
Member

j9ac9k commented Jun 27, 2023

The read the docs error in the CI has already been fixed on master, so you'll have to rebase master onto your branch for it to work (also likely will need to do that anyway due to merge conflicts).

@edumur
Copy link
Contributor Author

edumur commented Jun 28, 2023

To be sure I do not f** up things.
Could someone provide the git commands to pull the master and rebase it into my branch?

Thanks.

@j9ac9k
Copy link
Member

j9ac9k commented Jun 28, 2023

To be sure I do not f** up things. Could someone provide the git commands to pull the master and rebase it into my branch?

Thanks.

Since you made a PR, I actually have push rights to your branch; I'd be happy to do it if you'd like. If you want to do it, these are the steps I would do...

# go to your branch
git switch style 

 # create a backup in case things get hosed up
git switch -c style-backup 
git switch master
# sync up with upstream locally on your disk
git pull upstream master  
git switch style
git rebase master

When you run that last command, you're going to get merge conflicts that stop the rebase process, on each one of those, you'll have to resolve the conflict in the file, then run git add <path-to-file-with-conflict-now-resolved> and then git rebase --continue

Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 10 potential problems in the proposed changes. Check the Files changed tab for more details.

@j9ac9k
Copy link
Member

j9ac9k commented Jun 28, 2023

Read the docs reports the following errors (read the docs is configured to treat warnings as errors).

reading sources... [ 21%] api_reference/graphicsItems/axisitem
:49: (ERROR/3) Unexpected indentation.
:51: (ERROR/3) Unexpected indentation.
:52: (WARNING/2) Block quote ends without a blank line; unexpected unindent.
...
reading sources... [ 26%] api_reference/graphicsItems/errorbaritem
:7: (ERROR/3) Unexpected indentation.

@edumur
Copy link
Contributor Author

edumur commented Jul 2, 2023

Is there a way to run these tests locally?

Currently I am running test.py in [yqtgraph folder and got this:

(plotter_env) PS C:\Users\etien\Desktop\edumur\script\pyqtgraph> python .\test.py
sys.platform: win32
sys.version: 3.10.11 | packaged by Anaconda, Inc. | (main, May 16 2023, 00:55:32) [MSC v.1916 64 bit (AMD64)]
qt bindings: PyQt5 5.15.9 Qt 5.15.2
pyqtgraph: 0.13.4.dev0; None
config:
{'crashWarning': False,
 'editorCommand': None,
 'enableExperimental': False,
 'exitCleanup': True,
 'imageAxisOrder': 'col-major',
 'leftButtonPan': True,
 'mouseRateLimit': 100,
 'segmentedLineMode': 'auto',
 'useCupy': False,
 'useNumba': False,
 'useOpenGL': False}
============================================================================================= test session starts ==============================================================================================
platform win32 -- Python 3.10.11, pytest-7.4.0, pluggy-1.2.0
rootdir: C:\Users\etien\Desktop\edumur\script\pyqtgraph
configfile: pytest.ini
collected 417 items

pyqtgraph\examples\test_examples.py ...................................s..................................s.s................Timeout (0:01:00)!
Thread 0x00005268 (most recent call first):
  File "C:\Users\etien\Desktop\edumur\script\pyqtgraph\pyqtgraph\examples\test_examples.py", line 164 in testExamples
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\python.py", line 194 in pytest_pyfunc_call
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_callers.py", line 80 in _multicall
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_manager.py", line 112 in _hookexec
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_hooks.py", line 433 in __call__
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\python.py", line 1788 in runtest
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 169 in pytest_runtest_call
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_callers.py", line 80 in _multicall
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_manager.py", line 112 in _hookexec
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_hooks.py", line 433 in __call__
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 262 in <lambda>
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 341 in from_call
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 261 in call_runtest_hook
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 222 in call_and_report
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 133 in runtestprotocol
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\runner.py", line 114 in pytest_runtest_protocol
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_callers.py", line 80 in _multicall
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_manager.py", line 112 in _hookexec
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_hooks.py", line 433 in __call__
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\main.py", line 349 in pytest_runtestloop
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_callers.py", line 80 in _multicall
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_manager.py", line 112 in _hookexec
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_hooks.py", line 433 in __call__
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\main.py", line 324 in _main
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\main.py", line 270 in wrap_session
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\main.py", line 317 in pytest_cmdline_main
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_callers.py", line 80 in _multicall
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_manager.py", line 112 in _hookexec
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\pluggy\_hooks.py", line 433 in __call__
  File "C:\Users\etien\anaconda3\envs\plotter_env\lib\site-packages\_pytest\config\__init__.py", line 166 in main
  File "C:\Users\etien\Desktop\edumur\script\pyqtgraph\test.py", line 24 in <module>

@edumur
Copy link
Contributor Author

edumur commented Jul 2, 2023

Nevermind, I think I solved it .

@j9ac9k
Copy link
Member

j9ac9k commented Jul 2, 2023

Nevermind, I think I solved it .

Let me know if you need help debugging test failures!

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.

None yet

4 participants