I am looking for a one-call way to plot multiple stacked bars in one chart by giving it a 2D-Matrix for the y-axis.
This feature request is similar to #11048, where the bars are placed next to each other.
The documentation for matplotlib.pyplot.bar just allows scalar or array-like inputs.
The data used contains 2 x 8760 timesteps (hours). ATM I work with stackplot to generate the following graphs. Just the third graph is relevant to this issue.
My workaround would be to use stackplot for a lineplot:
`ax1.stackplot(x,y, labels=labels)
and duplicate the data in 'y' - shift the duplication just a second before the next timestep (hour) and merge those sets.
Expected outcome
I found a function that fits my problem but is not scalable for bigger arrays. (creation time is long and editing the view takes forever) But the bars in this case have a gap between the bars. I am looking for a way to adjust the width, so the gaps can be closed.
I think the value and x-axis labels are responsible for the long calculation, but I am not sure.
def stacked_bar(index, np_array, width=1, labels=None):
"""Plots a stacked bar chart with the data and labels provided.
Keyword arguments:
index -- 1-dimensional numpy array or nested list
containing data x-axis index
np_array -- 2-dimensional numpy array or nested list
containing data for each series in rows
labels -- (optional) list of series labels (these appear in
the legend)
width -- (optional) set width of bars
"""
#ToDo sortierung nach änderungshäufigkeit, damit chp unten ist
np_array = np.array(np_array)
n_rows = np.size(np_array, 1)
bottom = np.zeros(n_rows)
plt_definitions = []
index = np.arange(n_rows)
for i, row_data in enumerate(np_array): #in range(n_columns):
plt_definitions.append(plt.bar(index, np_array[i], width, bottom=bottom))
bottom += np_array[i]
if labels:
plt.legend(plt_definitions, labels)
labels.append("test" + str(i))
if not labels:
labels = []
for i, row_data in enumerate(np_array):
labels.append("test" + str(i))
plt.legend(plt_definitions, labels)
plt.show()
return
`
With this example:
`
labels = ['Cat A', 'Cat B', 'Cat C']
x = [1,2,3,4,5]
y = [[1,1,1,1,1],[1,2,3,1,1],[1,3,1,2,2]]
`
The result is:
If the data contains yearly data for 4 categorys the result is:
But the processing takes very long in my opinion.
Any ideas how to speed it up a little? Or is there a particular reason why bar charts take longer than filled lines graphs like in the example picture of the first post?
The reason that the bar version is much slower to render is that each bar is drawn as its own rectangle (so you have 4*(len(data)) of them) where as with the Line2D version it only has 4 artists to draw.
I went with the fill_between idea, which is indeed pretty fast in comparision. Thank you for that!
Here is my example code and the new function:
`
import numpy as np
import matplotlib.pyplot as plt
labels = ['Cat A', 'Cat B', 'Cat C']
x = [1,2,3,4,5]
y = [[1,1,1,2,5],[1,2,3,1,1],[1,3,1,2,2]]
def stacked_bar2(index,np_array, labels=None):
data = np.array(np_array)
plt_definitions = []
n_rows = np.size(np_array, 1)
bottom = np.zeros(n_rows)
top = np_array[0]
for i, row_data in enumerate(np_array): # in range(n_columns):
top = np.sum([bottom, np_array[i]], axis=0)
plt_definitions.append(plt.fill_between(index, top, y2=bottom,step='mid'))
bottom += np_array[i]
if not labels:
labels = []
for i, row_data in enumerate(np_array):
labels.append("test" + str(i))
plt.xlim(left=min(index)-1,right=max(index)+1)
plt.legend(plt_definitions, labels)
plt.show()
return
stacked_bar2(x,y,labels)
`
The result for the example looks like this:
For actual work it looks like this:
The first and last bar are just shown for half of the width in comparision to the rest:
step : {'pre', 'post', 'mid'}, optional - element
Even if I enlarge the x-axis, the first and last bars are still smaller. Any ideas on that?
For an additional element, you could step by the difference between the last two elements, i.e. add a[-1] + (a[-1] - a[-2]) or in short 2*a[-1] - a[-2].
Now method works with integer index and datetime index for numpy and as well for pandas.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
labels = ['Cat A', 'Cat B', 'Cat C']
integer = [1,2,3,4,5]
y = np.array([[1,1,1,1,1],[1,2,1,2,1],[1,2,3,4,5]])
np_datetime = np.arange('2005-01', '2005-06', dtype='datetime64[M]')
pd_datetime = pd.date_range('2018-01-01', periods=5, freq='H')
def add_last_index(x_axis_label, number_of_rows):
"""Method
This method adds another x axis label to the array. If the input is a datetime, it adds the correct next datetime.
If it's a integer, it adds one.
:param x_axis_label:
:param number_of_rows:
:return:
"""
if isinstance(x_axis_label[0],np.datetime64):
print("This is a numpy datetime")
# Calculate time granularity of the x axis ...
timedelta = np.datetime64(x_axis_label[1]) - np.datetime64(x_axis_label[0])
# ... and add one more label to the labels so that the last one is not cut out (because of the "post" step)
x_axis_label = np.append(x_axis_label, x_axis_label[len(x_axis_label) - 1] + timedelta)
elif isinstance(x_axis_label[0],pd.datetime):
print("This is a pandas datetime")
x_axis_label = pd.to_datetime(x_axis_label)
# Calculate time granularity of the x axis ...
timedelta = x_axis_label[len(x_axis_label)-1] - x_axis_label[0]
# ... and add one more label to the labels so that the last one is not cut out (because of the "post" step)
x_axis_label = x_axis_label.append(x_axis_label[1:2]+timedelta)
else:
# Add one more label to the labels so that the last one is not cut out (because of the "post" step)
x_axis_label = np.append(x_axis_label, number_of_rows + 1)
return x_axis_label
def stacked_bar(x_axis_label, np_array, labels=None):
np_array = np.array(np_array)
plt_definitions = []
number_of_rows = np.size(np_array, 1)
n_columns = np.size(np_array, 0)
np_array = np.append(np_array, np_array[0:n_columns, number_of_rows - 1:number_of_rows], axis=1)
# Append additional index at the end
x_axis_label = add_last_index(x_axis_label, number_of_rows)
bottom = np.zeros(number_of_rows+1)
top = np_array[0]
for i, row_data in enumerate(np_array): # in range(n_columns):
top = np.sum([bottom, np_array[i]], axis=0)
plt_definitions.append(plt.fill_between(x_axis_label, top, y2=bottom, step='post'))
bottom += np_array[i]
if not labels:
labels = []
for i, row_data in enumerate(np_array):
labels.append("test" + str(i))
plt.xlim(left=min(x_axis_label), right=max(x_axis_label))
plt.legend(plt_definitions, labels)
return
stacked_bar(pd_datetime,y, labels=labels)
plt.show()
This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!
Activity
timhoffm commented on Apr 29, 2019
Do you want something like https://matplotlib.org/gallery/lines_bars_and_markers/bar_stacked.html? If so, you can modify that example into a function that you can call on your 2D data.
busiing commented on May 2, 2019
I created a function:
`
def stacked_bar(index, np_array, width=1, labels=None): """Plots a stacked bar chart with the data and labels provided. Keyword arguments: index -- 1-dimensional numpy array or nested list containing data x-axis index np_array -- 2-dimensional numpy array or nested list containing data for each series in rows labels -- (optional) list of series labels (these appear in the legend) width -- (optional) set width of bars """ #ToDo sortierung nach änderungshäufigkeit, damit chp unten ist np_array = np.array(np_array) n_rows = np.size(np_array, 1) bottom = np.zeros(n_rows) plt_definitions = [] index = np.arange(n_rows) for i, row_data in enumerate(np_array): #in range(n_columns): plt_definitions.append(plt.bar(index, np_array[i], width, bottom=bottom)) bottom += np_array[i] if labels: plt.legend(plt_definitions, labels) labels.append("test" + str(i)) if not labels: labels = [] for i, row_data in enumerate(np_array): labels.append("test" + str(i)) plt.legend(plt_definitions, labels) plt.show() return`
With this example:
`
`
The result is:

