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/map.py CHANGED
@@ -4,32 +4,37 @@ This module holds the Map class, which is the basis for the Explore class.
4
4
  """
5
5
 
6
6
  import warnings
7
+ from collections.abc import Sequence
8
+ from statistics import mean
9
+ from typing import Any
7
10
 
8
11
  import matplotlib
9
12
  import matplotlib.colors as colors
10
13
  import numpy as np
11
14
  import pandas as pd
12
- from geopandas import GeoDataFrame, GeoSeries
15
+ from geopandas import GeoDataFrame
16
+ from geopandas import GeoSeries
13
17
  from jenkspy import jenks_breaks
14
18
  from mapclassify import classify
15
19
  from shapely import Geometry
16
20
 
17
21
  from ..geopandas_tools.conversion import to_gdf
18
- from ..geopandas_tools.general import (
19
- clean_geoms,
20
- drop_inactive_geometry_columns,
21
- get_common_crs,
22
- rename_geometry_if,
23
- )
22
+ from ..geopandas_tools.general import _rename_geometry_if
23
+ from ..geopandas_tools.general import clean_geoms
24
+ from ..geopandas_tools.general import drop_inactive_geometry_columns
25
+ from ..geopandas_tools.general import get_common_crs
24
26
  from ..helpers import get_object_name
25
-
27
+ from ..helpers import unit_is_meters
28
+ from ..raster.image_collection import Band
29
+ from ..raster.image_collection import Image
30
+ from ..raster.image_collection import ImageCollection
26
31
 
27
32
  try:
28
33
  from torchgeo.datasets.geo import RasterDataset
29
34
  except ImportError:
30
35
 
31
36
  class RasterDataset:
32
- """Placeholder"""
37
+ """Placeholder."""
33
38
 
34
39
 
35
40
  # the geopandas._explore raises a deprication warning. Ignoring for now.
@@ -60,8 +65,16 @@ _CATEGORICAL_CMAP = {
60
65
  DEFAULT_SCHEME = "quantiles"
61
66
 
62
67
 
63
- def proper_fillna(val, fill_val):
64
- """fillna doesn't always work. So doing it manually."""
68
+ def proper_fillna(val: Any, fill_val: Any) -> Any:
69
+ """Manually handle missing values when fillna doesn't work as expected.
70
+
71
+ Args:
72
+ val: The value to check and fill.
73
+ fill_val: The value to fill in.
74
+
75
+ Returns:
76
+ The original value or the filled value if conditions are met.
77
+ """
65
78
  try:
66
79
  if "NAType" in val.__class__.__name__:
67
80
  return fill_val
@@ -84,15 +97,28 @@ class Map:
84
97
  self,
85
98
  *gdfs: GeoDataFrame,
86
99
  column: str | None = None,
87
- labels: tuple[str] | None = None,
88
100
  k: int = 5,
89
101
  bins: tuple[float] | None = None,
90
102
  nan_label: str = "Missing",
91
103
  nan_color="#c2c2c2",
92
104
  scheme: str = DEFAULT_SCHEME,
105
+ cmap: str | None = None,
93
106
  **kwargs,
94
- ):
107
+ ) -> None:
108
+ """Initialiser.
95
109
 
110
+ Args:
111
+ *gdfs: Variable length GeoDataFrame list.
112
+ column: The column name to work with.
113
+ k: Number of bins or classes for classification (default: 5).
114
+ bins: Predefined bins for data classification.
115
+ nan_label: Label for missing data.
116
+ nan_color: Color for missing data.
117
+ scheme: Classification scheme to be used.
118
+ cmap (str): Colormap of the plot. See:
119
+ https://matplotlib.org/stable/tutorials/colors/colormaps.html
120
+ **kwargs: Arbitrary keyword arguments.
121
+ """
96
122
  gdfs, column, kwargs = self._separate_args(gdfs, column, kwargs)
97
123
 
98
124
  self._column = column
@@ -100,27 +126,14 @@ class Map:
100
126
  self._k = k
