10000 Support individual styling of major and minor grid through rcParams by konmenel · Pull Request #29481 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Support individual styling of major and minor grid through rcParams #29481

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from

Conversation

konmenel
Copy link

PR summary

Possibly closes #13919 issue. Additionally, PR #26121 also tried to solve the same issue but I understand that it is not backwards compatible and there haven't been any updates since 2023. My implementation should be backwards compatible.

I have tested the code with pytest. Some tests failed on my machine, but all were latex tests. The problem is probably my latex installation.

I also tested to code with a small python script that:

  1. Modifies the rcParams at runtime for 3 different scenarios and uses
  2. Uses mstyle files for the same 3 scenario

The results were identical and as expected.

Test

My test script:

# test_grid_rcparams.py
import matplotlib as mpl
import matplotlib.pyplot as plt

dpi = 150
#---------------------------------------------
# RUNTIME
#---------------------------------------------
mpl.rcdefaults()
mpl.rcParams["axes.grid"] = True
mpl.rcParams["ytick.minor.visible"] = True
mpl.rcParams["xtick.minor.visible"] = True
mpl.rcParams["axes.grid.which"] = "both"
mpl.rcParams["grid.color"] = "red"
mpl.rcParams["grid.linewidth"] = 1
mpl.rcParams["grid.linestyle"] = "-"
mpl.rcParams["grid.alpha"] = 1
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("runtime1.png", dpi=dpi)

mpl.rcdefaults()
mpl.rcParams["axes.grid"] = True
mpl.rcParams["ytick.minor.visible"] = True
mpl.rcParams["xtick.minor.visible"] = True
mpl.rcParams["axes.grid.which"] = "both"
mpl.rcParams["grid.color"] = "red"
mpl.rcParams["grid.linewidth"] = 1
mpl.rcParams["grid.linestyle"] = "-"
mpl.rcParams["grid.alpha"] = 1

mpl.rcParams["grid.major.color"] = "black"
mpl.rcParams["grid.major.linewidth"] = 2
mpl.rcParams["grid.major.linestyle"] = ":"
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("runtime2.png", dpi=dpi)

mpl.rcdefaults()
mpl.rcParams["axes.grid"] = True
mpl.rcParams["ytick.minor.visible"] = True
mpl.rcParams["xtick.minor.visible"] = True
mpl.rcParams["axes.grid.which"] = "both"
mpl.rcParams["grid.color"] = "red"
mpl.rcParams["grid.linewidth"] = 1
mpl.rcParams["grid.linestyle"] = "-"
mpl.rcParams["grid.alpha"] = 1

mpl.rcParams["grid.minor.color"] = "red"
mpl.rcParams["grid.minor.linewidth"] = 0.5
mpl.rcParams["grid.minor.linestyle"] = ":"
mpl.rcParams["grid.minor.alpha"] = 0.5
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("runtime3.png", dpi=dpi)


#---------------------------------------------
# MSTYLE FILE
#---------------------------------------------
mpl.rcdefaults()
plt.style.use("./style1.mstyle")
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("mstyle1.png", dpi=dpi)

mpl.rcdefaults()
plt.style.use("./style2.mstyle")
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("mstyle2.png", dpi=dpi)

mpl.rcdefaults()
plt.style.use("./style3.mstyle")
plt.figure()
plt.plot([0, 1], [0, 1])
plt.gcf().savefig("mstyle3.png", dpi=dpi)
# style1.mstyle
axes.grid: True
ytick.minor.visible: True
xtick.minor.visible: True
axes.grid.which: both
grid.color: red
grid.linewidth: 1
grid.linestyle: -
grid.alpha: 1
# style2.mstyle
axes.grid: True
ytick.minor.visible: True
xtick.minor.visible: True
axes.grid.which: both
grid.color: red
grid.linewidth: 1
grid.linestyle: -
grid.alpha: 1

grid.major.color: black
grid.major.linewidth: 2
grid.major.linestyle: :
# style3.mstyle
axes.grid: True
ytick.minor.visible: True
xtick.minor.visible: True
axes.grid.which: both
grid.color: red
grid.linewidth: 1
grid.linestyle: -
grid.alpha: 1

grid.minor.color: red
grid.minor.linewidth: 0.5
grid.minor.linestyle: :
grid.minor.alpha: 0.5

Results

mstyle1
mstyle2
mstyle3
runtime1
runtime2
runtime3

PR checklist

Copy link
@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Thank you for opening your first PR into Matplotlib!

If you have not heard from us in a week or so, please leave a new comment below and that should bring it to our attention. Most of our reviewers are volunteers and sometimes things fall through the cracks.

You can also join us on gitter for real-time discussion.

For details on testing, writing docs, and our review process, please see the developer guide

