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.
- antelope_reports-0.2.0/PKG-INFO +38 -0
- antelope_reports-0.2.0/README.md +14 -0
- antelope_reports-0.2.0/antelope_reports/__init__.py +27 -0
- antelope_reports-0.2.0/antelope_reports/charts/__init__.py +5 -0
- antelope_reports-0.2.0/antelope_reports/charts/base.py +210 -0
- antelope_reports-0.2.0/antelope_reports/charts/combi.py +379 -0
- antelope_reports-0.2.0/antelope_reports/charts/model_graph.py +142 -0
- antelope_reports-0.2.0/antelope_reports/charts/pos_neg.py +496 -0
- antelope_reports-0.2.0/antelope_reports/charts/simple.py +6 -0
- antelope_reports-0.2.0/antelope_reports/charts/vertical.py +225 -0
- antelope_reports-0.2.0/antelope_reports/charts/waterfall.py +883 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/__init__.py +26 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/ecoinvent_grids.py +228 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/lca_study.py +538 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/model_updater.py +321 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/param_manager.py +158 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/scenario_updater.py +129 -0
- antelope_reports-0.2.0/antelope_reports/fg_builder/stage_manager.py +187 -0
- antelope_reports-0.2.0/antelope_reports/kpi/__init__.py +2 -0
- antelope_reports-0.2.0/antelope_reports/kpi/equivalency.py +113 -0
- antelope_reports-0.2.0/antelope_reports/kpi/kpi.py +520 -0
- antelope_reports-0.2.0/antelope_reports/lca_test/__init__.py +281 -0
- antelope_reports-0.2.0/antelope_reports/lcia/__init__.py +42 -0
- antelope_reports-0.2.0/antelope_reports/lcia/ecoinvent.py +35 -0
- antelope_reports-0.2.0/antelope_reports/lcia/flow_comparator.py +104 -0
- antelope_reports-0.2.0/antelope_reports/lcia/lcia_eval.py +165 -0
- antelope_reports-0.2.0/antelope_reports/lcia/qdb_client/__init__.py +5 -0
- antelope_reports-0.2.0/antelope_reports/lcia/qdb_client/qdb_client.py +82 -0
- antelope_reports-0.2.0/antelope_reports/lcia/screening.py +105 -0
- antelope_reports-0.2.0/antelope_reports/lcia/traci.py +107 -0
- antelope_reports-0.2.0/antelope_reports/lcia/worldsteel.py +145 -0
- antelope_reports-0.2.0/antelope_reports/lcia/xlsx_lcia.py +222 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/__init__.py +5 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/conventions.py +12 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/interface.py +31 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/lca_model.py +154 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/lca_study.py +124 -0
- antelope_reports-0.2.0/antelope_reports/mfa_models/markets.py +141 -0
- antelope_reports-0.2.0/antelope_reports/mfa_study/__init__.py +16 -0
- antelope_reports-0.2.0/antelope_reports/mfa_study/dynamic_unit_study.py +228 -0
- antelope_reports-0.2.0/antelope_reports/mfa_study/lc_mfa_study.py +163 -0
- antelope_reports-0.2.0/antelope_reports/mfa_study/observed_mfa_study.py +94 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/__init__.py +17 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/components_mixin.py +38 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/lca_model_runner.py +439 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/multi_model_runner.py +43 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/quick_model_runner.py +87 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/results_writer.py +139 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/scenario_runner.py +119 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/sens_runner.py +186 -0
- antelope_reports-0.2.0/antelope_reports/model_runner/simple_model_builder.py +25 -0
- antelope_reports-0.2.0/antelope_reports/observer/__init__.py +3 -0
- antelope_reports-0.2.0/antelope_reports/observer/exchanges_from_spreadsheet.py +173 -0
- antelope_reports-0.2.0/antelope_reports/observer/float_conv.py +33 -0
- antelope_reports-0.2.0/antelope_reports/observer/headers.py +40 -0
- antelope_reports-0.2.0/antelope_reports/observer/model_maker.py +775 -0
- antelope_reports-0.2.0/antelope_reports/observer/node_updater.py +93 -0
- antelope_reports-0.2.0/antelope_reports/observer/observations_from_spreadsheet.py +165 -0
- antelope_reports-0.2.0/antelope_reports/observer/quick_and_easy.py +436 -0
- antelope_reports-0.2.0/antelope_reports/observer/upgrade_manager.py +329 -0
- antelope_reports-0.2.0/antelope_reports/report.py +823 -0
- antelope_reports-0.2.0/antelope_reports/tables/__init__.py +2 -0
- antelope_reports-0.2.0/antelope_reports/tables/fragment_view.py +66 -0
- antelope_reports-0.2.0/antelope_reports/tables/gsheet_writer.py +133 -0
- antelope_reports-0.2.0/antelope_reports/tables/inventory.py +3 -0
- antelope_reports-0.2.0/antelope_reports/tables/old/allocation.py +140 -0
- antelope_reports-0.2.0/antelope_reports/tables/old/base.py +321 -0
- antelope_reports-0.2.0/antelope_reports/tables/old/flowables.py +149 -0
- antelope_reports-0.2.0/antelope_reports.egg-info/PKG-INFO +38 -0
- antelope_reports-0.2.0/antelope_reports.egg-info/SOURCES.txt +74 -0
- antelope_reports-0.2.0/antelope_reports.egg-info/dependency_links.txt +1 -0
- antelope_reports-0.2.0/antelope_reports.egg-info/requires.txt +6 -0
- antelope_reports-0.2.0/antelope_reports.egg-info/top_level.txt +1 -0
- antelope_reports-0.2.0/pyproject.toml +33 -0
- antelope_reports-0.2.0/setup.cfg +4 -0
- 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,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
|