If the data contains yearly data for 4 categorys the result is:

But the processing takes very long in my opinion.
Any ideas how to speed it up a little? Or is there a particular reason why bar charts take longer than filled lines graphs like in the example picture of the first post?
tacaswell commented on May 2, 2019
The reason that the bar version is much slower to render is that each bar is drawn as its own rectangle (so you have 4*(len(data)) of them) where as with the Line2D version it only has 4 artists to draw.
I suggest taking a look at fill_between with the step kwarg https://matplotlib.org/api/_as_gen/matplotlib.pyplot.fill_between.html .
busiing commented on May 3, 2019
I went with the fill_between idea, which is indeed pretty fast in comparision. Thank you for that!
Here is my example code and the new function:
`
import numpy as np import matplotlib.pyplot as plt labels = ['Cat A', 'Cat B', 'Cat C'] x = [1,2,3,4,5] y = [[1,1,1,2,5],[1,2,3,1,1],[1,3,1,2,2]] def stacked_bar2(index,np_array, labels=None): data = np.array(np_array) plt_definitions = [] n_rows = np.size(np_array, 1) bottom = np.zeros(n_rows) top = np_array[0] for i, row_data in enumerate(np_array): # in range(n_columns): top = np.sum([bottom, np_array[i]], axis=0) plt_definitions.append(plt.fill_between(index, top, y2=bottom,step='mid')) bottom += np_array[i] if not labels: labels = [] for i, row_data in enumerate(np_array): labels.append("test" + str(i)) plt.xlim(left=min(index)-1,right=max(index)+1) plt.legend(plt_definitions, labels) plt.show() return stacked_bar2(x,y,labels)`
The result for the example looks like this:

