ssb-sgis 1.0.1__py3-none-any.whl → 1.0.3__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.
Files changed (60) hide show
  1. sgis/__init__.py +107 -121
  2. sgis/exceptions.py +5 -3
  3. sgis/geopandas_tools/__init__.py +1 -0
  4. sgis/geopandas_tools/bounds.py +86 -47
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
  6. sgis/geopandas_tools/centerlines.py +53 -44
  7. sgis/geopandas_tools/cleaning.py +87 -104
  8. sgis/geopandas_tools/conversion.py +164 -107
  9. sgis/geopandas_tools/duplicates.py +33 -19
  10. sgis/geopandas_tools/general.py +84 -52
  11. sgis/geopandas_tools/geometry_types.py +24 -10
  12. sgis/geopandas_tools/neighbors.py +23 -11
  13. sgis/geopandas_tools/overlay.py +136 -53
  14. sgis/geopandas_tools/point_operations.py +11 -10
  15. sgis/geopandas_tools/polygon_operations.py +53 -61
  16. sgis/geopandas_tools/polygons_as_rings.py +121 -78
  17. sgis/geopandas_tools/sfilter.py +17 -17
  18. sgis/helpers.py +116 -58
  19. sgis/io/dapla_functions.py +32 -23
  20. sgis/io/opener.py +13 -6
  21. sgis/io/read_parquet.py +2 -2
  22. sgis/maps/examine.py +55 -28
  23. sgis/maps/explore.py +471 -112
  24. sgis/maps/httpserver.py +12 -12
  25. sgis/maps/legend.py +285 -134
  26. sgis/maps/map.py +248 -129
  27. sgis/maps/maps.py +123 -119
  28. sgis/maps/thematicmap.py +260 -94
  29. sgis/maps/tilesources.py +3 -8
  30. sgis/networkanalysis/_get_route.py +5 -4
  31. sgis/networkanalysis/_od_cost_matrix.py +44 -1
  32. sgis/networkanalysis/_points.py +10 -4
  33. sgis/networkanalysis/_service_area.py +5 -2
  34. sgis/networkanalysis/closing_network_holes.py +22 -64
  35. sgis/networkanalysis/cutting_lines.py +58 -46
  36. sgis/networkanalysis/directednetwork.py +16 -8
  37. sgis/networkanalysis/finding_isolated_networks.py +6 -5
  38. sgis/networkanalysis/network.py +15 -13
  39. sgis/networkanalysis/networkanalysis.py +79 -61
  40. sgis/networkanalysis/networkanalysisrules.py +21 -17
  41. sgis/networkanalysis/nodes.py +2 -3
  42. sgis/networkanalysis/traveling_salesman.py +6 -3
  43. sgis/parallel/parallel.py +372 -142
  44. sgis/raster/base.py +9 -3
  45. sgis/raster/cube.py +331 -213
  46. sgis/raster/cubebase.py +15 -29
  47. sgis/raster/image_collection.py +2560 -0
  48. sgis/raster/indices.py +17 -12
  49. sgis/raster/raster.py +356 -275
  50. sgis/raster/sentinel_config.py +104 -0
  51. sgis/raster/zonal.py +38 -14
  52. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
  53. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
  54. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  55. {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
  56. sgis/raster/bands.py +0 -48
  57. sgis/raster/gradient.py +0 -78
  58. sgis/raster/methods_as_functions.py +0 -124
  59. sgis/raster/torchgeo.py +0 -150
  60. ssb_sgis-1.0.1.dist-info/RECORD +0 -63
sgis/maps/legend.py CHANGED
@@ -4,18 +4,21 @@ The Legend class is best accessed through the 'legend' attribute of the Thematic
4
4
  class.
5
5
 
6
6
  """
7
+
8
+ import itertools
7
9
  import warnings
10
+ from typing import Any
8
11
 
9
12
  import matplotlib
10
13
  import matplotlib.pyplot as plt
11
14
  import numpy as np
12
15
  import pandas as pd
16
+ from geopandas import GeoDataFrame
13
17
  from matplotlib.lines import Line2D
14
18
  from pandas import Series
15
19
 
16
20
  from ..geopandas_tools.bounds import points_in_bounds
17
21
 
18
-
19
22
  # the geopandas._explore raises a deprication warning. Ignoring for now.
20
23
  warnings.filterwarnings(
21
24
  action="ignore", category=matplotlib.MatplotlibDeprecationWarning
@@ -23,6 +26,82 @@ warnings.filterwarnings(
23
26
  pd.options.mode.chained_assignment = None
24
27
 
25
28
 
29
+ LEGEND_KWARGS = {
30
+ "title",
31
+ "size",
32
+ "position",
33
+ "fontsize",
34
+ "title_fontsize",
35
+ "markersize",
36
+ "framealpha",
37
+ "edgecolor",
38
+ "kwargs",
39
+ "labelspacing",
40
+ "title_color",
41
+ "width",
42
+ "height",
43
+ "labels",
44
+ "pretty_labels",
45
+ "thousand_sep",
46
+ "decimal_mark",
47
+ "label_sep",
48
+ "label_suffix",
49
+ "rounding",
50
+ "facecolor",
51
+ "labelcolor",
52
+ }
53
+
54
+ LOWERCASE_WORDS = {
55
+ "a",
56
+ "an",
57
+ "and",
58
+ "as",
59
+ "at",
60
+ "but",
61
+ "by",
62
+ "for",
63
+ "in",
64
+ "nor",
65
+ "of",
66
+ "on",
67
+ "or",
68
+ "the",
69
+ "up",
70
+ }
71
+
72
+
73
+ def prettify_label(label: str) -> str:
74
+ """Replace underscores with spaces and capitalize words that are all lowecase."""
75
+ return " ".join(
76
+ word.title() if word.islower() and word not in LOWERCASE_WORDS else word
77
+ for word in label.replace("_", " ").split()
78
+ )
79
+
80
+
81
+ def prettify_number(x: int | float, rounding: int) -> int:
82
+ rounding = int(float(f"1e+{abs(rounding)}"))
83
+ rounded_down = int(x // rounding * rounding)
84
+ rounded_up = rounded_down + rounding
85
+ diff_up = abs(x - rounded_up)
86
+ diff_down = abs(x - rounded_down)
87
+ if diff_up < diff_down:
88
+ return rounded_up
89
+ else:
90
+ return rounded_down
91
+
92
+
93
+ def prettify_bins(bins: list[int | float], rounding: int) -> list[int]:
94
+ return [
95
+ (
96
+ prettify_number(x, rounding)
97
+ if i != len(bins) - 1
98
+ else int(x)
99
+ # else prettify_number(x, rounding) + abs(rounding)
100
+ )
101
+ for i, x in enumerate(bins)
102
+ ]
103
+
104
+
26
105
  class Legend:
27
106
  """Holds the general attributes of the legend in the ThematicMap class.
28
107
 
@@ -33,78 +112,65 @@ class Legend:
33
112
  If a numeric column is used, additional attributes can be found in the
34
113
  ContinousLegend class.
35
114
 
36
- Attributes:
37
- title: Legend title. Defaults to the column name if used in the
38
- ThematicMap class.
39
- position: The legend's x and y position in the plot, specified as a tuple of
40
- x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
41
- in the bottom right corner, (0.2, 0.8) for the upper left corner.
42
- fontsize: Text size of the legend labels. Defaults to the size of
43
- the ThematicMap class.
44
- title_fontsize: Text size of the legend title. Defaults to the
45
- size * 1.2 of the ThematicMap class.
46
- markersize: Size of the color circles in the legend. Defaults to the size of
47
- the ThematicMap class.
48
- framealpha: Transparency of the legend background.
49
- edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
50
- kwargs: Stores additional keyword arguments taken by the matplotlib legend
51
- method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
52
- 'm' is the name of the ThematicMap instance. See here:
53
- https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
54
-
55
- Examples
56
- --------
57
- Create ten random points with a numeric column from 0 to 9.
115
+ Examples:
116
+ ---------
117
+ Create ten points with a numeric column from 0 to 9.
58
118
 
59
119
  >>> import sgis as sg
60
- >>> points = sg.random_points(10)
120
+ >>> points = sg.to_gdf(
121
+ ... [
122
+ ... (0, 1),
123
+ ... (1, 0),
124
+ ... (1, 1),
125
+ ... (0, 0),
126
+ ... (0.5, 0.5),
127
+ ... (0.5, 0.25),
128
+ ... (0.25, 0.25),
129
+ ... (0.75, 0.75),
130
+ ... (0.25, 0.75),
131
+ ... (0.75, 0.25),
132
+ ... ]
133
+ ... )
61
134
  >>> points["number"] = range(10)
62
135
  >>> points
63
- geometry number
64
- 0 POINT (0.59780 0.50425) 0
65
- 1 POINT (0.07019 0.26167) 1
66
- 2 POINT (0.56475 0.15422) 2
67
- 3 POINT (0.87293 0.60316) 3
68
- 4 POINT (0.47373 0.20040) 4
69
- 5 POINT (0.98661 0.15614) 5
70
- 6 POINT (0.30951 0.77057) 6
71
- 7 POINT (0.47802 0.52824) 7
72
- 8 POINT (0.12215 0.96588) 8
73
- 9 POINT (0.02938 0.93467) 9
136
+ geometry number
137
+ 0 POINT (0.00000 1.00000) 0
138
+ 1 POINT (1.00000 0.00000) 1
139
+ 2 POINT (1.00000 1.00000) 2
140
+ 3 POINT (0.00000 0.00000) 3
141
+ 4 POINT (0.50000 0.50000) 4
142
+ 5 POINT (0.50000 0.25000) 5
143
+ 6 POINT (0.25000 0.25000) 6
144
+ 7 POINT (0.75000 0.75000) 7
145
+ 8 POINT (0.25000 0.75000) 8
146
+ 9 POINT (0.75000 0.25000) 9
74
147
 
75
148
  Creating the ThematicMap instance will also create the legend. Since we
76
- specify a numeric column, a ContinousLegend instance is created.
77
-
78
- >>> m = sg.ThematicMap(points, column="number")
149
+ pass a numeric column, a ContinousLegend is created.
150
+
151
+ >>> m = sg.ThematicMap(
152
+ ... points,
153
+ ... column="number"
154
+ ... legend_kwargs=dict(
155
+ ... title="Meters",
156
+ ... label_sep="to",
157
+ ... label_suffix="num",
158
+ ... rounding=2,
159
+ ... position = (0.35, 0.28),
160
+ ... title_fontsize=11,
161
+ ... fontsize=9,
162
+ ... markersize=7.5,
163
+ ... ),
164
+ ... )
165
+ >>> m.plot()
79
166
  >>> m.legend
80
167
  <sgis.maps.legend.ContinousLegend object at 0x00000222206738D0>
81
-
82
- Changing the attributes that apply to both numeric and categorical columns.
83
-
84
- >>> m.legend.title = "Meters"
85
- >>> m.legend.title_fontsize = 11
86
- >>> m.legend.fontsize = 9
87
- >>> m.legend.markersize = 7.5
88
- >>> m.legend.position = (0.35, 0.28)
89
- >>> m.plot()
90
-
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.
97
-
98
- >>> m.label_sep = "to"
99
- >>> m.label_suffix = "num"
100
- >>> m.rounding = 2
101
- >>> m.plot()
102
-
103
168
  """
104
169
 
105
170
  def __init__(
106
171
  self,
107
172
  title: str | None = None,
173
+ pretty_labels: bool = True,
108
174
  labels: list[str] | None = None,
109
175
  position: tuple[float] | None = None,
110
176
  markersize: int | None = None,
@@ -113,7 +179,32 @@ class Legend:
113
179
  framealpha: float = 1.0,
114
180
  edgecolor: str = "#0f0f0f",
115
181
  **kwargs,
116
- ):
182
+ ) -> None:
183
+ """Initialiser.
184
+
185
+ Args:
186
+ title: Legend title. Defaults to the column name if used in the
187
+ ThematicMap class.
188
+ pretty_labels: If True, words will be capitalized and underscores turned to spaces.
189
+ If continous values, numbers will be rounded.
190
+ labels: Labels of the categories.
191
+ position: The legend's x and y position in the plot, specified as a tuple of
192
+ x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
193
+ in the bottom right corner, (0.2, 0.8) for the upper left corner.
194
+ fontsize: Text size of the legend labels. Defaults to the size of
195
+ the ThematicMap class.
196
+ title_fontsize: Text size of the legend title. Defaults to the
197
+ size * 1.2 of the ThematicMap class.
198
+ markersize: Size of the color circles in the legend. Defaults to the size of
199
+ the ThematicMap class.
200
+ framealpha: Transparency of the legend background.
201
+ edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
202
+ kwargs: Stores additional keyword arguments taken by the matplotlib legend
203
+ method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
204
+ 'm' is the name of the ThematicMap instance. See here:
205
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
206
+
207
+ """
117
208
  self.title = title
118
209
 
119
210
  if "size" in kwargs:
@@ -124,6 +215,7 @@ class Legend:
124
215
  self._fontsize = fontsize
125
216
  self._markersize = markersize
126
217
 
218
+ self.pretty_labels = pretty_labels
127
219
  self.framealpha = framealpha
128
220
  self.edgecolor = edgecolor
129
221
  self.width = kwargs.pop("width", 0.1)
@@ -133,12 +225,25 @@ class Legend:
133
225
 
134
226
  self.labels = labels
135
227
  self._position = position
136
- self.kwargs = kwargs
137
228
  self._position_has_been_set = True if position else False
138
229
 
139
- def _get_legend_sizes(self, size, kwargs):
140
- """Adjust fontsize and markersize to size kwarg."""
230
+ self.kwargs = {}
231
+ for key, value in kwargs.items():
232
+ if key not in LEGEND_KWARGS:
233
+ self.kwargs[key] = value
234
+ else:
235
+ try:
236
+ setattr(self, key, value)
237
+ except Exception:
238
+ setattr(self, f"_{key}", value)
141
239
 
240
+ @property
241
+ def valid_keywords(self) -> set[str]:
242
+ """List all valid keywords for the class initialiser."""
243
+ return LEGEND_KWARGS
244
+
245
+ def _get_legend_sizes(self, size: int | float, kwargs: dict) -> None:
246
+ """Adjust fontsize and markersize to size kwarg."""
142
247
  if "title_fontsize" in kwargs:
143
248
  self._title_fontsize = kwargs["title_fontsize"]
144
249
  self._title_fontsize_has_been_set = True
@@ -157,7 +262,9 @@ class Legend:
157
262
  else:
158
263
  self._markersize = size
159
264
 
160
- def _prepare_categorical_legend(self, categories_colors: dict, nan_label: str):
265
+ def _prepare_categorical_legend(
266
+ self, categories_colors: dict, nan_label: str
267
+ ) -> None:
161
268
  for attr in self.__dict__.keys():
162
269
  if attr in self.kwargs:
163
270
  self[attr] = self.kwargs.pop(attr)
@@ -178,6 +285,8 @@ class Legend:
178
285
 
179
286
  self._patches, self._categories = [], []
180
287
  for category, color in categories_colors.items():
288
+ if self.pretty_labels:
289
+ category = prettify_label(category)
181
290
  if category == nan_label:
182
291
  self._categories.append(nan_label)
183
292
  else:
@@ -195,7 +304,9 @@ class Legend:
195
304
  )
196
305
  )
197
306
 
198
- def _actually_add_legend(self, ax):
307
+ def _actually_add_legend(self, ax: matplotlib.axes.Axes) -> matplotlib.axes.Axes:
308
+ if self.pretty_labels:
309
+ self.title = prettify_label(self.title)
199
310
  legend = ax.legend(
200
311
  self._patches,
201
312
  self._categories,
@@ -215,7 +326,9 @@ class Legend:
215
326
 
216
327
  return ax
217
328
 
218
- def _get_best_legend_position(self, gdf, k: int):
329
+ def _get_best_legend_position(
330
+ self, gdf: GeoDataFrame, k: int
331
+ ) -> tuple[float, float]:
219
332
  minx, miny, maxx, maxy = gdf.total_bounds
220
333
  diffx = maxx - minx
221
334
  diffy = maxy - miny
@@ -243,51 +356,58 @@ class Legend:
243
356
 
244
357
  return bestx_01, besty_01
245
358
 
246
- def __getitem__(self, item):
359
+ def __getitem__(self, item: str) -> Any:
360
+ """Get attribute with square brackets."""
247
361
  return getattr(self, item)
248
362
 
249
- def __setitem__(self, key, value):
363
+ def __setitem__(self, key: str, value: Any) -> None:
364
+ """Set attribute with square brackets."""
250
365
  setattr(self, key, value)
251
366
 
252
- def get(self, key, default=None):
367
+ def get(self, key: Any, default: Any = None) -> Any:
368
+ """Get value of an attribute of the Legend."""
253
369
  try:
254
370
  return self[key]
255
371
  except (KeyError, ValueError, IndexError, AttributeError):
256
372
  return default
257
373
 
258
374
  @property
259
- def position(self):
375
+ def position(self) -> tuple[float, float]:
376
+ """Legend position in x, y."""
260
377
  return self._position
261
378
 
262
379
  @position.setter
263
- def position(self, new_value: bool):
380
+ def position(self, new_value: tuple[float, float]) -> None:
264
381
  self._position = new_value
265
382
  self._position_has_been_set = True
266
383
 
267
384
  @property
268
- def title_fontsize(self):
385
+ def title_fontsize(self) -> int:
386
+ """Legend title fontsize."""
269
387
  return self._title_fontsize
270
388
 
271
389
  @title_fontsize.setter
272
- def title_fontsize(self, new_value: bool):
390
+ def title_fontsize(self, new_value: int) -> None:
273
391
  self._title_fontsize = new_value
274
392
  self._title_fontsize_has_been_set = True
275
393
 
276
394
  @property
277
- def fontsize(self):
395
+ def fontsize(self) -> int:
396
+ """Legend fontsize."""
278
397
  return self._fontsize
279
398
 
280
399
  @fontsize.setter
281
- def fontsize(self, new_value: bool):
400
+ def fontsize(self, new_value: int) -> None:
282
401
  self._fontsize = new_value
283
402
  self._fontsize_has_been_set = True
284
403
 
285
404
  @property
286
- def markersize(self):
405
+ def markersize(self) -> int:
406
+ """Legend markersize."""
287
407
  return self._markersize
288
408
 
289
409
  @markersize.setter
290
- def markersize(self, new_value: bool):
410
+ def markersize(self, new_value: int) -> None:
291
411
  self._markersize = new_value
292
412
  self._markersize_has_been_set = True
293
413
 
@@ -311,18 +431,14 @@ class ContinousLegend(Legend):
311
431
  Defaults to None.
312
432
  label_sep: Text to put in between the two numbers in each color group in
313
433
  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
434
  thousand_sep: Separator between each thousand for large numbers. Defaults to
319
435
  None, meaning no separator.
320
436
  decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
321
437
  unless 'thousand_sep' is '.'. In this case, ',' (comma) will be used as
322
438
  decimal mark.
323
439
 
324
- Examples
325
- --------
440
+ Examples:
441
+ ---------
326
442
  Create ten random points with a numeric column from 0 to 9.
327
443
 
328
444
  >>> import sgis as sg
@@ -379,14 +495,43 @@ class ContinousLegend(Legend):
379
495
  def __init__(
380
496
  self,
381
497
  labels: list[str] | None = None,
382
- pretty_labels: bool = False,
498
+ pretty_labels: bool = True,
383
499
  label_suffix: str | None = None,
384
500
  label_sep: str = "-",
385
501
  rounding: int | None = None,
386
502
  thousand_sep: str | None = None,
387
503
  decimal_mark: str | None = None,
388
504
  **kwargs,
389
- ):
505
+ ) -> None:
506
+ """Initialiser.
507
+
508
+ Args:
509
+ labels: To manually set labels. If set, all other labeling attributes are
510
+ ignored. Should be given as a list of strings with the same length as
511
+ the number of color groups.
512
+ pretty_labels: If False (default), the minimum and maximum values of each
513
+ color group will be used as legend labels. If True, the labels will end
514
+ with the maximum value, but start at 1 + the maximum value of the previous
515
+ group. The labels will be correct but inaccurate.
516
+ label_suffix: The text to put after each number in the legend labels.
517
+ Defaults to None.
518
+ label_sep: Text to put in between the two numbers in each color group in
519
+ the legend. Defaults to '-'.
520
+ rounding: Number of decimals in the labels. By default, the rounding
521
+ depends on the column's maximum value and standard deviation.
522
+ OBS: The bins will not be rounded, meaning the labels might be wrong
523
+ if not bins are set manually.
524
+ thousand_sep: Separator between each thousand for large numbers. Defaults to
525
+ None, meaning no separator.
526
+ decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
527
+ unless 'thousand_sep' is '.'. In this case, ',' (comma) will be used as
528
+ decimal mark.
529
+ kwargs: Stores additional keyword arguments taken by the matplotlib legend
530
+ method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
531
+ 'm' is the name of the ThematicMap instance. See here:
532
+ https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
533
+
534
+ """
390
535
  super().__init__(**kwargs)
391
536
 
392
537
  self.pretty_labels = pretty_labels
@@ -396,7 +541,7 @@ class ContinousLegend(Legend):
396
541
  self.label_sep = label_sep
397
542
  self.label_suffix = "" if not label_suffix else label_suffix
398
543
  self._rounding = rounding
399
- self._rounding_has_been_set = True if rounding else False
544
+ # self._rounding_has_been_set = True if rounding else False
400
545
 
401
546
  def _get_rounding(self, array: Series | np.ndarray) -> int:
402
547
  def isinteger(x):
@@ -423,13 +568,15 @@ class ContinousLegend(Legend):
423
568
  return int(abs(np.log10(std_))) + 1
424
569
 
425
570
  @staticmethod
426
- def _set_rounding(bins, rounding: int | float):
427
- if rounding == 0:
428
- return [int(round(bin, 0)) for bin in bins]
571
+ def _set_rounding(bins, rounding: int | float) -> list[int | float]:
572
+ if not rounding:
573
+ return [int(round(bin_, 0)) for bin_ in bins]
574
+ elif rounding <= 0:
575
+ return [int(round(bin_, rounding)) for bin_ in bins]
429
576
  else:
430
- return [round(bin, rounding) for bin in bins]
577
+ return [round(bin_, rounding) for bin_ in bins]
431
578
 
432
- def _remove_max_legend_value(self):
579
+ def _remove_max_legend_value(self) -> None:
433
580
  if not self._legend:
434
581
  raise ValueError("Cannot modify legend before it is created.")
435
582
 
@@ -439,11 +586,11 @@ class ContinousLegend(Legend):
439
586
  colors: list[str],
440
587
  nan_label: str,
441
588
  bin_values: dict,
442
- ):
589
+ ) -> None:
443
590
  # TODO: clean up this messy method
444
591
 
445
- for attr in self.__dict__.keys():
446
- if attr in self.kwargs:
592
+ for attr in self.kwargs:
593
+ if attr in self.__dict__:
447
594
  self[attr] = self.kwargs.pop(attr)
448
595
 
449
596
  self._patches, self._categories = [], []
@@ -466,9 +613,10 @@ class ContinousLegend(Legend):
466
613
  if len(self.labels) != len(colors):
467
614
  raise ValueError(
468
615
  "Label list must be same length as the number of groups. "
469
- f"Got k={len(colors)} and labels={len(colors)}."
616
+ f"Got k={len(colors)} and labels={len(self.labels)}."
470
617
  f"labels: {', '.join(self.labels)}"
471
618
  f"colors: {', '.join(colors)}"
619
+ f"bins: {bins}"
472
620
  )
473
621
  self._categories = self.labels
474
622
 
@@ -491,72 +639,69 @@ class ContinousLegend(Legend):
491
639
  )
492
640
 
493
641
  else:
494
- for i, (cat1, cat2) in enumerate(zip(bins[:-1], bins[1:], strict=True)):
642
+ for i, (cat1, cat2) in enumerate(itertools.pairwise(bins)):
495
643
  if nan_label in str(cat1) or nan_label in str(cat2):
496
644
  self._categories.append(nan_label)
497
645
  continue
498
646
 
499
647
  min_ = np.min(bin_values[i])
500
648
  max_ = np.max(bin_values[i])
501
- min_rounded = self._set_rounding([min_], self._rounding)[0]
502
- max_rounded = self._set_rounding([max_], self._rounding)[0]
649
+
503
650
  if self.pretty_labels:
504
- if i != 0 and self._rounding == 0:
505
- cat1 = int(cat1 + 1)
506
- elif i != 0:
507
- cat1 = cat1 + float(f"1e-{self._rounding}")
651
+ if i == 0:
652
+ cat1 = int(min_) if (self.rounding or 0) <= 0 else min_
653
+
654
+ is_last = i == len(bins) - 2
655
+ if is_last:
656
+ cat2 = int(max_) if (self.rounding or 0) <= 0 else max_
657
+
658
+ if (self.rounding or 0) <= 0:
659
+ cat1 = int(cat1)
660
+ cat2 = int(cat2 - 1) if not is_last else int(cat2)
661
+ elif (self.rounding or 0) > 0:
662
+ cat1 = round(cat1, self._rounding)
663
+ cat2 = round(
664
+ cat2 - float(f"1e-{self._rounding}"), self._rounding
665
+ )
666
+ else:
667
+ cat1 = round(cat1, self._rounding)
668
+ cat2 = round(cat2, self._rounding)
508
669
 
509
670
  cat1 = self._format_number(cat1)
510
671
  cat2 = self._format_number(cat2)
511
672
 
512
673
  if min_ == max_:
513
- label = self._two_value_label(cat1, cat2)
674
+ label = self._get_two_value_label(cat1, cat2)
514
675
  self._categories.append(label)
515
676
  continue
516
677
 
517
- label = self._two_value_label(cat1, cat2)
678
+ label = self._get_two_value_label(cat1, cat2)
518
679
  self._categories.append(label)
519
680
 
520
- elif min_ == max_:
681
+ continue
682
+
683
+ min_rounded = self._set_rounding([min_], self._rounding)[0]
684
+ max_rounded = self._set_rounding([max_], self._rounding)[0]
685
+ if min_ == max_:
521
686
  min_rounded = self._format_number(min_rounded)
522
- label = self._one_value_label(min_rounded)
687
+ label = self._get_one_value_label(min_rounded)
523
688
  self._categories.append(label)
524
689
  else:
525
690
  min_rounded = self._format_number(min_rounded)
526
691
  max_rounded = self._format_number(max_rounded)
527
- label = self._two_value_label(min_rounded, max_rounded)
692
+ label = self._get_two_value_label(min_rounded, max_rounded)
528
693
  self._categories.append(label)
529
694
 
530
- def _actually_add_legend(self, ax):
531
- legend = ax.legend(
532
- self._patches,
533
- self._categories,
534
- fontsize=self._fontsize,
535
- title=self.title,
536
- title_fontsize=self._title_fontsize,
537
- bbox_to_anchor=self._position + (self.width, self.height),
538
- fancybox=False,
539
- framealpha=self.framealpha,
540
- edgecolor=self.edgecolor,
541
- labelspacing=self.labelspacing,
542
- **self.kwargs,
543
- )
544
-
545
- if self.title_color:
546
- plt.setp(legend.get_title(), color=self.title_color)
547
-
548
- return ax
549
-
550
- def _two_value_label(self, value1, value2):
695
+ def _get_two_value_label(self, value1: int | float, value2: int | float) -> str:
551
696
  return (
552
697
  f"{value1} {self.label_suffix} {self.label_sep} "
553
698
  f"{value2} {self.label_suffix}"
554
699
  )
555
700
 
556
- def _one_value_label(self, value1):
701
+ def _get_one_value_label(self, value1: int | float) -> str:
557
702
  return f"{value1} {self.label_suffix}"
558
703
 
559
- def _format_number(self, number):
704
+ def _format_number(self, number: int | float) -> int | float:
560
705
  if not self.thousand_sep and not self.decimal_mark:
561
706
  return number
562
707
 
@@ -581,10 +726,16 @@ class ContinousLegend(Legend):
581
726
  return number
582
727
 
583
728
  @property
584
- def rounding(self):
729
+ def rounding(self) -> int:
730
+ """Number of decimals in the labels.
731
+
732
+ By default, the rounding
733
+ depends on the column's maximum value and standard deviation.
734
+ OBS: The bins will not be rounded, meaning the labels might be wrong
735
+ if not bins are set manually.
736
+ """
585
737
  return self._rounding
586
738
 
587
739
  @rounding.setter
588
- def rounding(self, new_value: bool):
740
+ def rounding(self, new_value: int) -> None:
589
741
  self._rounding = new_value
590
- self._rounding_has_been_set = True