101
127
  self.nan_label = nan_label
102
128
  self.nan_color = nan_color
103
- self._cmap = kwargs.pop("cmap", None)
129
+ self._cmap = cmap
104
130
  self.scheme = scheme
105
131
 
106
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
107
- gdfs = [
108
- to_gdf(gdf) if not isinstance(gdf, GeoDataFrame) else gdf
109
- for gdf in gdfs
110
- ]
111
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
112
- raise ValueError("gdfs must be GeoDataFrames.")
113
-
114
- if "namedict" in kwargs:
115
- for i, gdf in enumerate(gdfs):
116
- gdf.name = kwargs["namedict"][i]
117
- kwargs.pop("namedict")
118
-
119
132
  # need to get the object names of the gdfs before copying. Only getting,
120
133
  # not setting, labels. So the original gdfs don't get the label column.
121
- self.labels = labels
122
- if not self.labels:
123
- self._get_labels(gdfs)
134
+ self.labels: list[str] = [
135
+ _determine_best_name(gdf, column, i) for i, gdf in enumerate(gdfs)
136
+ ]
124
137
 
125
138
  show = kwargs.pop("show", True)
126
139
  if isinstance(show, (int, bool)):
@@ -138,7 +151,7 @@ class Map:
138
151
  self._gdfs = []
139
152
  new_labels = []
140
153
  self.show = []
141
- for label, gdf, show in zip(self.labels, gdfs, show_args):
154
+ for label, gdf, show in zip(self.labels, gdfs, show_args, strict=False):
142
155
  if not len(gdf):
143
156
  continue
144
157
 
@@ -146,31 +159,18 @@ class Map:
146
159
  if not len(gdf):
147
160
  continue
148
161
 
149
- self._gdfs.append(gdf)
162
+ self._gdfs.append(to_gdf(gdf))
150
163
  new_labels.append(label)
151
164
  self.show.append(show)
152
165
  self.labels = new_labels
153
166
 
154
- # if len(self._gdfs):
155
- # last_show = self.show[-1]
156
- # else:
157
- # last_show = show
158
-
159
167
  # pop all geometry-like items from kwargs into self._gdfs
160
168
  self.kwargs = {}
161
169
  i = 0
162
170
  for key, value in kwargs.items():
163
- # if isinstance(value, GeoDataFrame):
164
- # self._gdfs.append(value)
165
- # self.labels.append(key)
166
- # try:
167
- # show = show_kwargs[i]
168
- # except IndexError:
169
- # pass
170
- # self.show.append(show)
171
- # i += 1
172
- # continue
173
171
  try:
172
+ if not len(value):
173
+ continue
174
174
  self._gdfs.append(to_gdf(value))
175
175
  self.labels.append(key)
176
176
  try:
@@ -189,10 +189,10 @@ class Map:
189
189
  )
190
190
 
191
191
  if not any(len(gdf) for gdf in self._gdfs):
192
- warnings.warn("None of the GeoDataFrames have rows.")
193
- self._gdfs = None
192
+ self._gdfs = []
194
193
  self._is_categorical = True
195
194
  self._unique_values = []
195
+ self._nan_idx = []
196
196
  return
197
197
 
198
198
  if not self.labels:
@@ -222,7 +222,7 @@ class Map:
222
222
  self._nan_idx = self._gdf[self._column].isna()
223
223
  self._get_unique_values()
224
224
 
225
- def _get_unique_values(self):
225
+ def _get_unique_values(self) -> None:
226
226
  if not self._is_categorical:
227
227
  self._unique_values = self._get_unique_floats()
228
228
  else:
@@ -243,7 +243,7 @@ class Map:
243
243
  Because floats don't always equal each other. This will make very
244
244
  similar values count as the same value in the color classification.