We strive to be a welcoming and open project. Please follow our Code of Conduct.

@timhoffm timhoffm changed the title FIX: issue #13919 Support individual styling of major and minor grid through rcParams Jan 27, 2025
Copy link
Member
@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. The solution is surprisingly simple. I would have thought that there are more complications because of the concurrent settings and the need for backward compatibility.

The only improvement needed is the clearer distinction on the color and linewidth parameter values between "none" and None.

Comment on lines 1240 to 1241
"grid.major.color": validate_color, # grid color
"grid.major.linestyle": _validate_linestyle, # solid
Copy link
Member

Choose a reason for hiding this comment

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

We have to be a bit more precise with these. The string "none" is a valid value for both. We should not repurpose it here with the meaning not defined. In the parsed rcParams dict, we need to be able to have both values None and "none".
Since matplotlibrc is purely text based, we have to distinguish by capitalization. 'None' must be parsed to None, and 'none' must be parsed to "none". Please create respective validate_color_or_None and validate_linestyle_or_None functions for that.

Copy link
Author

Choose a reason for hiding this comment

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

Sounds good. I was thinking of doing something like that but I saw that the functions parse the "None" and "none", so I didn't know if I should introduce a new function or not. Nevertheless, this should be easy.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, it’s a bit unfortunate that the existing parameters are permissive wrt. to capitalization. But the new ones have to distinguish. We may deprecate accepting None for the other variants. But that’d be for a separate PR.

