ssb-sgis 0.1.4__py3-none-any.whl → 0.1.6__py3-none-any.whl
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.
- sgis/geopandas_tools/general.py +18 -7
- sgis/geopandas_tools/geometry_types.py +1 -1
- sgis/geopandas_tools/line_operations.py +1 -1
- sgis/geopandas_tools/overlay.py +11 -0
- sgis/maps/explore.py +62 -99
- sgis/maps/legend.py +300 -203
- sgis/maps/map.py +69 -74
- sgis/maps/maps.py +9 -5
- sgis/maps/thematicmap.py +145 -61
- sgis/networkanalysis/_get_route.py +168 -114
- sgis/networkanalysis/_od_cost_matrix.py +7 -9
- sgis/networkanalysis/_points.py +0 -18
- sgis/networkanalysis/directednetwork.py +2 -2
- sgis/networkanalysis/network.py +16 -25
- sgis/networkanalysis/networkanalysis.py +301 -123
- sgis/networkanalysis/networkanalysisrules.py +2 -2
- {ssb_sgis-0.1.4.dist-info → ssb_sgis-0.1.6.dist-info}/METADATA +17 -9
- ssb_sgis-0.1.6.dist-info/RECORD +35 -0
- ssb_sgis-0.1.4.dist-info/RECORD +0 -35
- {ssb_sgis-0.1.4.dist-info → ssb_sgis-0.1.6.dist-info}/LICENSE +0 -0
- {ssb_sgis-0.1.4.dist-info → ssb_sgis-0.1.6.dist-info}/WHEEL +0 -0
sgis/maps/legend.py
CHANGED
|
@@ -5,18 +5,15 @@ class.
|
|
|
5
5
|
|
|
6
6
|
"""
|
|
7
7
|
import warnings
|
|
8
|
-
from statistics import mean
|
|
9
8
|
|
|
10
9
|
import matplotlib
|
|
11
10
|
import matplotlib.pyplot as plt
|
|
12
11
|
import numpy as np
|
|
13
12
|
import pandas as pd
|
|
14
|
-
from geopandas import GeoDataFrame
|
|
15
13
|
from matplotlib.lines import Line2D
|
|
16
14
|
from pandas import Series
|
|
17
15
|
|
|
18
|
-
from ..geopandas_tools.general import points_in_bounds
|
|
19
|
-
from ..geopandas_tools.point_operations import snap_all
|
|
16
|
+
from ..geopandas_tools.general import points_in_bounds
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
# the geopandas._explore raises a deprication warning. Ignoring for now.
|
|
@@ -27,24 +24,18 @@ pd.options.mode.chained_assignment = None
|
|
|
27
24
|
|
|
28
25
|
|
|
29
26
|
class Legend:
|
|
30
|
-
"""Holds the attributes of the legend in the ThematicMap class.
|
|
27
|
+
"""Holds the general attributes of the legend in the ThematicMap class.
|
|
31
28
|
|
|
32
|
-
This class
|
|
29
|
+
This class holds attributes of the 'legend' attribute of the ThematicMap class.
|
|
33
30
|
The fontsize, title_fontsize and markersize attributes are adjusted
|
|
34
31
|
according to the size attribute of the ThematicMap.
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
The 'labels' attribute can be used to set labels manually. By default, the
|
|
40
|
-
maximum and minimum values of each color group is used as label for numeric
|
|
41
|
-
columns. For categorical columns, the column values are used.
|
|
33
|
+
If a numeric column is used, additional attributes can be found in the
|
|
34
|
+
ContinousLegend class.
|
|
42
35
|
|
|
43
36
|
Attributes:
|
|
44
37
|
title: Legend title. Defaults to the column name if used in the
|
|
45
38
|
ThematicMap class.
|
|
46
|
-
labels: To manually set labels for the color groups. Must be a list/tuple of
|
|
47
|
-
same length as the number of color groups (k).
|
|
48
39
|
position: The legend's x and y position in the plot, specified as a tuple of
|
|
49
40
|
x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
|
|
50
41
|
in the bottom right corner, (0.2, 0.8) for the upper left corner.
|
|
@@ -56,13 +47,6 @@ class Legend:
|
|
|
56
47
|
the ThematicMap class.
|
|
57
48
|
framealpha: Transparency of the legend background.
|
|
58
49
|
edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
|
|
59
|
-
label_suffix: For numeric columns. The text to put after each number in the
|
|
60
|
-
legend labels.
|
|
61
|
-
label_sep: For numeric columns. Text to put in between the two numbers in each
|
|
62
|
-
color group in the legend.
|
|
63
|
-
rounding: For numeric columns. Number of decimals in the legend labels. By
|
|
64
|
-
default the rounding depends on the column's maximum value and standard
|
|
65
|
-
deviation.
|
|
66
50
|
kwargs: Stores additional keyword arguments taken by the matplotlib legend
|
|
67
51
|
method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
|
|
68
52
|
'm' is the name of the ThematicMap instance. See here:
|
|
@@ -88,9 +72,12 @@ class Legend:
|
|
|
88
72
|
8 POINT (0.12215 0.96588) 8
|
|
89
73
|
9 POINT (0.02938 0.93467) 9
|
|
90
74
|
|
|
91
|
-
Creating the ThematicMap instance.
|
|
75
|
+
Creating the ThematicMap instance will also create the legend. Since we
|
|
76
|
+
specify a numeric column, a ContinousLegend instance is created.
|
|
92
77
|
|
|
93
78
|
>>> m = sg.ThematicMap(points, column="number")
|
|
79
|
+
>>> m.legend
|
|
80
|
+
<sgis.maps.legend.ContinousLegend object at 0x00000222206738D0>
|
|
94
81
|
|
|
95
82
|
Changing the attributes that apply to both numeric and categorical columns.
|
|
96
83
|
|
|
@@ -99,34 +86,26 @@ class Legend:
|
|
|
99
86
|
>>> m.legend.fontsize = 9
|
|
100
87
|
>>> m.legend.markersize = 7.5
|
|
101
88
|
>>> m.legend.position = (0.35, 0.28)
|
|
102
|
-
>>> m.legend.kwargs["labelcolor"] = "red"
|
|
103
89
|
>>> m.plot()
|
|
104
90
|
|
|
105
|
-
|
|
91
|
+
Additional matplotlib keyword arguments can be specified as kwargs.
|
|
92
|
+
|
|
93
|
+
>>> m.legend.kwargs["labelcolor"] = "red"
|
|
94
|
+
|
|
95
|
+
Since we are using a numeric column, the legend is of type ContinousLegend.
|
|
96
|
+
We can therefore also access the attributes that only apply to numeric columns.
|
|
106
97
|
|
|
107
|
-
>>> m = sg.ThematicMap(points, column="number")
|
|
108
98
|
>>> m.label_sep = "to"
|
|
109
99
|
>>> m.label_suffix = "num"
|
|
110
100
|
>>> m.rounding = 2
|
|
111
101
|
>>> m.plot()
|
|
112
102
|
|
|
113
|
-
The final attribute, labels, should be changed along with the bins attribute
|
|
114
|
-
of the ThematicMap class. The following bins will create a plot with the color
|
|
115
|
-
groups 0-2, 3-5, 6-7 and 8-9. The legend labels can then be set accordingly.
|
|
116
|
-
|
|
117
|
-
>>> m.bins = [2, 5, 7]
|
|
118
|
-
>>> m.legend.labels = ["0 to 2 num", "3 to 5 num", "6 to 7 num", "8 to 9 num"]
|
|
119
|
-
>>> m.plot()
|
|
120
|
-
|
|
121
103
|
"""
|
|
122
104
|
|
|
123
105
|
def __init__(
|
|
124
106
|
self,
|
|
125
107
|
title: str | None = None,
|
|
126
108
|
labels: list[str] | None = None,
|
|
127
|
-
label_suffix: str = "",
|
|
128
|
-
label_sep: str = "-",
|
|
129
|
-
rounding: int | None = None,
|
|
130
109
|
position: tuple[float] | None = None,
|
|
131
110
|
markersize: int | None = None,
|
|
132
111
|
fontsize: int | None = None,
|
|
@@ -152,14 +131,10 @@ class Legend:
|
|
|
152
131
|
self.title_color = kwargs.pop("title_color", None)
|
|
153
132
|
self.labelspacing = kwargs.pop("labelspacing", 0.8)
|
|
154
133
|
|
|
155
|
-
self.label_suffix = label_suffix
|
|
156
|
-
self.label_sep = label_sep
|
|
157
134
|
self.labels = labels
|
|
158
|
-
self._rounding = rounding
|
|
159
135
|
self._position = position
|
|
160
136
|
self.kwargs = kwargs
|
|
161
137
|
self._position_has_been_set = True if position else False
|
|
162
|
-
self._rounding_has_been_set = True if rounding else False
|
|
163
138
|
|
|
164
139
|
def _get_legend_sizes(self, size, kwargs):
|
|
165
140
|
"""Adjust fontsize and markersize to size kwarg."""
|
|
@@ -182,128 +157,17 @@ class Legend:
|
|
|
182
157
|
else:
|
|
183
158
|
self._markersize = size
|
|
184
159
|
|
|
185
|
-
def
|
|
186
|
-
def isinteger(x):
|
|
187
|
-
return np.equal(np.mod(x, 1), 0)
|
|
188
|
-
|
|
189
|
-
if np.all(isinteger(array)):
|
|
190
|
-
return 0
|
|
191
|
-
if np.max(array) > 30 and np.std(array) > 5:
|
|
192
|
-
return 0
|
|
193
|
-
if np.max(array) > 5 and np.std(array) > 1:
|
|
194
|
-
return 1
|
|
195
|
-
if np.max(array) > 1 and np.std(array) > 0.1:
|
|
196
|
-
return 2
|
|
197
|
-
return int(abs(np.log10(np.std(array)))) + 1
|
|
198
|
-
|
|
199
|
-
@staticmethod
|
|
200
|
-
def _set_rounding(bins, rounding: int | float):
|
|
201
|
-
if rounding == 0:
|
|
202
|
-
return [int(round(bin, 0)) for bin in bins]
|
|
203
|
-
else:
|
|
204
|
-
return [round(bin, rounding) for bin in bins]
|
|
205
|
-
|
|
206
|
-
def _remove_max_legend_value(self):
|
|
207
|
-
if not self._legend:
|
|
208
|
-
raise ValueError("Cannot modify legend before it is created.")
|
|
209
|
-
|
|
210
|
-
def _actually_add_continous_legend(
|
|
211
|
-
self,
|
|
212
|
-
ax,
|
|
213
|
-
bins: list[float],
|
|
214
|
-
colors: list[str],
|
|
215
|
-
nan_label: str,
|
|
216
|
-
bin_values: dict,
|
|
217
|
-
):
|
|
218
|
-
for attr in self.__dict__.keys():
|
|
219
|
-
if attr in self.kwargs:
|
|
220
|
-
self[attr] = self.kwargs.pop(attr)
|
|
221
|
-
|
|
222
|
-
self._patches, self._categories = [], []
|
|
223
|
-
|
|
224
|
-
for color in colors:
|
|
225
|
-
self._patches.append(
|
|
226
|
-
Line2D(
|
|
227
|
-
[0],
|
|
228
|
-
[0],
|
|
229
|
-
linestyle="none",
|
|
230
|
-
marker="o",
|
|
231
|
-
alpha=self.kwargs.get("alpha", 1),
|
|
232
|
-
markersize=self._markersize,
|
|
233
|
-
markerfacecolor=color,
|
|
234
|
-
markeredgewidth=0,
|
|
235
|
-
)
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
if self.labels:
|
|
239
|
-
if len(self.labels) != len(colors):
|
|
240
|
-
raise ValueError(
|
|
241
|
-
f"Label list must be same length as 'k'. Got k={len(colors)} and "
|
|
242
|
-
f"labels={len(self.labels)}"
|
|
243
|
-
)
|
|
244
|
-
self._categories = self.labels
|
|
245
|
-
|
|
246
|
-
elif len(bins) == len(colors):
|
|
247
|
-
for i, _ in enumerate(bins):
|
|
248
|
-
min_ = np.min(bin_values[i])
|
|
249
|
-
max_ = np.max(bin_values[i])
|
|
250
|
-
min_rounded = self._set_rounding([min_], self._rounding)[0]
|
|
251
|
-
max_rounded = self._set_rounding([max_], self._rounding)[0]
|
|
252
|
-
if min_ == max_:
|
|
253
|
-
self._categories.append(f"{min_rounded} {self.label_suffix}")
|
|
254
|
-
else:
|
|
255
|
-
self._categories.append(
|
|
256
|
-
f"{min_rounded} {self.label_suffix} {self.label_sep} "
|
|
257
|
-
f"{max_rounded} {self.label_suffix}"
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
else:
|
|
261
|
-
for i, (cat1, cat2) in enumerate(zip(bins[:-1], bins[1:], strict=True)):
|
|
262
|
-
if nan_label in str(cat1) or nan_label in str(cat2):
|
|
263
|
-
self._categories.append(nan_label)
|
|
264
|
-
else:
|
|
265
|
-
min_ = np.min(bin_values[i])
|
|
266
|
-
max_ = np.max(bin_values[i])
|
|
267
|
-
min_rounded = self._set_rounding([min_], self._rounding)[0]
|
|
268
|
-
max_rounded = self._set_rounding([max_], self._rounding)[0]
|
|
269
|
-
if min_ == max_:
|
|
270
|
-
self._categories.append(f"{min_rounded} {self.label_suffix}")
|
|
271
|
-
else:
|
|
272
|
-
self._categories.append(
|
|
273
|
-
f"{min_rounded} {self.label_suffix} {self.label_sep} "
|
|
274
|
-
f"{max_rounded} {self.label_suffix}"
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
legend = ax.legend(
|
|
278
|
-
self._patches,
|
|
279
|
-
self._categories,
|
|
280
|
-
fontsize=self._fontsize,
|
|
281
|
-
title=self.title,
|
|
282
|
-
title_fontsize=self._title_fontsize,
|
|
283
|
-
bbox_to_anchor=self._position + (self.width, self.height),
|
|
284
|
-
fancybox=False,
|
|
285
|
-
framealpha=self.framealpha,
|
|
286
|
-
edgecolor=self.edgecolor,
|
|
287
|
-
labelspacing=self.labelspacing,
|
|
288
|
-
**self.kwargs,
|
|
289
|
-
)
|
|
290
|
-
|
|
291
|
-
if self.title_color:
|
|
292
|
-
plt.setp(legend.get_title(), color=self.title_color)
|
|
293
|
-
|
|
294
|
-
return ax
|
|
295
|
-
|
|
296
|
-
def _actually_add_categorical_legend(
|
|
297
|
-
self, ax, categories_colors: dict, nan_label: str
|
|
298
|
-
):
|
|
160
|
+
def _prepare_categorical_legend(self, categories_colors: dict, nan_label: str):
|
|
299
161
|
for attr in self.__dict__.keys():
|
|
300
162
|
if attr in self.kwargs:
|
|
301
163
|
self[attr] = self.kwargs.pop(attr)
|
|
302
164
|
|
|
165
|
+
# swap column values with label values if labels is dict
|
|
303
166
|
if self.labels and isinstance(self.labels, dict):
|
|
304
167
|
categories_colors = {
|
|
305
168
|
self.labels[cat]: color for cat, color in categories_colors.items()
|
|
306
169
|
}
|
|
170
|
+
# swap column values with label list and hope it's in the correct order
|
|
307
171
|
elif self.labels:
|
|
308
172
|
categories_colors = {
|
|
309
173
|
label: color
|
|
@@ -330,6 +194,8 @@ class Legend:
|
|
|
330
194
|
markeredgewidth=0,
|
|
331
195
|
)
|
|
332
196
|
)
|
|
197
|
+
|
|
198
|
+
def _actually_add_legend(self, ax):
|
|
333
199
|
legend = ax.legend(
|
|
334
200
|
self._patches,
|
|
335
201
|
self._categories,
|
|
@@ -380,6 +246,9 @@ class Legend:
|
|
|
380
246
|
def __getitem__(self, item):
|
|
381
247
|
return getattr(self, item)
|
|
382
248
|
|
|
249
|
+
def __setitem__(self, key, value):
|
|
250
|
+
setattr(self, key, value)
|
|
251
|
+
|
|
383
252
|
def get(self, key, default=None):
|
|
384
253
|
try:
|
|
385
254
|
return self[key]
|
|
@@ -395,15 +264,6 @@ class Legend:
|
|
|
395
264
|
self._position = new_value
|
|
396
265
|
self._position_has_been_set = True
|
|
397
266
|
|
|
398
|
-
@property
|
|
399
|
-
def rounding(self):
|
|
400
|
-
return self._rounding
|
|
401
|
-
|
|
402
|
-
@rounding.setter
|
|
403
|
-
def rounding(self, new_value: bool):
|
|
404
|
-
self._rounding = new_value
|
|
405
|
-
self._rounding_has_been_set = True
|
|
406
|
-
|
|
407
267
|
@property
|
|
408
268
|
def title_fontsize(self):
|
|
409
269
|
return self._title_fontsize
|
|
@@ -432,58 +292,295 @@ class Legend:
|
|
|
432
292
|
self._markersize_has_been_set = True
|
|
433
293
|
|
|
434
294
|
|
|
435
|
-
class
|
|
295
|
+
class ContinousLegend(Legend):
|
|
436
296
|
"""Holds the legend attributes specific to numeric columns.
|
|
437
297
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
The 'labels' attribute can be used to set labels manually. By default, the
|
|
443
|
-
maximum and minimum values of each color group is used as label.
|
|
298
|
+
The attributes consern the labeling of the groups in the legend.
|
|
299
|
+
Labels can be set manually with the 'labels' attribute, or the format
|
|
300
|
+
of the labels can be changed with the remaining attributes.
|
|
444
301
|
|
|
445
302
|
Attributes:
|
|
446
|
-
labels: To manually set labels
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
303
|
+
labels: To manually set labels. If set, all other labeling attributes are
|
|
304
|
+
ignored. Should be given as a list of strings with the same length as
|
|
305
|
+
the number of color groups.
|
|
306
|
+
pretty_labels: If False (default), the minimum and maximum values of each
|
|
307
|
+
color group will be used as legend labels. If True, the labels will end
|
|
308
|
+
with the maximum value, but start at 1 + the maximum value of the previous
|
|
309
|
+
group. The labels will be correct but inaccurate.
|
|
310
|
+
label_suffix: The text to put after each number in the legend labels.
|
|
311
|
+
Defaults to None.
|
|
312
|
+
label_sep: Text to put in between the two numbers in each color group in
|
|
313
|
+
the legend. Defaults to '-'.
|
|
314
|
+
rounding: Number of decimals in the labels. By default, the rounding
|
|
315
|
+
depends on the column's maximum value and standard deviation.
|
|
316
|
+
OBS: The bins will not be rounded, meaning the labels might be wrong
|
|
317
|
+
if not bins are set manually.
|
|
318
|
+
thousand_sep: Separator between each thousand for large numbers. Defaults to
|
|
319
|
+
None, meaning no separator.
|
|
320
|
+
decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
|
|
321
|
+
unless 'thousand_sep' is '.'. In this case, ',' (comma) will be used as
|
|
322
|
+
decimal mark.
|
|
454
323
|
|
|
455
324
|
Examples
|
|
456
325
|
--------
|
|
457
|
-
|
|
326
|
+
Create ten random points with a numeric column from 0 to 9.
|
|
458
327
|
|
|
459
|
-
|
|
328
|
+
>>> import sgis as sg
|
|
329
|
+
>>> points = sg.random_points(10)
|
|
330
|
+
>>> points["number"] = range(10)
|
|
331
|
+
>>> points
|
|
332
|
+
geometry number
|
|
333
|
+
0 POINT (0.59780 0.50425) 0
|
|
334
|
+
1 POINT (0.07019 0.26167) 1
|
|
335
|
+
2 POINT (0.56475 0.15422) 2
|
|
336
|
+
3 POINT (0.87293 0.60316) 3
|
|
337
|
+
4 POINT (0.47373 0.20040) 4
|
|
338
|
+
5 POINT (0.98661 0.15614) 5
|
|
339
|
+
6 POINT (0.30951 0.77057) 6
|
|
340
|
+
7 POINT (0.47802 0.52824) 7
|
|
341
|
+
8 POINT (0.12215 0.96588) 8
|
|
342
|
+
9 POINT (0.02938 0.93467) 9
|
|
460
343
|
|
|
344
|
+
Creating the ThematicMap instance with a numeric column.
|
|
461
345
|
|
|
462
|
-
|
|
463
|
-
"""Holds the attributes of the legend in the ThematicMap class.
|
|
346
|
+
>>> m = sg.ThematicMap(points, column="number")
|
|
464
347
|
|
|
465
|
-
|
|
466
|
-
The fontsize, title_fontsize and markersize attributes are adjusted
|
|
467
|
-
according to the size attribute of the ThematicMap.
|
|
348
|
+
Changing the attributes that apply to both numeric and categorical columns.
|
|
468
349
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
350
|
+
>>> m.legend.title = "Meters"
|
|
351
|
+
>>> m.legend.position = (0.35, 0.28)
|
|
352
|
+
|
|
353
|
+
Change the attributes that only apply to numeric columns.
|
|
354
|
+
|
|
355
|
+
>>> m.label_sep = "to"
|
|
356
|
+
>>> m.label_suffix = "num"
|
|
357
|
+
>>> m.rounding = 2
|
|
358
|
+
>>> m.plot()
|
|
359
|
+
|
|
360
|
+
Setting labels manually. For better control, it might be wise to also set the bins
|
|
361
|
+
manually. The following bins will create a plot with the color groups
|
|
362
|
+
0-2, 3-5, 6-7 and 8-9. The legend labels can then be set accordingly.
|
|
363
|
+
|
|
364
|
+
>>> m = sg.ThematicMap(points, column="number")
|
|
365
|
+
>>> m.bins = [0, 2, 5, 7, 9]
|
|
366
|
+
>>> m.legend.labels = ["0 to 2 num", "3 to 5 num", "6 to 7 num", "8 to 9 num"]
|
|
367
|
+
>>> m.plot()
|
|
368
|
+
|
|
369
|
+
We will get the same groups if we exclude the first and last bin values. The
|
|
370
|
+
minimum and maximum values will be filled anyway.
|
|
371
|
+
|
|
372
|
+
>>> m = sg.ThematicMap(points, column="number")
|
|
373
|
+
>>> m.bins = [2, 5, 7]
|
|
374
|
+
>>> m.legend.labels = ["0 to 2 num", "3 to 5 num", "6 to 7 num", "8 to 9 num"]
|
|
375
|
+
>>> m.plot()
|
|
484
376
|
|
|
485
|
-
Examples
|
|
486
|
-
--------
|
|
487
377
|
"""
|
|
488
378
|
|
|
489
|
-
|
|
379
|
+
def __init__(
|
|
380
|
+
self,
|
|
381
|
+
labels: list[str] | None = None,
|
|
382
|
+
pretty_labels: bool = False,
|
|
383
|
+
label_suffix: str | None = None,
|
|
384
|
+
label_sep: str = "-",
|
|
385
|
+
rounding: int | None = None,
|
|
386
|
+
thousand_sep: str | None = None,
|
|
387
|
+
decimal_mark: str | None = None,
|
|
388
|
+
**kwargs,
|
|
389
|
+
):
|
|
390
|
+
super().__init__(**kwargs)
|
|
391
|
+
|
|
392
|
+
self.pretty_labels = pretty_labels
|
|
393
|
+
self.thousand_sep = thousand_sep
|
|
394
|
+
self.decimal_mark = decimal_mark
|
|
395
|
+
|
|
396
|
+
self.label_sep = label_sep
|
|
397
|
+
self.label_suffix = "" if not label_suffix else label_suffix
|
|
398
|
+
self._rounding = rounding
|
|
399
|
+
self._rounding_has_been_set = True if rounding else False
|
|
400
|
+
|
|
401
|
+
def _get_rounding(self, array: Series | np.ndarray) -> int:
|
|
402
|
+
def isinteger(x):
|
|
403
|
+
return np.equal(np.mod(x, 1), 0)
|
|
404
|
+
|
|
405
|
+
if np.all(isinteger(array)):
|
|
406
|
+
return 0
|
|
407
|
+
|
|
408
|
+
closest_to_zero_idx = np.argmin(np.abs(array))
|
|
409
|
+
closest_to_zero = np.abs(array[closest_to_zero_idx])
|
|
410
|
+
|
|
411
|
+
between_1_and_0 = 1 > closest_to_zero > 0
|
|
412
|
+
if between_1_and_0:
|
|
413
|
+
return int(abs(np.log10(abs(closest_to_zero)))) + 1
|
|
414
|
+
|
|
415
|
+
std_ = np.std(array)
|
|
416
|
+
max_ = np.max(array)
|
|
417
|
+
if max_ > 30 and std_ > 5:
|
|
418
|
+
return 0
|
|
419
|
+
if max_ > 5 and std_ > 1:
|
|
420
|
+
return 1
|
|
421
|
+
if max_ > 1 and std_ > 0.1:
|
|
422
|
+
return 2
|
|
423
|
+
return int(abs(np.log10(std_))) + 1
|
|
424
|
+
|
|
425
|
+
@staticmethod
|
|
426
|
+
def _set_rounding(bins, rounding: int | float):
|
|
427
|
+
if rounding == 0:
|
|
428
|
+
return [int(round(bin, 0)) for bin in bins]
|
|
429
|
+
else:
|
|
430
|
+
return [round(bin, rounding) for bin in bins]
|
|
431
|
+
|
|
432
|
+
def _remove_max_legend_value(self):
|
|
433
|
+
if not self._legend:
|
|
434
|
+
raise ValueError("Cannot modify legend before it is created.")
|
|
435
|
+
|
|
436
|
+
def _prepare_continous_legend(
|
|
437
|
+
self,
|
|
438
|
+
bins: list[float],
|
|
439
|
+
colors: list[str],
|
|
440
|
+
nan_label: str,
|
|
441
|
+
bin_values: dict,
|
|
442
|
+
):
|
|
443
|
+
# TODO: clean up this messy method
|
|
444
|
+
|
|
445
|
+
for attr in self.__dict__.keys():
|
|
446
|
+
if attr in self.kwargs:
|
|
447
|
+
self[attr] = self.kwargs.pop(attr)
|
|
448
|
+
|
|
449
|
+
self._patches, self._categories = [], []
|
|
450
|
+
|
|
451
|
+
for color in colors:
|
|
452
|
+
self._patches.append(
|
|
453
|
+
Line2D(
|
|
454
|
+
[0],
|
|
455
|
+
[0],
|
|
456
|
+
linestyle="none",
|
|
457
|
+
marker="o",
|
|
458
|
+
alpha=self.kwargs.get("alpha", 1),
|
|
459
|
+
markersize=self._markersize,
|
|
460
|
+
markerfacecolor=color,
|
|
461
|
+
markeredgewidth=0,
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
if self.labels:
|
|
466
|
+
if len(self.labels) != len(colors):
|
|
467
|
+
raise ValueError(
|
|
468
|
+
"Label list must be same length as the number of groups. "
|
|
469
|
+
f"Got k={len(colors)} and labels={len(colors)}."
|
|
470
|
+
f"labels: {', '.join(self.labels)}"
|
|
471
|
+
f"colors: {', '.join(colors)}"
|
|
472
|
+
)
|
|
473
|
+
self._categories = self.labels
|
|
474
|
+
|
|
475
|
+
elif len(bins) == len(colors):
|
|
476
|
+
for i, _ in enumerate(bins):
|
|
477
|
+
min_ = np.min(bin_values[i])
|
|
478
|
+
max_ = np.max(bin_values[i])
|
|
479
|
+
min_rounded = self._set_rounding([min_], self._rounding)[0]
|
|
480
|
+
max_rounded = self._set_rounding([max_], self._rounding)[0]
|
|
481
|
+
if min_ == max_:
|
|
482
|
+
self._categories.append(f"{min_rounded} {self.label_suffix}")
|
|
483
|
+
else:
|
|
484
|
+
self._categories.append(
|
|
485
|
+
f"{min_rounded} {self.label_suffix} {self.label_sep} "
|
|
486
|
+
f"{max_rounded} {self.label_suffix}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
else:
|
|
490
|
+
for i, (cat1, cat2) in enumerate(zip(bins[:-1], bins[1:], strict=True)):
|
|
491
|
+
if nan_label in str(cat1) or nan_label in str(cat2):
|
|
492
|
+
self._categories.append(nan_label)
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
min_ = np.min(bin_values[i])
|
|
496
|
+
max_ = np.max(bin_values[i])
|
|
497
|
+
min_rounded = self._set_rounding([min_], self._rounding)[0]
|
|
498
|
+
max_rounded = self._set_rounding([max_], self._rounding)[0]
|
|
499
|
+
if self.pretty_labels:
|
|
500
|
+
if i != 0 and self._rounding == 0:
|
|
501
|
+
cat1 = int(cat1 + 1)
|
|
502
|
+
elif i != 0:
|
|
503
|
+
cat1 = cat1 + float(f"1e-{self._rounding}")
|
|
504
|
+
|
|
505
|
+
cat1 = self._format_number(cat1)
|
|
506
|
+
cat2 = self._format_number(cat2)
|
|
507
|
+
|
|
508
|
+
if min_ == max_:
|
|
509
|
+
label = self._two_value_label(cat1, cat2)
|
|
510
|
+
self._categories.append(label)
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
label = self._two_value_label(cat1, cat2)
|
|
514
|
+
self._categories.append(label)
|
|
515
|
+
|
|
516
|
+
elif min_ == max_:
|
|
517
|
+
min_rounded = self._format_number(min_rounded)
|
|
518
|
+
label = self._one_value_label(min_rounded)
|
|
519
|
+
self._categories.append(label)
|
|
520
|
+
else:
|
|
521
|
+
min_rounded = self._format_number(min_rounded)
|
|
522
|
+
max_rounded = self._format_number(max_rounded)
|
|
523
|
+
label = self._two_value_label(min_rounded, max_rounded)
|
|
524
|
+
self._categories.append(label)
|
|
525
|
+
|
|
526
|
+
def _actually_add_legend(self, ax):
|
|
527
|
+
legend = ax.legend(
|
|
528
|
+
self._patches,
|
|
529
|
+
self._categories,
|
|
530
|
+
fontsize=self._fontsize,
|
|
531
|
+
title=self.title,
|
|
532
|
+
title_fontsize=self._title_fontsize,
|
|
533
|
+
bbox_to_anchor=self._position + (self.width, self.height),
|
|
534
|
+
fancybox=False,
|
|
535
|
+
framealpha=self.framealpha,
|
|
536
|
+
edgecolor=self.edgecolor,
|
|
537
|
+
labelspacing=self.labelspacing,
|
|
538
|
+
**self.kwargs,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
if self.title_color:
|
|
542
|
+
plt.setp(legend.get_title(), color=self.title_color)
|
|
543
|
+
|
|
544
|
+
return ax
|
|
545
|
+
|
|
546
|
+
def _two_value_label(self, value1, value2):
|
|
547
|
+
return (
|
|
548
|
+
f"{value1} {self.label_suffix} {self.label_sep} "
|
|
549
|
+
f"{value2} {self.label_suffix}"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def _one_value_label(self, value1):
|
|
553
|
+
return f"{value1} {self.label_suffix}"
|
|
554
|
+
|
|
555
|
+
def _format_number(self, number):
|
|
556
|
+
if not self.thousand_sep and not self.decimal_mark:
|
|
557
|
+
return number
|
|
558
|
+
|
|
559
|
+
if self.thousand_sep:
|
|
560
|
+
number = f"{number:,}".replace(",", "*temp_thousand*")
|
|
561
|
+
|
|
562
|
+
if self.decimal_mark:
|
|
563
|
+
number = str(number).replace(".", "*temp_decimal*")
|
|
564
|
+
|
|
565
|
+
if self.thousand_sep == "." and not self.decimal_mark:
|
|
566
|
+
number = number.replace(".", ",").replace(
|
|
567
|
+
"*temp_thousand*", self.thousand_sep
|
|
568
|
+
)
|
|
569
|
+
elif not self.thousand_sep:
|
|
570
|
+
number = number.replace("*temp_decimal*", self.decimal_mark)
|
|
571
|
+
elif not self.decimal_mark:
|
|
572
|
+
number = number.replace("*temp_thousand*", self.thousand_sep)
|
|
573
|
+
else:
|
|
574
|
+
number = number.replace("*temp_thousand*", self.thousand_sep).replace(
|
|
575
|
+
"*temp_decimal*", self.decimal_mark
|
|
576
|
+
)
|
|
577
|
+
return number
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def rounding(self):
|
|
581
|
+
return self._rounding
|
|
582
|
+
|
|
583
|
+
@rounding.setter
|
|
584
|
+
def rounding(self, new_value: bool):
|
|
585
|
+
self._rounding = new_value
|
|
586
|
+
self._rounding_has_been_set = True
|