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

[ENH]: Multi hatching in ax.stackplot() #27146

Closed
pawjast opened this issue Oct 19, 2023 · 6 comments · Fixed by #27158
Closed

[ENH]: Multi hatching in ax.stackplot() #27146

pawjast opened this issue Oct 19, 2023 · 6 comments · Fixed by #27158
Labels
Good first issue Open a pull request against these issues if there are no active ones! New feature
Milestone

Comments

@pawjast
Copy link

pawjast commented Oct 19, 2023

Problem

ax.stackplot() is used for conveniently plotting stacked area plot.

The documentation says that any **kwargs is passed on to ax.fill_between.

So, it all works fine if I work with colours, but not so much with hatching pattern.

If I want to add hatching, this is what I get by default:

import matplotlib.pyplot as plt
import numpy as np

# Data
cols = 10
rows = 4

data = (
    np.reshape(np.arange(0, cols, 1), (1, -1)) ** 2
    + np.reshape(np.arange(0, rows), (-1, 1))
    + np.random.random((rows, cols))*5
)

# Plot
fig, ax = plt.subplots(
    figsize=(10,6),
    facecolor="white",
    layout="constrained"
)
fig.suptitle(
    "with hatching",
    fontsize=25,
    weight="bold"
)

x = range(data.shape[1])
ax.stackplot(
    x, data,
    hatch="x"
)

ax.set_xlim(0, 9)
ax.set_ylim(0, 350)

The outcome:
image

But I would be more interested in getting something like this:
image

Proposed solution

The calling signature would be the same as before with exception that the hatch parameter would accept e.g. a list with hatching styles you want to use.

Example:

ax.stackplot(
    x, data,
    hatch=["x", "\\", ".", "-"]
)

The example I've used above in the problem was create using the code below and in this case it varies the hatching by the density:

fig, ax = plt.subplots(
    figsize=(10,6),
    facecolor="white",
    layout="constrained"
)
fig.suptitle(
    "with individual hatching",
    fontsize=25,
    weight="bold"
)

x = range(data.shape[1])
stack_baseline = np.zeros((data.shape[1]))
for i, y in enumerate(data):
    ax.fill_between(
        x, stack_baseline, y+stack_baseline,
        facecolor=(1, 1, 1, 0),
        hatch="x"*(data.shape[0]-i)
    )
    stack_baseline += y

ax.set_xlim(0, 9)
ax.set_ylim(0, 350)
@KartikeyBartwal
Copy link

Hello!
Don't you think 'with hatching' looks cleaner and makes it easier to extract insights? It's never a bad idea to have another style of representation at our disposal, but what is your opinion on this?

@rcomer
Copy link
Member

rcomer commented Oct 20, 2023

To me this request seems analogous to the hatch parameter added to pie at v3.7.
https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.7.0.html#hatch-parameter-for-pie

@timhoffm timhoffm added the Good first issue Open a pull request against these issues if there are no active ones! label Oct 20, 2023
@github-actions
Copy link

Good first issue - notes for new contributors

This issue is suited to new contributors because it does not require understanding of the Matplotlib internals. To get started, please see our contributing guide.

We do not assign issues. Check the Development section in the sidebar for linked pull requests (PRs). If there are none, feel free to start working on it. If there is an open PR, please collaborate on the work by reviewing it rather than duplicating it in a competing PR.

If something is unclear, please reach out on any of our communication channels.

@timhoffm
Copy link
Member

This is a reaonable request.

It's a good first issue because the change is local in

