Repository: matplotlib/matplotlib
Pull request: negative_linestyles kwarg in contour.py
## PR Summary
I made the `negative_linestyles` for contours toggle-able via kwarg. The only way to toggle this currently is with `rcParams`.

*I am re-opening this issue from a fresh branch. There was some discussion in #23102.*

In the attached issue, we had some discussion on whether this is a good idea or not, but I think it is worth considering. We can maintain current functionality and just apply the kwarg if it is defined. This will not break previous functionality. I wrote some tests for this, but I know they can be improved. I'm not completely certain on using baseline_images with parametrized inputs.

Please use this PR and the attached issue to discuss whether this is a good idea or not. I'm happy to take this in a different direction if there are some better ideas!

Closes #23028

### Previous
```
plt.rcParams['contour.negative_linestyle'] = 'solid'
fig, ax = plt.subplots()
CS = ax.contour(X, Y, Z, 6, colors='k')  # Negative contours default to dashed.
ax.clabel(CS, fontsize=9, inline=True)
ax.set_title('Single color - negative contours solid')
```

### With this PR
```
fig2, ax2 = plt.subplots()
CS = ax2.contour(X, Y, Z, 6, colors='k', negative_linestyles='solid')
ax2.clabel(CS, fontsize=9, inline=True)
ax2.set_title('Single color - negative contours solid')
```

## PR Checklist

<!-- Please mark any checkboxes that do not apply to this PR as [N/A]. -->
**Tests and Styling**
- [x] Has pytest style unit tests (and `pytest` passes).
- [x] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (install `flake8-docstrings` and run `flake8 --docstring-convention=all`).