For actual work it looks like this:

Even if I enlarge the x-axis, the first and last bars are still smaller. Any ideas on that?
timhoffm commented on May 3, 2019
Set step='post' and add the last element again to your data.
See https://matplotlib.org/devdocs/gallery/lines_bars_and_markers/step_demo.html
busiing commented on May 6, 2019
Ty @timhoffm
I adjusted the code so it adds the last value of each row at the end, added one index value for index, bottom and top:
`
def stacked_bar(index,np_array, labels=None): np_array = np.array(np_array) plt_definitions = [] n_rows = np.size(np_array, 1) n_columns = np.size(np_array, 0) np_array = np.append(np_array, np_array[0:n_columns, n_rows - 1:n_rows], axis=1) index = np.append(index,n_rows+1) bottom = np.zeros(n_rows+1) top = np_array[0] print(top) for i, row_data in enumerate(np_array): # in range(n_columns): top = np.sum([bottom, np_array[i]], axis=0) plt_definitions.append(plt.fill_between(index, top, y2=bottom,step='post')) bottom += np_array[i] if not labels: labels = [] for i, row_data in enumerate(np_array): labels.append("test" + str(i)) plt.xlim(left=min(index) - 1, right=max(index) + 1) plt.legend(plt_definitions, labels) return`
Works out pretty good.
But I have a problem, if index is a time series.
index = np.append(index,n_rows+1)
I didn't figure out yet, how to add one element to a time series that fits its (unknown) resolution.
timhoffm commented on May 6, 2019
For an additional element, you could step by the difference between the last two elements, i.e. add a[-1] + (a[-1] - a[-2]) or in short 2*a[-1] - a[-2].
busiing commented on May 7, 2019
Figured it out.
Now method works with integer index and datetime index for numpy and as well for pandas.
import numpy as np import matplotlib.pyplot as plt import pandas as pd labels = ['Cat A', 'Cat B', 'Cat C'] integer = [1,2,3,4,5] y = np.array([[1,1,1,1,1],[1,2,1,2,1],[1,2,3,4,5]]) np_datetime = np.arange('2005-01', '2005-06', dtype='datetime64[M]') pd_datetime = pd.date_range('2018-01-01', periods=5, freq='H') def add_last_index(x_axis_label, number_of_rows): """Method This method adds another x axis label to the array. If the input is a datetime, it adds the correct next datetime. If it's a integer, it adds one. :param x_axis_label: :param number_of_rows: :return: """ if isinstance(x_axis_label[0],np.datetime64): print("This is a numpy datetime") # Calculate time granularity of the x axis ... timedelta = np.datetime64(x_axis_label[1]) - np.datetime64(x_axis_label[0]) # ... and add one more label to the labels so that the last one is not cut out (because of the "post" step) x_axis_label = np.append(x_axis_label, x_axis_label[len(x_axis_label) - 1] + timedelta) elif isinstance(x_axis_label[0],pd.datetime): print("This is a pandas datetime") x_axis_label = pd.to_datetime(x_axis_label) # Calculate time granularity of the x axis ... timedelta = x_axis_label[len(x_axis_label)-1] - x_axis_label[0] # ... and add one more label to the labels so that the last one is not cut out (because of the "post" step) x_axis_label = x_axis_label.append(x_axis_label[1:2]+timedelta) else: # Add one more label to the labels so that the last one is not cut out (because of the "post" step) x_axis_label = np.append(x_axis_label, number_of_rows + 1) return x_axis_label def stacked_bar(x_axis_label, np_array, labels=None): np_array = np.array(np_array) plt_definitions = [] number_of_rows = np.size(np_array, 1) n_columns = np.size(np_array, 0) np_array = np.append(np_array, np_array[0:n_columns, number_of_rows - 1:number_of_rows], axis=1) # Append additional index at the end x_axis_label = add_last_index(x_axis_label, number_of_rows) bottom = np.zeros(number_of_rows+1) top = np_array[0] for i, row_data in enumerate(np_array): # in range(n_columns): top = np.sum([bottom, np_array[i]], axis=0) plt_definitions.append(plt.fill_between(x_axis_label, top, y2=bottom, step='post')) bottom += np_array[i] if not labels: labels = [] for i, row_data in enumerate(np_array): labels.append("test" + str(i)) plt.xlim(left=min(x_axis_label), right=max(x_axis_label)) plt.legend(plt_definitions, labels) return stacked_bar(pd_datetime,y, labels=labels) plt.show()Worked out for the "small" example:

As well for the "big" example:

github-actions commented on Jun 14, 2023
This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!
timhoffm commented on Jun 14, 2023
This is followed up on in #24313.