245
245
  """
246
- array = self._gdf.loc[~self._nan_idx, self._column]
246
+ array = self._gdf.loc[list(~self._nan_idx), self._column]
247
247
  self._min = np.min(array)
248
248
  self._max = np.max(array)
249
249
  self._get_multiplier(array)
@@ -254,7 +254,7 @@ class Map:
254
254
 
255
255
  return np.sort(np.array(unique.loc[no_duplicates.index]))
256
256
 
257
- def _array_to_large_int(self, array):
257
+ def _array_to_large_int(self, array: np.ndarray | pd.Series) -> pd.Series:
258
258
  """Multiply values in float array, then convert to integer."""
259
259
  if not isinstance(array, pd.Series):
260
260
  array = pd.Series(array)
@@ -266,9 +266,8 @@ class Map:
266
266
 
267
267
  return pd.concat([unique_multiplied, isna]).sort_index()
268
268
 
269
- def _get_multiplier(self, array: np.ndarray):
270
- """Find the number of zeros needed to push the max value of the array above
271
- +-1_000_000.
269
+ def _get_multiplier(self, array: np.ndarray) -> None:
270
+ """Find the number of zeros needed to push the max value of the array above +-1_000_000.
272
271
 
273
272
  Adding this as an attribute to use later in _classify_from_bins.
274
273
  """
@@ -293,31 +292,72 @@ class Map:
293
292
  def _add_minmax_to_bins(self, bins: list[float | int]) -> list[float | int]:
294
293
  """If values are outside the bin range, add max and/or min values of array."""
295
294
  # make sure they are lists
296
- bins = [bin for bin in bins]
297
-
298
- if min(bins) > 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
299
- bins
300
- ):
301
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
302
-
303
- if min(bins) < 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
304
- bins
305
- ):
306
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
295
+ bins = [bin_ for bin_ in bins]
296
+
297
+ if min(bins) > 0 and min(
298
+ self._gdf.loc[list(~self._nan_idx), self._column]
299
+ ) < min(bins):
300
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
301
+ # if isinstance(num, float):
302
+ # num -= (
303
+ # float(f"1e-{abs(self.legend.rounding)}")
304
+ # if self.legend and self.legend.rounding
305
+ # else 0
306
+ # )
307
+ bins = [num] + bins
308
+
309
+ if min(bins) < 0 and min(
310
+ self._gdf.loc[list(~self._nan_idx), self._column]
311
+ ) < min(bins):
312
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
313
+ # if isinstance(num, float):
314
+ # num -= (
315
+ # float(f"1e-{abs(self.legend.rounding)}")
316
+ # if self.legend and self.legend.rounding
317
+ # else 0
318
+ # )
319
+ bins = [num] + bins
307
320
 
308
321
  if max(bins) > 0 and max(
309
322
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
310
323
  ) > max(bins):
311
- bins = bins + [
312
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
313
- ]
324
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
325
+ # if isinstance(num, float):
326
+ # num += (
327
+ # float(f"1e-{abs(self.legend.rounding)}")
328
+ # if self.legend and self.legend.rounding
329
+ # else 0
330
+ # )
331
+ bins = bins + [num]
314
332
 
315
333
  if max(bins) < 0 and max(
316
334
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
317
335
  ) < max(bins):
318
- bins = bins + [
319
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
320
- ]
336
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
337
+ # if isinstance(num, float):
338
+ # num += (
339
+ # float(f"1e-{abs(self.legend.rounding)}")
340
+ # if self.legend and self.legend.rounding
341
+ # else 0
342
+ # )
343
+
344
+ bins = bins + [num]
345
+
346
+ def adjust_bin(num: int | float, i: int) -> int | float:
347
+ if isinstance(num, int):
348
+ return num
349
+ adjuster = (
350
+ float(f"1e-{abs(self.legend.rounding)}")
351
+ if self.legend and self.legend.rounding
352
+ else 0
353
+ )
354
+ if i == 0:
355
+ return num - adjuster
356
+ elif i == len(bins) - 1:
357
+ return num + adjuster
358
+ return num
359
+
360
+ bins = [adjust_bin(x, i) for i, x in enumerate(bins)]
321
361
 