**Documentation**
- [ ] New features are documented, with examples if plot related.
- [ ] New features have an entry in `doc/users/next_whats_new/` (follow instructions in README.rst there).
- [ ] API changes documented in `doc/api/next_api_changes/` (follow instructions in README.rst there).
- [ ] Documentation is sphinx and numpydoc compliant (the docs should [build](https://matplotlib.org/devel/documenting_mpl.html#building-the-docs) without error).

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at
  https://matplotlib.org/devel/gitwash/development_workflow.html.

- Do not create the PR out of main, but out of a separate branch.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  http://matplotlib.org/devel/documenting_mpl.html#formatting.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[ENH]: contour kwarg for negative_linestyle
### Problem

if you contour a negative quantity, it gets dashed lines.  Leaving aside whether this is a good default or not, the only way to toggle this is via `rcParams['contour.negative_linestyle']=False`.  


### Proposed solution


I think this should be togglable via kwarg, though I appreciate that overlaps with `linestyle` and only is activated with monochrome contours.  

(I actually think the default should be False, FWIW - this surprises me every time, and I make quite a few contour plots).

Hints from issue discussion:
Should the current `linestyles` kwarg be used to accomplish this or should a new kwarg be added? I have a simple solution adding a new kwarg (though it would need a little more work before it is ready).

The following code snippet and images will show a solution with an added kwarg, but I expect this is not exactly what you had in mind.


```
import numpy as np
import matplotlib.pyplot as plt

delta = 0.025
x = np.arange(-3.0, 3.0, delta)
y = np.arange(-2.0, 2.0, delta)
X, Y = np.meshgrid(x, y)
Z1 = np.exp(-X**2 - Y**2)
Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2)
Z = (Z1 - Z2) * 2

# Negative contour defaults to dashed
fig, ax = plt.subplots()
CS = ax.contour(X, Y, Z, 6, colors='k')
ax.clabel(CS, fontsize=9, inline=True)
ax.set_title('Single color - negative contours dashed (default)')

# Set negative contours to be solid instead of dashed (default)
# using negative_linestyle='solid'
fig2, ax2 = plt.subplots()
CS = ax2.contour(X, Y, Z, 6, colors='k', negative_linestyles='solid')
ax2.clabel(CS, fontsize=9, inline=True)
ax2.set_title('Single color - negative contours solid')
```

### Default
![image](https://user-images.githubusercontent.com/28690153/168208633-7e0ca845-e218-445c-a19a-850f279a15ec.png)

### `negative_linestyles='solid'`
![image](https://user-images.githubusercontent.com/28690153/168208620-88c3a5eb-7bb4-4f6a-8890-b23c76a30928.png)
> Should the current `linestyles` kwarg be used to accomplish this or should a new kwarg be added?

How would we squeeze this into the current `linestyles` parameter? The only thing I could imagine is something like `linestyles={'positive': 'solid', 'negative': 'dotted'}`, which I find a bit cumbersome. I suppose an extra kwarg is simpler.
> How would we squeeze this into the current linestyles parameter?

I had no good solution for this either, but thought I would ask just in case.

The next question is: 
1. should this be a boolean (the rcParams method seems to be)
2. or should the kwarg accept a separate entry for negative linestyles?

I think the latter makes a lot more sense, but I don't work with contour plots regularly.
I think since the rcParam is separate, its fine to keep the (potential) kwarg separate.  BTW I'm not wedded to this idea, but it does seem strange to have a feature only accessible by rcParam.  

As for bool vs string, I'd say both?   right now the rcParam defaults to "dashed" - I think that's fine for a kwarg as well, but being able to say *False* to skip the behaviour would be nice as well.  
> BTW I'm not wedded to this idea

I can mock-up a basic solution. Maybe we can get opinions from other contributors and maintainers in the meantime.

I think this would add value because it would make customization of negative contours more straightforward. Since this doesn't require removing any current functionality, I don't see any cons to adding the kwarg.

Commit message: Added negative_linestyles to contour

Patch to review:
diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py
index d666a906812d..e8e6f88fd9ae 100644
--- a/lib/matplotlib/contour.py
+++ b/lib/matplotlib/contour.py
@@ -701,7 +701,7 @@ def __init__(self, ax, *args,
                  hatches=(None,), alpha=None, origin=None, extent=None,
                  cmap=None, colors=None, norm=None, vmin=None, vmax=None,
                  extend='neither', antialiased=None, nchunk=0, locator=None,
-                 transform=None,
+                 transform=None, negative_linestyles=None,
                  **kwargs):
         """
         Draw contour lines or filled regions, depending on
@@ -786,6 +786,13 @@ def __init__(self, ax, *args,
 
         self._transform = transform
 
+        self.negative_linestyles = negative_linestyles
+        # If negative_linestyles was not defined as a kwarg,
+        # define negative_linestyles with rcParams
+        if self.negative_linestyles is None:
+            self.negative_linestyles = \
+                mpl.rcParams['contour.negative_linestyle']
+
         kwargs = self._process_args(*args, **kwargs)
         self._process_levels()
 
@@ -1276,11 +1283,10 @@ def _process_linestyles(self):
         if linestyles is None:
             tlinestyles = ['solid'] * Nlev
             if self.monochrome:
-                neg_ls = mpl.rcParams['contour.negative_linestyle']
                 eps = - (self.zmax - self.zmin) * 1e-15
                 for i, lev in enumerate(self.levels):
                     if lev < eps:
-                        tlinestyles[i] = neg_ls
+                        tlinestyles[i] = self.negative_linestyles
         else:
             if isinstance(linestyles, str):
                 tlinestyles = [linestyles] * Nlev
@@ -1751,6 +1757,18 @@ def _initialize_x_y(self, z):
     iterable is shorter than the number of contour levels
     it will be repeated as necessary.
 
+negative_linestyles : None or str, optional
+                      {'solid', 'dashed', 'dashdot', 'dotted'}
+    *Only applies to* `.contour`.
+
+    If *negative_linestyles* is None, the default is 'dashed' for
+    negative contours.
+
+    *negative_linestyles* can also be an iterable of the above
+    strings specifying a set of linestyles to be used. If this
+    iterable is shorter than the number of contour levels
+    it will be repeated as necessary.
+
 hatches : list[str], optional
     *Only applies to* `.contourf`.
 


Code review comments:
diff_hunk: @@ -1751,6 +1757,18 @@ def _initialize_x_y(self, z):
     iterable is shorter than the number of contour levels
     it will be repeated as necessary.
 
+negative_linestyles : None or str, optional
+                      {'solid', 'dashed', 'dashdot', 'dotted'}
path: lib/matplotlib/contour.py
text: @user1:
```suggestion
negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \
optional
```
(or where ever the line should break.)