grid_linewidth = mpl._val_or_rc(grid_linewidth, "grid.linewidth")
grid_color = (
mpl._val_or_rc(grid_color, "grid.color")
if mpl.rcParams[f"grid.{major_minor}.color"] == "none"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if mpl.rcParams[f"grid.{major_minor}.color"] == "none"
if mpl.rcParams[f"grid.{major_minor}.color"] is None

see comment on validation

)
grid_linestyle = (
mpl._val_or_rc(grid_linestyle, "grid.linestyle")
if mpl.rcParams[f"grid.{major_minor}.linestyle"] == "none"
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if mpl.rcParams[f"grid.{major_minor}.linestyle"] == "none"
if mpl.rcParams[f"grid.{major_minor}.linestyle"] is None

see comment on validation

grid_alpha = mpl.rcParams["grid.alpha"]
grid_alpha = (
mpl.rcParams["grid.alpha"]
if f"grid.{major_minor}.alpha" not in mpl.rcParams
Copy link
Member

Choose a reason for hiding this comment

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

Why is the logic here different (hasattr) compared to the other parameters (is None)?

Copy link
Author
@konmenel konmenel Jan 27, 2025

Choose a reason for hiding this comment

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

Honestly, I have no idea so the only explanation is stupidity. Furthermore, I think this is a bug because grid.minor/major.alpha is always in rcParams. I will fix this so that is the same with the rest.

@timhoffm
Copy link
Member

One related aspect to think about: Do we anticipate that we'll need a distinction between x and y axis as well? Because if so, we should directly introduce these.

timhoffm pushed a commit to timhoffm/matplotlib that referenced this pull request Jan 27, 2025
The `*_or_None` validators have accepted any capitalization of the
string "none" as input. This is overly permissive and
will likely lead to conflicts in the future because we cannot
distinguish between resolving to `None` and `"none"` which may be need
for some parameters in the future.

Inspired by matplotlib#29481.
@konmenel
Copy link
Author
konmenel commented Jan 27, 2025

I made the suggested changes.

One related aspect to think about: Do we anticipate that we'll need a distinction between x and y axis as well? Because if so, we should directly introduce these.

Can you clarify what you meant by "a distinction between x and y axis"? Do you mean something like

rcParams["grid.major.xaxis.linestyle"] = ":"
rcParams["grid.major.yaxis.linestyle"] = "-"

timhoffm pushed a commit to timhoffm/matplotlib that referenced this pull request Jan 27, 2025
The `*_or_None` validators have accepted any capitalization of the
string "none" as input. This is overly permissive and
will likely lead to conflicts in the future because we cannot
distinguish between resolving to `None` and `"none"` which may be need
for some parameters in the future.

Inspired by matplotlib#29481.
@konmenel
Copy link
Author
konmenel commented Jan 27, 2025

Sorry I forgot to update the ".pyi" file

@timhoffm
Copy link
Member

between x and y axis"? Do you mean something like

rcParams["grid.major.xaxis.linestyle"] = ":"
rcParams["grid.major.yaxis.linestyle"] = "-"

Basically yes. Exact spelling t.b.d.

@@ -140,6 +141,7 @@ def validate_fillstylelist(
) -> list[Literal["full", "left", "right", "bottom", "top", "none"]]: ...
def validate_markevery(s: Any) -> MarkEveryType: ...
def _validate_linestyle(s: Any) -> LineStyleType: ...
def _validate_linestyle_or_None(s: Any) -> LineStyleType | None: ...
Copy link
Author

Choose a reason for hiding this comment

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

Not sure why mypy fails for this function but it doesn't for the _validate_linestyle. The stub and runtime arguments are different for both.

Copy link
Member

Choose a reason for hiding this comment

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

No idea. Just make the arguments the same.

@konmenel
Copy link
Author
konmenel commented Jan 27, 2025

Basically yes. Exact spelling t.b.d.

I think this is possible however it might look "ugly". The change will require the Tick class to know which child (XTick, YTick) was instantiated since the lines are created in the parent class.

One possible way is by accessing the self.__name__ attribute which should either be "xtick" or "ytick" since the base class is never instantiated. However, this might cause problems if this attribute ever changes which might be hard to debug. It is also possible to add a new attribute to the Tick class which will default to None and will be set by the child class to be used explicitly for this purpose so that we don't use the __name__ attribute.

Another way is to add an extra required argument to the constructor of Tick, probably an enum or a bool. For instance, an argument axis that will be either 1 or 2 if XTick or YTick has called the Tick.__init__ method, respectively.

Any thoughts on those?

@timhoffm
Copy link
Member

We already rely on Tick.__name__ see

name = self.__name__
major_minor = "major" if major else "minor"
self._size = mpl._val_or_rc(size, f"{name}.{major_minor}.size")
self._width = mpl._val_or_rc(width, f"{name}.{major_minor}.width")
self._base_pad = mpl._val_or_rc(pad, f"{name}.{major_minor}.pad")
color = mpl._val_or_rc(color, f"{name}.color")
labelcolor = mpl._val_or_rc(labelcolor, f"{name}.labelcolor")
if cbook._str_equal(labelcolor, 'inherit'):
# inherit from tick color
labelcolor = mpl.rcParams[f"{name}.color"]
labelsize = mpl._val_or_rc(labelsize, f"{name}.labelsize")

It's ok to use that.

Spelling: We have "xaxis", "xticks", "xscale" as axis-dependent names (but they are all also used in the python interface). We do not have any xgrid or similar names. Options:

  • "xgrid.major.linestyle" - in analogy to "xticks"
  • "grid.xaxis.major.linestyle" / "grid.x.major.linestyle" - still put it under the "grid" top level group
  • "xtick.major.grid.linestyle" - put everything under the corresponding tick
  • "xaxis.grid.major.linestyle" - put everything under the corresponding axis

None of them look like the obvious right choice to me.

@konmenel
Copy link
Author

Out of the four, I prefer two:

  • "xtick.major.grid.linestyle" - many options already exist under "xtick.major.*" so it is logical.
  • "grid.xaxis.major.linestyle" - configures similar to both axis. Also, it will be in a similar place in the matplotlibrc making easier to find if you are looking for it.

I will try to implement the second one for now. I can change the key later.

timhoffm added a commit to timhoffm/matplotlib that referenced this pull request Jan 27, 2025
The `*_or_None` validators have accepted any capitalization of the
string "none" as input. This is overly permissive and
will likely lead to conflicts in the future because we cannot
distinguish between resolving to `None` and `"none"` which may be need
for some parameters in the future.

Inspired by matplotlib#29481.
@konmenel
Copy link
Author

I implemented the x and y axis distinction however I came across something that needs further clarification. Right now "grid.xaxis.major.linestyle" is a valid key but "grid.xaxis.linestyle" is not. Should it be a valid option as well? If so it is unclear which of "grid.major.linestyle"and "grid.xaxis.linestyle" should take precedence.

@timhoffm
Copy link
Member

No, we don't need "grid.xaxis.linestyle". If starting from scratch, I'd just have the granular settings "grid.xaxis.major.linestyle" etc. After all, these are advanced style configuration options, and if somebody cares to adapt them, it's ok to write out the details. We keep the high-level ones "grid.linestyle" for backward compatibility (and it is a nice convenience) but we don't need the middle-ground.

Copy link
Member
@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Looks good. Please add a What's New note according to https://matplotlib.org/devdocs/devel/api_changes.html#what-s-new-notes

`_val_or_rc` now accept multiple rc names and return val or the first non-None value in rcParams. Returns last rc name if all other are None. Also, simplified code in `Tick` for grid lines creatation
@konmenel
Copy link
Author

I added the What's New note. I am not if the syntax is correct for the rc parameters. I have never used rst before so let me know if it needs correcting. Also, I tried to resolve the stubtest unsuccessfully.

@timhoffm
Copy link
Member

Also, I tried to resolve the stubtest unsuccessfully.

I suppose you've tricked yourself. You've made the _validate_linestyle parameter names consistent, which renders the allowlist entry unnecessary (and mypy complains on that). Then, you've repeated the same for _validate_linestyle_or_None.

Try removing both allowlist entries.

Copy link
Member
@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

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

Please also add a test. Basically, create a plot with the rcParams set and investigate the grid line properties, e.g. using ax.xaxis.get_gridlines()[0].get_color(). You might need same_color() for comparison if the returned color format does not match the input format.

Using :rc:`grid.major.*` or :rc:`grid.minor.*` will overwrite the value in
:rc:`grid.*` for the major and minor gridlines, respectively. Similarly,
specifying :rc:`grid.xaxis.major.*` and :rc:`grid.yaxis.major.*` will overwrite
`grid.major.*` for x and y axis major gridlines respectively.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
`grid.major.*` for x and y axis major gridlines respectively.
:rc:`grid.major.*` for x and y axis major gridlines respectively.

Should fix sphinx.

specifying :rc:`grid.xaxis.major.*` and :rc:`grid.yaxis.major.*` will overwrite
`grid.major.*` for x and y axis major gridlines respectively.

.. plot::
Copy link
Member

Choose a reason for hiding this comment

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

The example is a bit heavy. This should be illustrative easy to grap. It should give an idea what you can do and how. It's not an exhaustive documentation of the feature. The rendered entry should fit on a (large) screen. Please boil down to one figure (you can have two Axes side by side if really needed) and take a subset of rcParams only.

Typically, one figure should be enough. Select a few parameters.

Comment on lines 1263 to 1282
"grid.xaxis.major.color": validate_color_or_None, # grid color
"grid.xaxis.major.linestyle": _validate_linestyle_or_None, # solid
"grid.xaxis.major.linewidth": validate_float_or_None, # in points
"grid.xaxis.major.alpha": validate_float_or_None,

"grid.xaxis.minor.color": validate_color_or_None, # grid color
"grid.xaxis.minor.linestyle": _validate_linestyle_or_None, # solid
"grid.xaxis.minor.linewidth": validate_float_or_None, # in points
"grid.xaxis.minor.alpha": validate_float_or_None,

"grid.yaxis.major.color": validate_color_or_None, # grid color
"grid.yaxis.major.linestyle": _validate_linestyle_or_None, # solid
"grid.yaxis.major.linewidth": validate_float_or_None, # in points
"grid.yaxis.major.alpha": validate_float_or_None,

"grid.yaxis.minor.color": validate_color_or_None, # grid color
"grid.yaxis.minor.linestyle": _validate_linestyle_or_None, # solid
"grid.yaxis.minor.linewidth": validate_float_or_None, # in points
"grid.yaxis.minor.alpha": validate_float_or_None,

Copy link
Member

Choose a reason for hiding this comment

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

Seeing this in code, it feels a bit too much. I've browsed though a lot of plots and didn't find relevant usage. If the grids are different on x and y, it's typically that only one of them is activated. I've not seen the case that people use different styles for x and y.

Therefore I revert my previous proposal and think we should leave this out (YAGNI). Sorry for the back-and-forth, but sometimes one has to go the step to see it was wrong. On the plus side, this interplay lead to the expansion of _val_or_rc() which I think is a real win.

Copy link
Author

Choose a reason for hiding this comment

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

That is far. I will revert the changes. It's simple anyway with the new _val_or_rc() to go back-and-forth.

@konmenel
Copy link
Author

Do you want me to create the test in the file "test_rcparams.py" or create a separate file?

@timhoffm
Copy link
Member

Please put it in test_axis.py. This is where the semantic code lives and where it logically belongs. test_rcparams.py is only for the rcparams infrastructure not testind individual values.

@konmenel
Copy link
Author

I was working on the test and I realised that the get_gridlines() only returns the major gridlines. If it's okay I will introduce two new functions get_major_gridlines() and get_minor_gridlines() to be explicit. That way I will able to easily extract the minor gridlines as well. Also, the original function will now call the get_major_gridlines() to be backwards compatible

@timhoffm
Copy link
Member

Then use ax.xaxis.get_major_ticks()[0].gridline. There are various ways to drill down, and not all are complete or consistent. Sorting/consolidating that is for another time. Let's not create more functions as part of this PR.

@konmenel
Copy link
Author

I fix the spelling so all tests should pass now. If there is anything else please let me know.

@carlosgmartin
Copy link

@timhoffm Any idea when this might be merged?

@timhoffm
Copy link
Member
timhoffm commented Apr 1, 2025

This needs a second review by a core developer, and review time is scarce. However, this is flagged for 3.11 and I'm pretty sure it will get the attention in time for the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Needs decision
Development

Successfully merging this pull request may close these issues.

Impossible to configure minor/major grid line style independently in rcParams
3 participants
0