322
362
  return bins
323
363
 
@@ -329,16 +369,26 @@ class Map:
329
369
  ) -> tuple[tuple[GeoDataFrame], str]:
330
370
  """Separate GeoDataFrames from string (column argument)."""
331
371
 
332
- def as_dict(obj):
372
+ def as_dict(obj) -> dict:
333
373
  if hasattr(obj, "__dict__"):
334
374
  return obj.__dict__
335
375
  elif isinstance(obj, dict):
336
376
  return obj
337
- raise TypeError
338
-
339
- allowed_types = (GeoDataFrame, GeoSeries, Geometry, RasterDataset)
377
+ raise TypeError(type(obj))
378
+
379
+ allowed_types = (
380
+ GeoDataFrame,
381
+ GeoSeries,
382
+ Geometry,
383
+ RasterDataset,
384
+ ImageCollection,
385
+ Image,
386
+ Band,
387
+ )
340
388
 
341
- gdfs: tuple[GeoDataFrame | GeoSeries | Geometry | RasterDataset] = ()
389
+ gdfs = ()
390
+ more_gdfs = {}
391
+ i = 0
342
392
  for arg in args:
343
393
  if isinstance(arg, str):
344
394
  if column is None:
@@ -349,12 +399,31 @@ class Map:
349
399
  )
350
400
  elif isinstance(arg, allowed_types):
351
401
  gdfs = gdfs + (arg,)
402
+ # elif isinstance(arg, Sequence) and not isinstance(arg, str):
352
403
  elif isinstance(arg, dict) or hasattr(arg, "__dict__"):
353
404
  # add dicts or classes with GeoDataFrames to kwargs
354
- more_gdfs = {}
355
405
  for key, value in as_dict(arg).items():
356
406
  if isinstance(value, allowed_types):
357
407
  more_gdfs[key] = value
408
+ elif isinstance(value, dict) or hasattr(value, "__dict__"):
409
+ # elif isinstance(value, Sequence) and not isinstance(value, str):
410
+ try:
411
+ # same as above, one level down
412
+ more_gdfs |= {
413
+ k: v
414
+ for k, v in as_dict(value).items()
415
+ if isinstance(v, allowed_types)
416
+ }
417
+ except Exception:
418
+ # ignore all exceptions
419
+ pass
420
+
421
+ elif isinstance(arg, Sequence) and not isinstance(arg, str):
422
+ # add dicts or classes with GeoDataFrames to kwargs
423
+ for value in arg:
424
+ if isinstance(value, allowed_types):
425
+ name = _determine_best_name(value, column, i)
426
+ more_gdfs[name] = value
358
427
  elif isinstance(value, dict) or hasattr(value, "__dict__"):
359
428
  try:
360
429
  # same as above, one level down
@@ -366,18 +435,24 @@ class Map:
366
435
  except Exception:
367
436
  # no need to raise here
368
437
  pass
438
+ elif isinstance(value, Sequence) and not isinstance(value, str):
439
+ for x in value:
440
+ if not isinstance(x, allowed_types):
441
+ continue
442
+ name = _determine_best_name(value, column, i)
443
+ more_gdfs[name] = x
444
+ i += 1
369
445
 
370
- kwargs |= more_gdfs
446
+ kwargs |= more_gdfs
371
447
 
372
448
  return gdfs, column, kwargs
373
449
 
374
- def _prepare_continous_map(self):
450
+ def _prepare_continous_map(self) -> None:
375
451
  """Create bins if not already done and adjust k if needed."""
376
-
377
452
  if self.scheme is None:
378
453
  return
379
454
 
380
- if not self.bins:
455
+ if self.bins is None:
381
456
  self.bins = self._create_bins(self._gdf, self._column)
382
457
  if len(self.bins) <= self._k and len(self.bins) != len(self._unique_values):
383
458
  self._k = len(self.bins)
@@ -389,17 +464,6 @@ class Map:
389
464
  self._unique_values = self.nan_label