def stackplot(axes, x, *args,
labels=(), colors=None, baseline='zero',
**kwargs):
"""
Draw a stacked area plot.
Parameters
----------
x : (N,) array-like
y : (M, N) array-like
The data is assumed to be unstacked. Each of the following
calls is legal::
stackplot(x, y) # where y has shape (M, N)
stackplot(x, y1, y2, y3) # where y1, y2, y3, y4 have length N
baseline : {'zero', 'sym', 'wiggle', 'weighted_wiggle'}
Method used to calculate the baseline:
- ``'zero'``: Constant zero baseline, i.e. a simple stacked plot.
- ``'sym'``: Symmetric around zero and is sometimes called
'ThemeRiver'.
- ``'wiggle'``: Minimizes the sum of the squared slopes.
- ``'weighted_wiggle'``: Does the same but weights to account for
size of each layer. It is also called 'Streamgraph'-layout. More
details can be found at http://leebyron.com/streamgraph/.
labels : list of str, optional
A sequence of labels to assign to each data series. If unspecified,
then no labels will be applied to artists.
colors : list of color, optional
A sequence of colors to be cycled through and used to color the stacked
areas. The sequence need not be exactly the same length as the number
of provided *y*, in which case the colors will repeat from the
beginning.
If not specified, the colors from the Axes property cycle will be used.
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER
**kwargs
All other keyword arguments are passed to `.Axes.fill_between`.
Returns
-------
list of `.PolyCollection`
A list of `.PolyCollection` instances, one for each element in the
stacked area plot.
"""
y = np.vstack(args)
labels = iter(labels)
if colors is not None:
colors = itertools.cycle(colors)
else:
colors = (axes._get_lines.get_next_color() for _ in y)
# Assume data passed has not been 'stacked', so stack it here.
# We'll need a float buffer for the upcoming calculations.
stack = np.cumsum(y, axis=0, dtype=np.promote_types(y.dtype, np.float32))
_api.check_in_list(['zero', 'sym', 'wiggle', 'weighted_wiggle'],
baseline=baseline)
if baseline == 'zero':
first_line = 0.
elif baseline == 'sym':
first_line = -np.sum(y, 0) * 0.5
stack += first_line[None, :]
elif baseline == 'wiggle':
m = y.shape[0]
first_line = (y * (m - 0.5 - np.arange(m)[:, None])).sum(0)
first_line /= -m
stack += first_line
elif baseline == 'weighted_wiggle':
total = np.sum(y, 0)
# multiply by 1/total (or zero) to avoid infinities in the division:
inv_total = np.zeros_like(total)
mask = total > 0
inv_total[mask] = 1.0 / total[mask]
increase = np.hstack((y[:, 0:1], np.diff(y)))
below_size = total - stack
below_size += 0.5 * y
move_up = below_size * inv_total
move_up[:, 0] = 0.5
center = (move_up - 0.5) * increase
center = np.cumsum(center.sum(0))
first_line = center - 0.5 * total
stack += first_line
# Color between x = 0 and the first array.
coll = axes.fill_between(x, first_line, stack[0, :],
facecolor=next(colors), label=next(labels, None),
**kwargs)
coll.sticky_edges.y[:] = [0]
r = [coll]
# Color between array i-1 and array i
for i in range(len(y) - 1):
r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :],
facecolor=next(colors),
label=next(labels, None),
**kwargs))
return r

and you can simply handle hatching analogous to the existing color handling.

@pawjast
Copy link
Author

pawjast commented Oct 20, 2023

Hello! Don't you think 'with hatching' looks cleaner and makes it easier to extract insights? It's never a bad idea to have another style of representation at our disposal, but what is your opinion on this?

To me, uniform hatching covering the whole data area is purely decorative and you still need colours to understand the data. As oppose to when you have individual hatching styles. Then you can go only with the hatching fill and remove the colours. Scientific papers are often required to be made in a way so you can print them in black and white. And they often use hatching for this purpose to distinguish between different areas.

@nbarlowATI
Copy link
Contributor

Hi! I agree on the usefulness of having different hatching styles in a stack plot, and had a go at implementing this following the guidance above (many thanks for making this so clear and newbie-friendly BTW!) in the draft PR: #27158

@QuLogic QuLogic added this to the v3.9.0 milestone Oct 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Good first issue Open a pull request against these issues if there are no active ones! New feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants