antelope-reports 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. antelope_reports-0.2.0/PKG-INFO +38 -0
  2. antelope_reports-0.2.0/README.md +14 -0
  3. antelope_reports-0.2.0/antelope_reports/__init__.py +27 -0
  4. antelope_reports-0.2.0/antelope_reports/charts/__init__.py +5 -0
  5. antelope_reports-0.2.0/antelope_reports/charts/base.py +210 -0
  6. antelope_reports-0.2.0/antelope_reports/charts/combi.py +379 -0
  7. antelope_reports-0.2.0/antelope_reports/charts/model_graph.py +142 -0
  8. antelope_reports-0.2.0/antelope_reports/charts/pos_neg.py +496 -0
  9. antelope_reports-0.2.0/antelope_reports/charts/simple.py +6 -0
  10. antelope_reports-0.2.0/antelope_reports/charts/vertical.py +225 -0
  11. antelope_reports-0.2.0/antelope_reports/charts/waterfall.py +883 -0
  12. antelope_reports-0.2.0/antelope_reports/fg_builder/__init__.py +26 -0
  13. antelope_reports-0.2.0/antelope_reports/fg_builder/ecoinvent_grids.py +228 -0
  14. antelope_reports-0.2.0/antelope_reports/fg_builder/lca_study.py +538 -0
  15. antelope_reports-0.2.0/antelope_reports/fg_builder/model_updater.py +321 -0
  16. antelope_reports-0.2.0/antelope_reports/fg_builder/param_manager.py +158 -0
  17. antelope_reports-0.2.0/antelope_reports/fg_builder/scenario_updater.py +129 -0
  18. antelope_reports-0.2.0/antelope_reports/fg_builder/stage_manager.py +187 -0
  19. antelope_reports-0.2.0/antelope_reports/kpi/__init__.py +2 -0
  20. antelope_reports-0.2.0/antelope_reports/kpi/equivalency.py +113 -0
  21. antelope_reports-0.2.0/antelope_reports/kpi/kpi.py +520 -0
  22. antelope_reports-0.2.0/antelope_reports/lca_test/__init__.py +281 -0
  23. antelope_reports-0.2.0/antelope_reports/lcia/__init__.py +42 -0
  24. antelope_reports-0.2.0/antelope_reports/lcia/ecoinvent.py +35 -0
  25. antelope_reports-0.2.0/antelope_reports/lcia/flow_comparator.py +104 -0
  26. antelope_reports-0.2.0/antelope_reports/lcia/lcia_eval.py +165 -0
  27. antelope_reports-0.2.0/antelope_reports/lcia/qdb_client/__init__.py +5 -0
  28. antelope_reports-0.2.0/antelope_reports/lcia/qdb_client/qdb_client.py +82 -0
  29. antelope_reports-0.2.0/antelope_reports/lcia/screening.py +105 -0
  30. antelope_reports-0.2.0/antelope_reports/lcia/traci.py +107 -0
  31. antelope_reports-0.2.0/antelope_reports/lcia/worldsteel.py +145 -0
  32. antelope_reports-0.2.0/antelope_reports/lcia/xlsx_lcia.py +222 -0
  33. antelope_reports-0.2.0/antelope_reports/mfa_models/__init__.py +5 -0
  34. antelope_reports-0.2.0/antelope_reports/mfa_models/conventions.py +12 -0
  35. antelope_reports-0.2.0/antelope_reports/mfa_models/interface.py +31 -0
  36. antelope_reports-0.2.0/antelope_reports/mfa_models/lca_model.py +154 -0
  37. antelope_reports-0.2.0/antelope_reports/mfa_models/lca_study.py +124 -0
  38. antelope_reports-0.2.0/antelope_reports/mfa_models/markets.py +141 -0
  39. antelope_reports-0.2.0/antelope_reports/mfa_study/__init__.py +16 -0
  40. antelope_reports-0.2.0/antelope_reports/mfa_study/dynamic_unit_study.py +228 -0
  41. antelope_reports-0.2.0/antelope_reports/mfa_study/lc_mfa_study.py +163 -0
  42. antelope_reports-0.2.0/antelope_reports/mfa_study/observed_mfa_study.py +94 -0
  43. antelope_reports-0.2.0/antelope_reports/model_runner/__init__.py +17 -0
  44. antelope_reports-0.2.0/antelope_reports/model_runner/components_mixin.py +38 -0
  45. antelope_reports-0.2.0/antelope_reports/model_runner/lca_model_runner.py +439 -0
  46. antelope_reports-0.2.0/antelope_reports/model_runner/multi_model_runner.py +43 -0
  47. antelope_reports-0.2.0/antelope_reports/model_runner/quick_model_runner.py +87 -0
  48. antelope_reports-0.2.0/antelope_reports/model_runner/results_writer.py +139 -0
  49. antelope_reports-0.2.0/antelope_reports/model_runner/scenario_runner.py +119 -0
  50. antelope_reports-0.2.0/antelope_reports/model_runner/sens_runner.py +186 -0
  51. antelope_reports-0.2.0/antelope_reports/model_runner/simple_model_builder.py +25 -0
  52. antelope_reports-0.2.0/antelope_reports/observer/__init__.py +3 -0
  53. antelope_reports-0.2.0/antelope_reports/observer/exchanges_from_spreadsheet.py +173 -0
  54. antelope_reports-0.2.0/antelope_reports/observer/float_conv.py +33 -0
  55. antelope_reports-0.2.0/antelope_reports/observer/headers.py +40 -0
  56. antelope_reports-0.2.0/antelope_reports/observer/model_maker.py +775 -0
  57. antelope_reports-0.2.0/antelope_reports/observer/node_updater.py +93 -0
  58. antelope_reports-0.2.0/antelope_reports/observer/observations_from_spreadsheet.py +165 -0
  59. antelope_reports-0.2.0/antelope_reports/observer/quick_and_easy.py +436 -0
  60. antelope_reports-0.2.0/antelope_reports/observer/upgrade_manager.py +329 -0
  61. antelope_reports-0.2.0/antelope_reports/report.py +823 -0
  62. antelope_reports-0.2.0/antelope_reports/tables/__init__.py +2 -0
  63. antelope_reports-0.2.0/antelope_reports/tables/fragment_view.py +66 -0
  64. antelope_reports-0.2.0/antelope_reports/tables/gsheet_writer.py +133 -0
  65. antelope_reports-0.2.0/antelope_reports/tables/inventory.py +3 -0
  66. antelope_reports-0.2.0/antelope_reports/tables/old/allocation.py +140 -0
  67. antelope_reports-0.2.0/antelope_reports/tables/old/base.py +321 -0
  68. antelope_reports-0.2.0/antelope_reports/tables/old/flowables.py +149 -0
  69. antelope_reports-0.2.0/antelope_reports.egg-info/PKG-INFO +38 -0
  70. antelope_reports-0.2.0/antelope_reports.egg-info/SOURCES.txt +74 -0
  71. antelope_reports-0.2.0/antelope_reports.egg-info/dependency_links.txt +1 -0
  72. antelope_reports-0.2.0/antelope_reports.egg-info/requires.txt +6 -0
  73. antelope_reports-0.2.0/antelope_reports.egg-info/top_level.txt +1 -0
  74. antelope_reports-0.2.0/pyproject.toml +33 -0
  75. antelope_reports-0.2.0/setup.cfg +4 -0
  76. antelope_reports-0.2.0/setup.py +12 -0
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.1
2
+ Name: antelope_reports
3
+ Version: 0.2.0
4
+ Summary: Tools for summarizing and visualizing LCA models in Antelope
5
+ Author-email: Brandon Kuczenski <brandon@scope3consulting.com>
6
+ License: BSD-3-Clause
7
+ Project-URL: Repository, https://github.com/AntelopeLCA/reports
8
+ Keywords: antelope,waterfall,bar chart,model runner,scenario,lca study
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Scientific/Engineering
16
+ Requires-Python: >=3.6
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: antelope-foreground>=0.3.3
19
+ Requires-Dist: antelope-core>0.3.3
20
+ Requires-Dist: synonym-dict>=0.2.4
21
+ Requires-Dist: pandas
22
+ Requires-Dist: matplotlib
23
+ Requires-Dist: pydot>=2.0.0
24
+
25
+ # reports
26
+ Models, charts, and tables generated from Antelope catalogs. Heavyweight with pandas, plots, etc.
27
+
28
+ This package is a collection of useful support tools that (a) are not required in core Antelope and (b) do
29
+ or may require heavyweight support packages that would not be desirable in a "slender" install.
30
+
31
+ The utilities are grouped into several categories, each described briefly below.
32
+
33
+ The general support level of this code is "late alpha". Most of these tools work, and many of them are
34
+ used in production. But they are not CI-ed and may not be well documented.
35
+
36
+ ## charts
37
+
38
+ Graphical visualizations of LCA results.
@@ -0,0 +1,14 @@
1
+ # reports
2
+ Models, charts, and tables generated from Antelope catalogs. Heavyweight with pandas, plots, etc.
3
+
4
+ This package is a collection of useful support tools that (a) are not required in core Antelope and (b) do
5
+ or may require heavyweight support packages that would not be desirable in a "slender" install.
6
+
7
+ The utilities are grouped into several categories, each described briefly below.
8
+
9
+ The general support level of this code is "late alpha". Most of these tools work, and many of them are
10
+ used in production. But they are not CI-ed and may not be well documented.
11
+
12
+ ## charts
13
+
14
+ Graphical visualizations of LCA results.
@@ -0,0 +1,27 @@
1
+ from .observer import QuickAndEasy, MissingValue
2
+
3
+ from time import time
4
+
5
+
6
+ class DumbTimer(object):
7
+ """
8
+ a centralized timer
9
+ """
10
+ def __init__(self, head='==', tail=None):
11
+ self.t0 = time()
12
+ self.head = head
13
+ self.tail = tail or ''
14
+
15
+ @property
16
+ def _t(self):
17
+ return time() - self.t0
18
+
19
+ @property
20
+ def _ts(self):
21
+ return '(%5.2f s)' % self._t
22
+
23
+ def format(self, string):
24
+ return '%s%s %s%s' % (self.head, self._ts, string, self.tail)
25
+
26
+ def check(self, string):
27
+ print(self.format(string))
@@ -0,0 +1,5 @@
1
+ from .base import save_plot
2
+ from .combi import scenario_compare_figure, stack_bar_figure
3
+
4
+ from .pos_neg import PosNegChart
5
+ from .waterfall import WaterfallChart
@@ -0,0 +1,210 @@
1
+ from __future__ import division
2
+ import colorsys
3
+ import matplotlib as mpl
4
+ import matplotlib.pyplot as plt
5
+ from textwrap import wrap
6
+
7
+ import os
8
+
9
+ # grumble something about breaking styles for no good reason with 2.0.0
10
+ mpl.rcParams['patch.force_edgecolor'] = True
11
+ mpl.rcParams['errorbar.capsize'] = 3
12
+ mpl.rcParams['grid.color'] = 'k'
13
+ mpl.rcParams['grid.linestyle'] = ':'
14
+ mpl.rcParams['grid.linewidth'] = 0.5
15
+ mpl.rcParams['lines.linewidth'] = 1.0
16
+ mpl.rcParams['axes.autolimit_mode'] = 'round_numbers'
17
+ mpl.rcParams['axes.xmargin'] = 0
18
+ mpl.rcParams['axes.ymargin'] = 0
19
+
20
+
21
+ prefab_colors = ((0.1, 0.6, 0.7), (0.9, 0.3, 0.4), (0.3, 0.9, 0.6), (0.1, 0.1, 0.8), (0.4, 0.9, 0.3), (0.3, 0.4, 0.9))
22
+ net_color = (0.6, 0.6, 0.6)
23
+
24
+
25
+ def linspace(a, b, num):
26
+ if num < 2:
27
+ yield a
28
+ else:
29
+ dif = float(b - a) / (num - 1)
30
+ for i in range(num - 1):
31
+ yield a
32
+ a += dif
33
+ yield b
34
+
35
+
36
+ def color_range(num, hue, sat=0.5):
37
+ """
38
+ Generator for a series of num RGB tuples, derived from the color provided in hue.
39
+ If hue is a 2-tuple, provides a gradation from [0] to [1]. Uses either hsv or hsl to rgb.
40
+ """
41
+ if not isinstance(hue, tuple):
42
+ hue = (hue, hue)
43
+ h = linspace(hue[0], hue[1], num)
44
+ v = linspace(0.95, 0.45, num)
45
+ for i in range(num):
46
+ yield colorsys.hsv_to_rgb(next(h), sat, next(v))
47
+
48
+
49
+ def hue_from_string(s, base=16):
50
+ hue = int(s, base)
51
+ return hue / (base**len(s))
52
+
53
+
54
+ def _label_bar(patch, value=None, label=None, valueformat='%4.3g', labelformat='%s'):
55
+ bl = patch.get_xy()
56
+ x = 0.5 * patch.get_width() + bl[0]
57
+ y = 0.5 * patch.get_height() + bl[1]
58
+ if value is not None:
59
+ patch.axes.text(x, y, valueformat % value, ha='center', va='center')
60
+ if label is not None:
61
+ patch.axes.text(x, y + (0.52 * patch.get_height()), labelformat % label, ha='center', va='bottom')
62
+
63
+
64
+ def label_segment(patch, data, label, threshold, sparse_flag):
65
+ """
66
+ Label the segment if it's big enough. The threshold is the minimum size for a value statement; one-quarter the
67
+ threshold is the minimum size for a bar label. Segments smaller than one-quarter the threshold are considered
68
+ non-sparse, and if there is more than one non-sparse segment in succession, only the first will be labeled.
69
+ A single non-sparse segment resets the labeling. Positive and negative sparse_flags should be tracked separately.
70
+ :param patch:
71
+ :param data:
72
+ :param label:
73
+ :param threshold:
74
+ :param sparse_flag:
75
+ :return:
76
+ """
77
+ if abs(data) > threshold:
78
+ _label_bar(patch, value=data, label=label)
79
+ if sparse_flag or abs(data) > (threshold / 4):
80
+ _label_bar(patch, label=label)
81
+ if abs(data) < (threshold / 4):
82
+ return False
83
+ return True
84
+
85
+
86
+ def label_vbar(patch, value, valueformat='%6.3g', sep=0):
87
+ (y0, y1) = patch.axes.get_ylim()
88
+ vgap = 0.02 * (y1 - y0)
89
+ bl = patch.get_xy()
90
+ x = 0.5 * patch.get_width() + bl[0]
91
+
92
+ y = bl[1] + patch.get_height() + sep + vgap
93
+
94
+ patch.axes.text(x, y, valueformat % value, ha='center', va='bottom')
95
+
96
+
97
+ def has_nonzero(res):
98
+ data = sum([abs(i) for i in res.contrib_query()])
99
+ return data != 0
100
+
101
+
102
+ def has_pos_neg(res):
103
+ """
104
+ Returns true if the input contains both positive and negative data points
105
+ :param res:
106
+ :return:
107
+ """
108
+ data = res.contrib_query()
109
+ try:
110
+ poss = next(k for k in data if k > 0)
111
+ negs = next(k for k in data if k < 0)
112
+ except StopIteration:
113
+ return False
114
+ if poss != 0 and negs != 0:
115
+ return True
116
+ return False
117
+
118
+
119
+ def save_plot(file, close_after=True, transparent=True, format=None):
120
+
121
+ if format is None:
122
+ n, f = os.path.splitext(file)
123
+ if f:
124
+ format = f[1:]
125
+ else:
126
+ format = 'eps'
127
+
128
+ plt.savefig(file, format=format, bbox_inches='tight', transparent=transparent)
129
+ if close_after:
130
+ plt.close()
131
+
132
+
133
+ def standard_labels(ax, stages, ticks=None, width=25, rotate=True):
134
+ """
135
+
136
+ :param ax: axes to modify
137
+ :param stages: tick labels
138
+ :param ticks: locations of ticks [default: range(len(stages))]
139
+ :param width: [25] text wrap width
140
+ :param rotate: [True] whether to rotate long labels. specify bool or rotation argument.
141
+ :return:
142
+ """
143
+ rotation = 0
144
+ if ticks is None:
145
+ ticks = range(len(stages))
146
+ ax.set_xticks(ticks)
147
+ if max([len(l) for l in stages]) > 12:
148
+ labels = ['\n'.join(wrap(l, width)) for l in stages]
149
+ if rotate:
150
+ if isinstance(rotate, bool):
151
+ rotation = 90
152
+ else:
153
+ rotation = rotate
154
+ else:
155
+ labels = stages
156
+
157
+ ax.set_xticklabels(labels, rotation=rotation)
158
+
159
+
160
+ def open_ylims(ax, margin=0.1):
161
+ """
162
+ workaround for buggy Axes.margins() (https://github.com/matplotlib/matplotlib/pull/7995 and others)
163
+ :param ax:
164
+ :param margin:
165
+ :return:
166
+ """
167
+ bottom, top = ax.get_ylim()
168
+ yr = top - bottom
169
+ if bottom < 0 < top:
170
+ bottom -= margin * yr
171
+ top += margin * yr
172
+ ax.set_ylim(bottom=bottom, top=top)
173
+
174
+
175
+ """
176
+ people = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H')
177
+ segments = 4
178
+
179
+ # generate some multi-dimensional data & arbitrary labels
180
+ data = 3 + 10 * np.random.rand(segments, len(people))
181
+ percentages = (np.random.randint(5,20, (len(people), segments)))
182
+ y_pos = np.arange(len(people))
183
+
184
+ fig = plt.figure(figsize=(10, 8))
185
+ ax = fig.add_subplot(111)
186
+
187
+ colors = 'rgbwmc'
188
+ patch_handles = []
189
+ left = np.zeros(len(people)) # left alignment of data starts at zero
190
+ for i, d in enumerate(data):
191
+ patch_handles.append(ax.barh(y_pos, d,
192
+ color=colors[i % len(colors)], align='center',
193
+ left=left))
194
+ # accumulate the left-hand offsets
195
+ left += d
196
+
197
+ # go through all of the bar segments and annotate
198
+ for j in range(len(patch_handles)):
199
+ for i, patch in enumerate(patch_handles[j].get_children()):
200
+ bl = patch.get_xy()
201
+ x = 0.5*patch.get_width() + bl[0]
202
+ y = 0.5*patch.get_height() + bl[1]
203
+ ax.text(x, y, "%d%%" % (percentages[i, j]), ha='center')
204
+
205
+ ax.set_yticks(y_pos)
206
+ ax.set_yticklabels(people)
207
+ ax.set_xlabel('Distance')
208
+
209
+ plt.show()
210
+ """
@@ -0,0 +1,379 @@
1
+ """
2
+ Combination charts.
3
+ - Horizontal aspect;
4
+ - Positive + Negative bars + net bar;
5
+ - multi-impact;
6
+ - scenario comparisons.
7
+
8
+ These are expert charts. Nonexpert users have reported their "brain entirely shuts down" when presented with
9
+ more elaborate instances of these charts.
10
+ """
11
+
12
+ import matplotlib.pyplot as plt
13
+ from .base import color_range, label_segment, has_pos_neg, has_nonzero, save_plot, hue_from_string
14
+
15
+
16
+ def _one_bar(ax, pos_y, neg_y, data, hue, units, threshold, legend_labels=None):
17
+ """
18
+
19
+ :param ax:
20
+ :param pos_y:
21
+ :param neg_y:
22
+ :param data:
23
+ :param hue: if hue is a float-- it's a gradient specifier
24
+ if hue is a 2-tuple-- it's a gradient specifier with different-colored endpoints
25
+ if hue is a tuple of 3-tuples-- treat it AS a color specifier
26
+ :param units:
27
+ :param threshold:
28
+ :param legend_labels: if present, use as an iterable to assign labels to bars for legend purposes
29
+ :return:
30
+ """
31
+ poss = sum([k for k in data if k > 0])
32
+ negs = sum([k for k in data if k < 0])
33
+
34
+ total = poss + negs
35
+
36
+ left = 0.0
37
+ right = 0.0
38
+
39
+ if isinstance(hue, float):
40
+ colors = color_range(len(data), hue)
41
+ elif len(hue) == 2 and isinstance(hue[0], float):
42
+ colors = color_range(len(data), hue)
43
+ else:
44
+ for k in hue:
45
+ assert len(k) == 3, 'invalid color specifier'
46
+ colors = (k for k in hue)
47
+
48
+ total_y = None # where to put the net marker
49
+ total_ha = 'left' #
50
+
51
+ # position the net total indicator
52
+ if poss != 0:
53
+ if negs != 0:
54
+ if total > 0:
55
+ # total indicator is aligned with neg
56
+ total_y = neg_y # - 0.1
57
+ if total > poss / 2:
58
+ total_ha = 'right'
59
+ else:
60
+ total_y = pos_y # + 0.1
61
+ if total > negs / 2:
62
+ total_ha = 'right'
63
+ else:
64
+ neg_y = pos_y
65
+ else:
66
+ if negs == 0:
67
+ return pos_y # nothing to do
68
+ neg_y = pos_y
69
+
70
+ # print('NY: %f PY: %f TY: %f ha: %s' % (neg_y, pos_y, total_y, total_ha))
71
+
72
+ # sparsify labels-- don't pile labels up if there are multiple tiny segments. see _label_segment
73
+ sparse_label_flag_pos = True
74
+ sparse_label_flag_neg = True
75
+
76
+ for (i, d) in enumerate(data):
77
+ color = next(colors)
78
+ if legend_labels is None:
79
+ _thelabel = chr(ord('A') + i)
80
+ else:
81
+ _thelabel = ''
82
+ if d > 0:
83
+ patch = ax.barh(pos_y, d, color=color, align='center', left=right, height=1)
84
+ sparse_label_flag_pos = label_segment(patch[0], d, _thelabel, threshold, sparse_label_flag_pos)
85
+ right += d
86
+ elif d < 0:
87
+ left += d
88
+ patch = ax.barh(neg_y, abs(d), color=color, align='center', left=left, height=1)
89
+ sparse_label_flag_neg = label_segment(patch[0], d, _thelabel, threshold, sparse_label_flag_neg)
90
+ else: # d == 0
91
+ continue
92
+ if legend_labels is not None:
93
+ try:
94
+ patch.set_label(legend_labels[i])
95
+ except IndexError:
96
+ pass
97
+
98
+ if units is None:
99
+ unitstring = ''
100
+ else:
101
+ unitstring = ' [%s]' % units
102
+
103
+ if poss != 0:
104
+ ax.text(right, pos_y, ' %6.3g%s' % (right, unitstring), fontsize=12, fontweight='bold',
105
+ ha='left', va='center')
106
+
107
+ if negs != 0:
108
+ ax.text(right, neg_y, ' %6.3g%s' % (left, unitstring), ha='left', va='center', fontsize=12, fontweight='bold')
109
+
110
+ if total_y is not None:
111
+ ax.plot(total, total_y, 'd', markersize=10, markeredgecolor='k', markeredgewidth=0.5)
112
+ ax.barh(total_y, total, color=(0.74, 0.74, 0.74), height=0.65, edgecolor='none')
113
+ ax.text(total, total_y, ' %6.3g ' % total, ha=total_ha, va='center')
114
+
115
+ return neg_y - 0.55
116
+
117
+
118
+ def stack_bar(ax, data, hue, units, title='Contribution Analysis', subtitle='by stage', **kwargs):
119
+ """
120
+ Draws a single stack bar chart on the existing axes.
121
+ ax = axes
122
+ data = ordered data corresponding to the above contributions
123
+ hue = float [0..1] used as the base color in shading the bars
124
+ hue can also be (hue1, hue2), in which case the bars follow a gradient from hue1 to hue2
125
+ units = appended to total in square brackets if present
126
+ title, subtitle- drawn on graph
127
+
128
+ If several stack bar charts are to be shown side by side, they should all have the data ordered the same.
129
+ """
130
+ stack_bars(ax, [data], hue, [units], title=title, subtitle=subtitle, **kwargs)
131
+
132
+
133
+ def stack_bars(ax, series, hue, units, labels=None, title='Scenario Analysis', subtitle='by stage',
134
+ center_scale=False, **kwargs):
135
+ """
136
+
137
+ :param ax:
138
+ :param series: must be a list of iterables of numerical data, presumably all the same length
139
+ :param hue:
140
+ :param units:
141
+ :param labels: None, or a list of text strings to label each bar set
142
+ :param title:
143
+ :param subtitle:
144
+ :return:
145
+ """
146
+ # first, determine left and right ranges
147
+ left = 0.0
148
+ right = 0.0
149
+ for data in series:
150
+ poss = sum([k for k in data if k > 0])
151
+ negs = sum([k for k in data if k < 0])
152
+
153
+ if poss > right:
154
+ right = poss
155
+ if negs < left:
156
+ left = negs
157
+
158
+ if center_scale:
159
+ right = max([abs(right), abs(left)])
160
+ if left < 0:
161
+ left = -right
162
+
163
+ data_range = right - left
164
+ threshold = 0.07 * data_range
165
+ top = 1.1
166
+ bar_skip = 1.6
167
+
168
+ # defaults
169
+ pos_y = .55
170
+ btm = 0
171
+
172
+ # make the plots
173
+ for i, data in enumerate(series):
174
+ neg_y = pos_y - 1
175
+ btm = _one_bar(ax, pos_y, neg_y, data, hue, units[i], threshold, **kwargs)
176
+ if labels is not None:
177
+ ax.text(left, pos_y, '%s ' % labels[i], ha='right', va='center', fontsize=12)
178
+ pos_y = btm - bar_skip
179
+
180
+ # common to full plot
181
+
182
+ ax.text(0, 2.3, subtitle, color=(0.3, 0.3, 0.3), size='small')
183
+ ax.text(0, 2.8, title, size='large')
184
+
185
+ ax.spines['top'].set_visible(False)
186
+ ax.spines['bottom'].set_visible(False)
187
+ ax.spines['left'].set_visible(False)
188
+ ax.spines['right'].set_visible(False)
189
+
190
+ ax.set_yticks([])
191
+ if data_range == 0:
192
+ ax.set_xticks([])
193
+ ax.set_xlim(left, left + 1)
194
+ ax.text(0, 1.1, 'No impacts', ha='left', va='bottom')
195
+ ax.set_ylim(1, 3.6)
196
+ else:
197
+ ax.plot((0, 0), (btm, top), 'k-') # line at origin
198
+
199
+ ax.xaxis.set_ticks_position('bottom')
200
+ ax.set_xlim(left - 0.02 * data_range, right + 0.08 * data_range)
201
+ ax.set_ylim(btm, 3.6)
202
+
203
+
204
+ def stack_bar_figure(results, stages, hues=None, color_dict=None, legend=False):
205
+ """
206
+ Create a stack bar figure.
207
+ :param results:
208
+ :param stages:
209
+ :param hues: by default, each stack bar is a gradient of a single hue- supply an array of hues matching length of
210
+ results array, or else hue is generated automatically from quantity uuid / ext ref
211
+ :param color_dict: optionally supply a dict mapping stage name to rgb triple, instead of hue gradient
212
+ :param legend: [False] if true, draw a legend below the bottom bar
213
+ :return:
214
+ """
215
+
216
+ if hues is None:
217
+ hues = [None] * len(results)
218
+
219
+ units = [r.quantity.unit for r in results]
220
+
221
+ # prepare data, determine figure height
222
+ height = 0
223
+ data = [None] * len(results)
224
+ for i, r in enumerate(results):
225
+ data[i] = r.contrib_query(stages)
226
+ height += 1.8
227
+ if has_pos_neg(r):
228
+ height += 0.4
229
+
230
+ fig = plt.figure(figsize=(8, height))
231
+
232
+ if color_dict is None:
233
+ colors = None
234
+ else:
235
+ colors = [color_dict[s] for s in stages]
236
+
237
+ ax = []
238
+
239
+ for i, r in enumerate(results):
240
+ ax.append(fig.add_subplot(len(results), 1, i + 1))
241
+
242
+ quantity = r.quantity
243
+
244
+ if colors is None:
245
+ hue = hues[i]
246
+ if hue is None:
247
+ hue = hue_from_string('%.4s' % quantity.uuid)
248
+ else:
249
+ hue = colors
250
+
251
+ stack_bar(ax[i], data[i], hue, units[i], quantity['Name'], quantity['Indicator'], legend_labels=stages)
252
+
253
+ def _zero_pos(_a):
254
+ _xl = _a.get_xlim()
255
+ return max(_xl) / (max(_xl) - min(_xl))
256
+
257
+ _min = min(_zero_pos(a) for a in ax)
258
+
259
+ for a in ax:
260
+ """
261
+ need to adjust axes limits to align zeros
262
+ """
263
+ _xl = a.get_xlim()
264
+ _low = max(_xl) * (_min - 1.0) / _min
265
+ a.set_xlim(_low, max(_xl))
266
+
267
+ if legend:
268
+ ax[0].legend(loc='upper right', bbox_to_anchor=(0, 1))
269
+
270
+ return fig
271
+
272
+
273
+ def _stackbar_subfig_height(results):
274
+ """
275
+ returns the height of
276
+ :param results: an array of LciaResult objects
277
+ :return:
278
+ """
279
+ height = 0
280
+
281
+ for r in results:
282
+ if has_nonzero(r):
283
+ height += 0.9
284
+ if has_pos_neg(r):
285
+ height += 0.7
286
+ else:
287
+ height += 0.6
288
+ return height
289
+
290
+
291
+ def _scenario_fig_height(results, scenario=False):
292
+ height = 0
293
+ if scenario:
294
+ for m, res in enumerate(results):
295
+ height += _stackbar_subfig_height(res)
296
+ height += len(results[0]) - 1
297
+ else:
298
+ for m, res in enumerate(results):
299
+ height += _stackbar_subfig_height([res])
300
+ height += 1
301
+
302
+ print('### Height is: %f' % height)
303
+ return height
304
+
305
+
306
+ def scenario_compare_figure(results, stages, hues=None, scenarios=None, savefile=None, center_scale=False,
307
+ figwidth=8):
308
+ """
309
+ A bit of a swiss army knife.
310
+
311
+ if scenarios is None, results is an n-array of LciaResult objects. Creates an n-subplot figure of
312
+ stacked pos/neg bars, annotated by reference to k ordered stages.
313
+
314
+ if scenarios is not none, it must be an m-array of scenario names or labels. results is interpreted as
315
+ an m-array of n-arrays of LciaResult objects. Creates an n-subplot figure of m-stacked pos/neg bars, each
316
+ featuring k stages
317
+
318
+ this needs to be done vertical, with the stages to the side
319
+
320
+ or else the waterfall charts need to be scaled
321
+ waterfall wasn't made that way
322
+
323
+ :param results: either a n-array or a m-by-n array of results
324
+ :param stages: k query terms to results
325
+ :param hues: for n quantities represented (quick-compute from UUID)
326
+ :param scenarios: None or m-array
327
+ :param savefile: if present, save the fig to the specified file
328
+ :return:
329
+ """
330
+
331
+ if scenarios is None:
332
+ height = _scenario_fig_height(results, scenario=False)
333
+ fig = plt.figure(figsize=(figwidth, height))
334
+ quantities = [r.quantity for r in results]
335
+
336
+ else:
337
+ height = _scenario_fig_height(results, scenario=True)
338
+ fig = plt.figure(figsize=(figwidth, height))
339
+ quantities = [r.quantity for r in results[0]]
340
+
341
+ if hues is None:
342
+ hues = [None] * len(quantities)
343
+
344
+ for n, quantity in enumerate(quantities):
345
+ hue = hues[n]
346
+ ax = fig.add_subplot(len(quantities), 1, n + 1)
347
+
348
+ if hue is None:
349
+ hue = hue_from_string('%.4s' % quantity.uuid)
350
+
351
+ if scenarios is None:
352
+ series = results[n].contrib_query(stages)
353
+ stack_bars(ax, [series], hue, [quantity.unit],
354
+ title=quantity['Name'], subtitle=quantity['Indicator'],
355
+ center_scale=center_scale)
356
+
357
+ else:
358
+ series = [r[n].contrib_query(stages) for r in results]
359
+ units = [r[n].quantity.unit for r in results]
360
+
361
+ stack_bars(ax, series, hue, units, labels=scenarios,
362
+ title=quantity['Name'], subtitle=quantity['Indicator'],
363
+ center_scale=center_scale)
364
+
365
+ """
366
+ if n == 0:
367
+ colors = [k for k in color_range(len(stages), hue)]
368
+ handles = [mpatches.Patch(color=colors[i], label='%s -- %s' % (chr(ord('A') + i), k))
369
+ for i, k in enumerate(stages)]
370
+
371
+ ax.legend(handles=handles, bbox_to_anchor=(1.75, 0.75))
372
+ """
373
+ leg = 'Stages: \n' + '\n'.join(['%s -- %s' % (chr(ord('A') + i), k) for i, k in enumerate(stages)])
374
+ print(leg)
375
+
376
+ if savefile is not None:
377
+ save_plot(savefile, close_after=False)
378
+
379
+ return fig