390
465
  self._k = 1
391
466
 
392
- def _get_labels(self, gdfs: tuple[GeoDataFrame]) -> None:
393
- """Putting the labels/names in a list before copying the gdfs."""
394
- self.labels: list[str] = []
395
- for i, gdf in enumerate(gdfs):
396
- if hasattr(gdf, "name") and isinstance(gdf.name, str):
397
- name = gdf.name
398
- else:
399
- name = get_object_name(gdf)
400
- name = name or str(i)
401
- self.labels.append(name)
402
-
403
467
  def _set_labels(self) -> None:
404
468
  """Setting the labels after copying the gdfs."""
405
469
  gdfs = []
@@ -408,7 +472,9 @@ class Map:
408
472
  gdfs.append(gdf)
409
473
  self._gdfs = gdfs
410
474
 
411
- def _to_common_crs_and_one_geom_col(self, gdfs: list[GeoDataFrame]):
475
+ def _to_common_crs_and_one_geom_col(
476
+ self, gdfs: list[GeoDataFrame]
477
+ ) -> list[GeoDataFrame]:
412
478
  """Need common crs and max one geometry column."""
413
479
  crs_list = list({gdf.crs for gdf in gdfs if gdf.crs is not None})
414
480
  if crs_list:
@@ -416,7 +482,7 @@ class Map:
416
482
  new_gdfs = []
417
483
  for gdf in gdfs:
418
484
  gdf = gdf.reset_index(drop=True)
419
- gdf = drop_inactive_geometry_columns(gdf).pipe(rename_geometry_if)
485
+ gdf = drop_inactive_geometry_columns(gdf).pipe(_rename_geometry_if)
420
486
  if crs_list:
421
487
  try:
422
488
  gdf = gdf.to_crs(self.crs)
@@ -451,7 +517,18 @@ class Map:
451
517
  if not self._column:
452
518
  return True
453
519
 
520
+ def is_maybe_km2():
521
+ if "area" in self._column and (
522
+ "km2" in self._column
523
+ or "kilomet" in self._column
524
+ and ("sq" in self._column or "2" in self._column)
525
+ ):
526
+ return True
527
+ else:
528
+ return False
529
+
454
530
  maybe_area = 1 if "area" in self._column else 0
531
+ maybe_area_km2 = 1 if is_maybe_km2() else 0
455
532
  maybe_length = (
456
533
  1 if any(x in self._column for x in ["meter", "metre", "leng"]) else 0
457
534
  )
@@ -460,7 +537,10 @@ class Map:
460
537
  col_not_present = 0
461
538
  for gdf in self._gdfs:
462
539
  if self._column not in gdf:
463
- if maybe_area:
540
+ if maybe_area_km2 and unit_is_meters(gdf):
541
+ gdf["area_km2"] = gdf.area / 1_000_000
542
+ maybe_area_km2 += 1
543
+ elif maybe_area:
464
544
  gdf["area"] = gdf.area
465
545
  maybe_area += 1
466
546
  elif maybe_length:
@@ -473,6 +553,9 @@ class Map:
473
553
  all_nan += 1
474
554
  return True
475
555
 
556
+ if maybe_area_km2 > 1:
557
+ self._column = "area_km2"
558
+ return False
476
559
  if maybe_area > 1:
477
560
  self._column = "area"
478
561
  return False
@@ -488,7 +571,7 @@ class Map:
488
571
 
489
572
  return False
490
573
 
491
- def _get_categorical_colors(self) -> None:
574
+ def _make_categories_colors_dict(self) -> None:
492
575
  # custom categorical cmap
493
576
  if not self._cmap and len(self._unique_values) <= len(_CATEGORICAL_CMAP):
494
577
  self._categories_colors_dict = {
@@ -510,6 +593,7 @@ class Map:
510
593
  for i, category in enumerate(self._unique_values)
511
594
  }
512
595
 
596
+ def _fix_nans(self) -> None:
513
597
  if any(self._nan_idx):
