stackplot.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """
  2. Stacked area plot for 1D arrays inspired by Douglas Y'barbo's stackoverflow
  3. answer:
  4. https://stackoverflow.com/q/2225995/
  5. (https://stackoverflow.com/users/66549/doug)
  6. """
  7. import itertools
  8. import numpy as np
  9. from matplotlib import _api
  10. __all__ = ['stackplot']
  11. def stackplot(axes, x, *args,
  12. labels=(), colors=None, hatch=None, baseline='zero',
  13. **kwargs):
  14. """
  15. Draw a stacked area plot or a streamgraph.
  16. Parameters
  17. ----------
  18. x : (N,) array-like
  19. y : (M, N) array-like
  20. The data can be either stacked or unstacked. Each of the following
  21. calls is legal::
  22. stackplot(x, y) # where y has shape (M, N) e.g. y = [y1, y2, y3, y4]
  23. stackplot(x, y1, y2, y3, y4) # where y1, y2, y3, y4 have length N
  24. baseline : {'zero', 'sym', 'wiggle', 'weighted_wiggle'}
  25. Method used to calculate the baseline:
  26. - ``'zero'``: Constant zero baseline, i.e. a simple stacked plot.
  27. - ``'sym'``: Symmetric around zero and is sometimes called
  28. 'ThemeRiver'.
  29. - ``'wiggle'``: Minimizes the sum of the squared slopes.
  30. - ``'weighted_wiggle'``: Does the same but weights to account for
  31. size of each layer. It is also called 'Streamgraph'-layout. More
  32. details can be found at http://leebyron.com/streamgraph/.
  33. labels : list of str, optional
  34. A sequence of labels to assign to each data series. If unspecified,
  35. then no labels will be applied to artists.
  36. colors : list of :mpltype:`color`, optional
  37. A sequence of colors to be cycled through and used to color the stacked
  38. areas. The sequence need not be exactly the same length as the number
  39. of provided *y*, in which case the colors will repeat from the
  40. beginning.
  41. If not specified, the colors from the Axes property cycle will be used.
  42. hatch : list of str, default: None
  43. A sequence of hatching styles. See
  44. :doc:`/gallery/shapes_and_collections/hatch_style_reference`.
  45. The sequence will be cycled through for filling the
  46. stacked areas from bottom to top.
  47. It need not be exactly the same length as the number
  48. of provided *y*, in which case the styles will repeat from the
  49. beginning.
  50. .. versionadded:: 3.9
  51. Support for list input
  52. data : indexable object, optional
  53. DATA_PARAMETER_PLACEHOLDER
  54. **kwargs
  55. All other keyword arguments are passed to `.Axes.fill_between`.
  56. Returns
  57. -------
  58. list of `.PolyCollection`
  59. A list of `.PolyCollection` instances, one for each element in the
  60. stacked area plot.
  61. """
  62. y = np.vstack(args)
  63. labels = iter(labels)
  64. if colors is not None:
  65. colors = itertools.cycle(colors)
  66. else:
  67. colors = (axes._get_lines.get_next_color() for _ in y)
  68. if hatch is None or isinstance(hatch, str):
  69. hatch = itertools.cycle([hatch])
  70. else:
  71. hatch = itertools.cycle(hatch)
  72. # Assume data passed has not been 'stacked', so stack it here.
  73. # We'll need a float buffer for the upcoming calculations.
  74. stack = np.cumsum(y, axis=0, dtype=np.promote_types(y.dtype, np.float32))
  75. _api.check_in_list(['zero', 'sym', 'wiggle', 'weighted_wiggle'],
  76. baseline=baseline)
  77. if baseline == 'zero':
  78. first_line = 0.
  79. elif baseline == 'sym':
  80. first_line = -np.sum(y, 0) * 0.5
  81. stack += first_line[None, :]
  82. elif baseline == 'wiggle':
  83. m = y.shape[0]
  84. first_line = (y * (m - 0.5 - np.arange(m)[:, None])).sum(0)
  85. first_line /= -m
  86. stack += first_line
  87. elif baseline == 'weighted_wiggle':
  88. total = np.sum(y, 0)
  89. # multiply by 1/total (or zero) to avoid infinities in the division:
  90. inv_total = np.zeros_like(total)
  91. mask = total > 0
  92. inv_total[mask] = 1.0 / total[mask]
  93. increase = np.hstack((y[:, 0:1], np.diff(y)))
  94. below_size = total - stack
  95. below_size += 0.5 * y
  96. move_up = below_size * inv_total
  97. move_up[:, 0] = 0.5
  98. center = (move_up - 0.5) * increase
  99. center = np.cumsum(center.sum(0))
  100. first_line = center - 0.5 * total
  101. stack += first_line
  102. # Color between x = 0 and the first array.
  103. coll = axes.fill_between(x, first_line, stack[0, :],
  104. facecolor=next(colors),
  105. hatch=next(hatch),
  106. label=next(labels, None),
  107. **kwargs)
  108. coll.sticky_edges.y[:] = [0]
  109. r = [coll]
  110. # Color between array i-1 and array i
  111. for i in range(len(y) - 1):
  112. r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :],
  113. facecolor=next(colors),
  114. hatch=next(hatch),
  115. label=next(labels, None),
  116. **kwargs))
  117. return r