514
598
  self._gdf[self._column] = self._gdf[self._column].fillna(self.nan_label)
515
599
  self._categories_colors_dict[self.nan_label] = self.nan_color
@@ -530,8 +614,7 @@ class Map:
530
614
  If 'scheme' is not specified, the jenks_breaks function is used, which is
531
615
  much faster than the one from Mapclassifier.
532
616
  """
533
-
534
- if not len(gdf.loc[~self._nan_idx, column]):
617
+ if not len(gdf.loc[list(~self._nan_idx), column]):
535
618
  return np.array([0])
536
619
 
537
620
  n_classes = (
@@ -547,29 +630,26 @@ class Map:
547
630
  n_classes = len(self._unique_values)
548
631
 
549
632
  if self.scheme == "jenks":
550
- try:
551
- bins = jenks_breaks(
552
- gdf.loc[~self._nan_idx, column], n_classes=n_classes
553
- )
554
- bins = self._add_minmax_to_bins(bins)
555
- except Exception:
556
- pass
633
+ bins = jenks_breaks(
634
+ gdf.loc[list(~self._nan_idx), column], n_classes=n_classes
635
+ )
557
636
  else:
558
637
  binning = classify(
559
- np.asarray(gdf.loc[~self._nan_idx, column]),
638
+ np.asarray(gdf.loc[list(~self._nan_idx), column]),
560
639
  scheme=self.scheme,
561
- k=self._k,
640
+ # k=self._k,
641
+ k=n_classes,
562
642
  )
563
643
  bins = binning.bins
564
- bins = self._add_minmax_to_bins(bins)
565
644
 
566
- unique_bins = list({round(bin, 5) for bin in bins})
567
- unique_bins.sort()
645
+ bins = self._add_minmax_to_bins(bins)
568
646
 
569
- if self._k == len(self._unique_values) - 1:
570
- return np.array(unique_bins)
647
+ unique_bins = list({round(bin_, 5) for bin_ in bins})
648
+ unique_bins.sort()
571
649
 
572
- if len(unique_bins) == len(self._unique_values):
650
+ if self._k == len(self._unique_values) - 1 or len(unique_bins) == len(
651
+ self._unique_values
652
+ ):
573
653
  return np.array(unique_bins)
574
654
 
575
655
  if len(unique_bins) == len(bins) - 1:
@@ -577,7 +657,7 @@ class Map:
577
657
 
578
658
  return np.array(bins)
579
659
 
580
- def change_cmap(self, cmap: str, start: int = 0, stop: int = 256):
660
+ def change_cmap(self, cmap: str, start: int = 0, stop: int = 256) -> "Map":
581
661
  """Change the color palette of the plot.
582
662
 
583
663
  Args:
@@ -606,6 +686,7 @@ class Map:
606
686
 
607
687
  def _classify_from_bins(self, gdf: GeoDataFrame, bins: np.ndarray) -> np.ndarray:
608
688
  """Place the column values into groups."""
689
+ bins = bins.copy()
609
690
 
610
691
  # if equal lenght, convert to integer and check for equality
611
692
  if len(bins) == len(self._unique_values):
@@ -621,6 +702,14 @@ class Map:
621
702
  if len(bins) == self._k + 1:
622
703
  bins = bins[1:]
623
704
 
705
+ if (
706
+ self.legend
707
+ and self.legend.rounding
708
+ and (self.legend.rounding or 1) <= 0
709
+ ):
710
+ bins[0] = bins[0] - 1
711
+ bins[-1] = bins[-1] + 1
712
+
624
713
  if gdf[self._column].isna().all():
625
714
  return np.repeat(len(bins), len(gdf))
626
715
 
@@ -647,11 +736,12 @@ class Map:
647
736
  return np.array([rank_dict[val] for val in classified])
648
737
 
649
738
  @property
650
- def k(self):
739
+ def k(self) -> int:
740
+ """Number of bins."""
651
741
  return self._k
652
742
 
653
743
  @k.setter
654
- def k(self, new_value: bool):
744
+ def k(self, new_value: int) -> None:
655
745
  if not self._is_categorical and new_value > len(self._unique_values):
656
746
  raise ValueError(
657
747
  "'k' cannot be greater than the number of unique values in the column.'"
@@ -661,55 +751,84 @@ class Map:
661
751
  self._k = int(new_value)
662
752
 
663
753
  @property
664
- def cmap(self):
754
+ def cmap(self) -> str:
755
+ """Colormap."""
665
756
  return self._cmap
666
757
 
667
758
  @cmap.setter
668
- def cmap(self, new_value: bool):
759
+ def cmap(self, new_value: str) -> None:
669
760
  self._cmap = new_value
670
- self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
761
+ if not self._is_categorical:
762
+ self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
671
763
 
672
764
  @property
673
- def gdf(self):
765
+ def gdf(self) -> GeoDataFrame:
766
+ """All GeoDataFrames concated."""
674
767
  return self._gdf
675
768
 
676
769
  @gdf.setter
677
- def gdf(self, _):
770
+ def gdf(self, _) -> None:
678
771
  raise ValueError(
679
772
  "Cannot change 'gdf' after init. Put the GeoDataFrames into "
680
773
  "the class initialiser."
681
774
  )
682
775
 
683
776
  @property
684
- def gdfs(self):
777
+ def gdfs(self) -> list[GeoDataFrame]:
778
+ """All GeoDataFrames as a list."""
685
779
  return self._gdfs
686
780
 
687
781
  @gdfs.setter
688
- def gdfs(self, _):
782
+ def gdfs(self, _) -> None:
689
783
  raise ValueError(
690
784
  "Cannot change 'gdfs' after init. Put the GeoDataFrames into "
691
785
  "the class initialiser."
692
786
  )
693
787
 
694
788
  @property
695
- def column(self):
789
+ def column(self) -> str | None:
790
+ """Column to use as colormap."""
696
791
  return self._column
697
792
 
698
793
  @column.setter
699
- def column(self, _):
794
+ def column(self, _) -> None:
700
795
  raise ValueError(
701
796
  "Cannot change 'column' after init. Specify 'column' in the "
702
797
  "class initialiser."
703
798
  )
704
799
 
705
- def __setitem__(self, item, new_item):
800
+ def __setitem__(self, item: Any, new_item: Any) -> None:
801
+ """Set an attribute with square brackets."""
706
802
  return setattr(self, item, new_item)
707
803
 
708
- def __getitem__(self, item):
804
+ def __getitem__(self, item: Any) -> Any:
805
+ """Get an attribute with square brackets."""
709
806
  return getattr(self, item)
710
807
 
711
- def get(self, key, default=None):
808
+ def get(self, key: Any, default: Any | None = None) -> Any:
809
+ """Get an attribute with default value if not present."""
712
810
  try:
713
811
  return self[key]
714
812
  except (KeyError, ValueError, IndexError, AttributeError):
715
813
  return default
814
+
815
+
816
+ def _determine_best_name(obj: Any, column: str | None, i: int) -> str:
817
+ try:
818
+ # Frame 3: actual object name Frame 2: maps.py:explore(). Frame 1: __init__. Frame 0: this function.
819
+ return str(get_object_name(obj, start=3))
820
+ except ValueError:
821
+ if isinstance(obj, GeoSeries) and obj.name:
822
+ return str(obj.name)
823
+ elif isinstance(obj, GeoDataFrame) and len(obj.columns) == 2 and not column:
824
+ series = obj.drop(columns=obj._geometry_column_name).iloc[:, 0]
825
+ if (
826
+ len(series.unique()) == 1
827
+ and mean(isinstance(x, str) for x in series) > 0.5
828
+ ):
829
+ return str(next(iter(series)))
830
+ elif series.name:
831
+ return str(series.name)
832
+ else:
833
+ # generic label e.g. Image(1)
834
+ return f"{obj.__class__.__name__}({i})"