ssb-sgis 1.0.2__py3-none-any.whl → 1.0.4__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 (50) hide show
  1. sgis/__init__.py +20 -9
  2. sgis/debug_config.py +24 -0
  3. sgis/exceptions.py +2 -2
  4. sgis/geopandas_tools/bounds.py +33 -36
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
  6. sgis/geopandas_tools/centerlines.py +4 -91
  7. sgis/geopandas_tools/cleaning.py +1576 -583
  8. sgis/geopandas_tools/conversion.py +38 -19
  9. sgis/geopandas_tools/duplicates.py +29 -8
  10. sgis/geopandas_tools/general.py +263 -100
  11. sgis/geopandas_tools/geometry_types.py +4 -4
  12. sgis/geopandas_tools/neighbors.py +19 -15
  13. sgis/geopandas_tools/overlay.py +2 -2
  14. sgis/geopandas_tools/point_operations.py +5 -5
  15. sgis/geopandas_tools/polygon_operations.py +510 -105
  16. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  17. sgis/geopandas_tools/sfilter.py +29 -12
  18. sgis/helpers.py +3 -3
  19. sgis/io/dapla_functions.py +238 -19
  20. sgis/io/read_parquet.py +1 -1
  21. sgis/maps/examine.py +27 -12
  22. sgis/maps/explore.py +450 -65
  23. sgis/maps/legend.py +177 -76
  24. sgis/maps/map.py +206 -103
  25. sgis/maps/maps.py +178 -105
  26. sgis/maps/thematicmap.py +243 -83
  27. sgis/networkanalysis/_service_area.py +6 -1
  28. sgis/networkanalysis/closing_network_holes.py +2 -2
  29. sgis/networkanalysis/cutting_lines.py +15 -8
  30. sgis/networkanalysis/directednetwork.py +1 -1
  31. sgis/networkanalysis/finding_isolated_networks.py +15 -8
  32. sgis/networkanalysis/networkanalysis.py +17 -19
  33. sgis/networkanalysis/networkanalysisrules.py +1 -1
  34. sgis/networkanalysis/traveling_salesman.py +1 -1
  35. sgis/parallel/parallel.py +64 -27
  36. sgis/raster/__init__.py +0 -6
  37. sgis/raster/base.py +208 -0
  38. sgis/raster/cube.py +54 -8
  39. sgis/raster/image_collection.py +3257 -0
  40. sgis/raster/indices.py +17 -5
  41. sgis/raster/raster.py +138 -243
  42. sgis/raster/sentinel_config.py +120 -0
  43. sgis/raster/zonal.py +0 -1
  44. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  45. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  46. sgis/raster/methods_as_functions.py +0 -0
  47. sgis/raster/torchgeo.py +0 -171
  48. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  49. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  50. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/map.py CHANGED
@@ -4,6 +4,8 @@ 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
7
9
  from typing import Any
8
10
 
9
11
  import matplotlib
@@ -22,6 +24,10 @@ from ..geopandas_tools.general import clean_geoms
22
24
  from ..geopandas_tools.general import drop_inactive_geometry_columns
23
25
  from ..geopandas_tools.general import get_common_crs
24
26
  from ..helpers import get_object_name
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
25
31
 
26
32
  try:
27
33
  from torchgeo.datasets.geo import RasterDataset
@@ -42,18 +48,18 @@ pd.options.mode.chained_assignment = None
42
48
  # similar colors. The palette is like the "Set2" cmap from matplotlib, but with more
43
49
  # colors. If more than 14 categories, the geopandas default cmap is used.
44
50
  _CATEGORICAL_CMAP = {
45
- 0: "#4576ff",
46
- 1: "#ff455e",
47
- 2: "#ffa617",
48
- 3: "#ff8cc9",
49
- 4: "#804e00",
50
- 5: "#99ff00",
51
- 6: "#fff700",
51
+ 0: "#3b93ff",
52
+ 1: "#ff3370",
53
+ 2: "#f7cf19",
54
+ 3: "#60e825",
55
+ 4: "#ff8cc9",
56
+ 5: "#804e00",
57
+ 6: "#e3dc00",
52
58
  7: "#00ffee",
53
- 8: "#36d19b",
54
- 9: "#94006b",
55
- 10: "#750000",
59
+ 9: "#870062",
60
+ 10: "#751500",
56
61
  11: "#1c6b00",
62
+ 8: "#7cebb9",
57
63
  }
58
64
 
59
65
  DEFAULT_SCHEME = "quantiles"
@@ -91,12 +97,12 @@ class Map:
91
97
  self,
92
98
  *gdfs: GeoDataFrame,
93
99
  column: str | None = None,
94
- labels: tuple[str] | None = None,
95
100
  k: int = 5,
96
101
  bins: tuple[float] | None = None,
97
102
  nan_label: str = "Missing",
98
103
  nan_color="#c2c2c2",
99
104
  scheme: str = DEFAULT_SCHEME,
105
+ cmap: str | None = None,
100
106
  **kwargs,
101
107
  ) -> None:
102
108
  """Initialiser.
@@ -104,12 +110,13 @@ class Map:
104
110
  Args:
105
111
  *gdfs: Variable length GeoDataFrame list.
106
112
  column: The column name to work with.
107
- labels: Tuple of labels for each GeoDataFrame.
108
113
  k: Number of bins or classes for classification (default: 5).
109
114
  bins: Predefined bins for data classification.
110
115
  nan_label: Label for missing data.
111
116
  nan_color: Color for missing data.
112
117
  scheme: Classification scheme to be used.
118
+ cmap (str): Colormap of the plot. See:
119
+ https://matplotlib.org/stable/tutorials/colors/colormaps.html
113
120
  **kwargs: Arbitrary keyword arguments.
114
121
  """
115
122
  gdfs, column, kwargs = self._separate_args(gdfs, column, kwargs)
@@ -119,27 +126,14 @@ class Map:
119
126
  self._k = k
120
127
  self.nan_label = nan_label
121
128
  self.nan_color = nan_color
122
- self._cmap = kwargs.pop("cmap", None)
129
+ self._cmap = cmap
123
130
  self.scheme = scheme
124
131
 
125
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
126
- gdfs = [
127
- to_gdf(gdf) if not isinstance(gdf, GeoDataFrame) else gdf
128
- for gdf in gdfs
129
- ]
130
- if not all(isinstance(gdf, GeoDataFrame) for gdf in gdfs):
131
- raise ValueError("gdfs must be GeoDataFrames.")
132
-
133
- if "namedict" in kwargs:
134
- for i, gdf in enumerate(gdfs):
135
- gdf.name = kwargs["namedict"][i]
136
- kwargs.pop("namedict")
137
-
138
132
  # need to get the object names of the gdfs before copying. Only getting,
139
133
  # not setting, labels. So the original gdfs don't get the label column.
140
- self.labels = labels
141
- if not self.labels:
142
- self._get_labels(gdfs)
134
+ self.labels: list[str] = [
135
+ _determine_best_name(gdf, column, i) for i, gdf in enumerate(gdfs)
136
+ ]
143
137
 
144
138
  show = kwargs.pop("show", True)
145
139
  if isinstance(show, (int, bool)):
@@ -153,6 +147,7 @@ class Map:
153
147
  show_temp = show
154
148
 
155
149
  show_args = show_temp[: len(gdfs)]
150
+ # gdfs that are in kwargs
156
151
  show_kwargs = show_temp[len(gdfs) :]
157
152
  self._gdfs = []
158
153
  new_labels = []
@@ -165,31 +160,20 @@ class Map:
165
160
  if not len(gdf):
166
161
  continue
167
162
 
168
- self._gdfs.append(gdf)
163
+ self._gdfs.append(to_gdf(gdf))
169
164
  new_labels.append(label)
170
165
  self.show.append(show)
171
166
  self.labels = new_labels
172
167
 
173
- # if len(self._gdfs):
174
- # last_show = self.show[-1]
175
- # else:
176
- # last_show = show
177
-
178
168
  # pop all geometry-like items from kwargs into self._gdfs
179
169
  self.kwargs = {}
180
170
  i = 0
181
171
  for key, value in kwargs.items():
182
- # if isinstance(value, GeoDataFrame):
183
- # self._gdfs.append(value)
184
- # self.labels.append(key)
185
- # try:
186
- # show = show_kwargs[i]
187
- # except IndexError:
188
- # pass
189
- # self.show.append(show)
190
- # i += 1
191
- # continue
192
172
  try:
173
+ if isinstance(value, Geometry):
174
+ value = to_gdf(value)
175
+ if not len(value):
176
+ continue
193
177
  self._gdfs.append(to_gdf(value))
194
178
  self.labels.append(key)
195
179
  try:
@@ -207,11 +191,11 @@ class Map:
207
191
  f"length as gdfs ({len(gdfs)}). Got len {len(show)}"
208
192
  )
209
193
 
210
- if not any(len(gdf) for gdf in self._gdfs):
211
- warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
212
- self._gdfs = None
194
+ if not self._gdfs or not any(len(gdf) for gdf in self._gdfs):
195
+ self._gdfs = []
213
196
  self._is_categorical = True
214
197
  self._unique_values = []
198
+ self._nan_idx = []
215
199
  return
216
200
 
217
201
  if not self.labels:
@@ -241,6 +225,10 @@ class Map:
241
225
  self._nan_idx = self._gdf[self._column].isna()
242
226
  self._get_unique_values()
243
227
 
228
+ def __bool__(self) -> bool:
229
+ """True of any gdfs with more than 0 rows."""
230
+ return bool(len(self._gdfs) + len(self._gdf))
231
+
244
232
  def _get_unique_values(self) -> None:
245
233
  if not self._is_categorical:
246
234
  self._unique_values = self._get_unique_floats()
@@ -262,7 +250,7 @@ class Map:
262
250
  Because floats don't always equal each other. This will make very
263
251
  similar values count as the same value in the color classification.
264
252
  """
265
- array = self._gdf.loc[~self._nan_idx, self._column]
253
+ array = self._gdf.loc[list(~self._nan_idx), self._column]
266
254
  self._min = np.min(array)
267
255
  self._max = np.max(array)
268
256
  self._get_multiplier(array)
@@ -313,29 +301,70 @@ class Map:
313
301
  # make sure they are lists
314
302
  bins = [bin_ for bin_ in bins]
315
303
 
316
- if min(bins) > 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
317
- bins
318
- ):
319
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
320
-
321
- if min(bins) < 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
322
- bins
323
- ):
324
- bins = [min(self._gdf.loc[~self._nan_idx, self._column])] + bins
304
+ if min(bins) > 0 and min(
305
+ self._gdf.loc[list(~self._nan_idx), self._column]
306
+ ) < min(bins):
307
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
308
+ # if isinstance(num, float):
309
+ # num -= (
310
+ # float(f"1e-{abs(self.legend.rounding)}")
311
+ # if self.legend and self.legend.rounding
312
+ # else 0
313
+ # )
314
+ bins = [num] + bins
315
+
316
+ if min(bins) < 0 and min(
317
+ self._gdf.loc[list(~self._nan_idx), self._column]
318
+ ) < min(bins):
319
+ num = min(self._gdf.loc[list(~self._nan_idx), self._column])
320
+ # if isinstance(num, float):
321
+ # num -= (
322
+ # float(f"1e-{abs(self.legend.rounding)}")
323
+ # if self.legend and self.legend.rounding
324
+ # else 0
325
+ # )
326
+ bins = [num] + bins
325
327
 
326
328
  if max(bins) > 0 and max(
327
329
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
328
330
  ) > max(bins):
329
- bins = bins + [
330
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
331
- ]
331
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
332
+ # if isinstance(num, float):
333
+ # num += (
334
+ # float(f"1e-{abs(self.legend.rounding)}")
335
+ # if self.legend and self.legend.rounding
336
+ # else 0
337
+ # )
338
+ bins = bins + [num]
332
339
 
333
340
  if max(bins) < 0 and max(
334
341
  self._gdf.loc[self._gdf[self._column].notna(), self._column]
335
342
  ) < max(bins):
336
- bins = bins + [
337
- max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
338
- ]
343
+ num = max(self._gdf.loc[self._gdf[self._column].notna(), self._column])
344
+ # if isinstance(num, float):
345
+ # num += (
346
+ # float(f"1e-{abs(self.legend.rounding)}")
347
+ # if self.legend and self.legend.rounding
348
+ # else 0
349
+ # )
350
+
351
+ bins = bins + [num]
352
+
353
+ def adjust_bin(num: int | float, i: int) -> int | float:
354
+ if isinstance(num, int):
355
+ return num
356
+ adjuster = (
357
+ float(f"1e-{abs(self.legend.rounding)}")
358
+ if self.legend and self.legend.rounding
359
+ else 0
360
+ )
361
+ if i == 0:
362
+ return num - adjuster
363
+ elif i == len(bins) - 1:
364
+ return num + adjuster
365
+ return num
366
+
367
+ bins = [adjust_bin(x, i) for i, x in enumerate(bins)]
339
368
 
340
369
  return bins
341
370
 
@@ -347,16 +376,26 @@ class Map:
347
376
  ) -> tuple[tuple[GeoDataFrame], str]:
348
377
  """Separate GeoDataFrames from string (column argument)."""
349
378
 
350
- def as_dict(obj):
379
+ def as_dict(obj) -> dict:
351
380
  if hasattr(obj, "__dict__"):
352
381
  return obj.__dict__
353
382
  elif isinstance(obj, dict):
354
383
  return obj
355
- raise TypeError
356
-
357
- allowed_types = (GeoDataFrame, GeoSeries, Geometry, RasterDataset)
384
+ raise TypeError(type(obj))
385
+
386
+ allowed_types = (
387
+ GeoDataFrame,
388
+ GeoSeries,
389
+ Geometry,
390
+ RasterDataset,
391
+ ImageCollection,
392
+ Image,
393
+ Band,
394
+ )
358
395
 
359
- gdfs: tuple[GeoDataFrame | GeoSeries | Geometry | RasterDataset] = ()
396
+ gdfs = ()
397
+ more_gdfs = {}
398
+ i = 0
360
399
  for arg in args:
361
400
  if isinstance(arg, str):
362
401
  if column is None:
@@ -367,12 +406,31 @@ class Map:
367
406
  )
368
407
  elif isinstance(arg, allowed_types):
369
408
  gdfs = gdfs + (arg,)
409
+ # elif isinstance(arg, Sequence) and not isinstance(arg, str):
370
410
  elif isinstance(arg, dict) or hasattr(arg, "__dict__"):
371
411
  # add dicts or classes with GeoDataFrames to kwargs
372
- more_gdfs = {}
373
412
  for key, value in as_dict(arg).items():
374
413
  if isinstance(value, allowed_types):
375
414
  more_gdfs[key] = value
415
+ elif isinstance(value, dict) or hasattr(value, "__dict__"):
416
+ # elif isinstance(value, Sequence) and not isinstance(value, str):
417
+ try:
418
+ # same as above, one level down
419
+ more_gdfs |= {
420
+ k: v
421
+ for k, v in as_dict(value).items()
422
+ if isinstance(v, allowed_types)
423
+ }
424
+ except Exception:
425
+ # ignore all exceptions
426
+ pass
427
+
428
+ elif isinstance(arg, Sequence) and not isinstance(arg, str):
429
+ # add dicts or classes with GeoDataFrames to kwargs
430
+ for value in arg:
431
+ if isinstance(value, allowed_types):
432
+ name = _determine_best_name(value, column, i)
433
+ more_gdfs[name] = value
376
434
  elif isinstance(value, dict) or hasattr(value, "__dict__"):
377
435
  try:
378
436
  # same as above, one level down
@@ -384,8 +442,15 @@ class Map:
384
442
  except Exception:
385
443
  # no need to raise here
386
444
  pass
445
+ elif isinstance(value, Sequence) and not isinstance(value, str):
446
+ for x in value:
447
+ if not isinstance(x, allowed_types):
448
+ continue
449
+ name = _determine_best_name(value, column, i)
450
+ more_gdfs[name] = x
451
+ i += 1
387
452
 
388
- kwargs |= more_gdfs
453
+ kwargs |= more_gdfs
389
454
 
390
455
  return gdfs, column, kwargs
391
456
 
@@ -394,7 +459,7 @@ class Map:
394
459
  if self.scheme is None:
395
460
  return
396
461
 
397
- if not self.bins:
462
+ if self.bins is None:
398
463
  self.bins = self._create_bins(self._gdf, self._column)
399
464
  if len(self.bins) <= self._k and len(self.bins) != len(self._unique_values):
400
465
  self._k = len(self.bins)
@@ -406,17 +471,6 @@ class Map:
406
471
  self._unique_values = self.nan_label
407
472
  self._k = 1
408
473
 
409
- def _get_labels(self, gdfs: tuple[GeoDataFrame]) -> None:
410
- """Putting the labels/names in a list before copying the gdfs."""
411
- self.labels: list[str] = []
412
- for i, gdf in enumerate(gdfs):
413
- if hasattr(gdf, "name") and isinstance(gdf.name, str):
414
- name = gdf.name
415
- else:
416
- name = get_object_name(gdf)
417
- name = name or str(i)
418
- self.labels.append(name)
419
-
420
474
  def _set_labels(self) -> None:
421
475
  """Setting the labels after copying the gdfs."""
422
476
  gdfs = []
@@ -467,10 +521,21 @@ class Map:
467
521
 
468
522
  def _check_if_categorical(self) -> bool:
469
523
  """Quite messy this..."""
470
- if not self._column:
524
+ if not self._column or not self._gdfs:
471
525
  return True
472
526
 
527
+ def is_maybe_km2():
528
+ if "area" in self._column and (
529
+ "km2" in self._column
530
+ or "kilomet" in self._column
531
+ and ("sq" in self._column or "2" in self._column)
532
+ ):
533
+ return True
534
+ else:
535
+ return False
536
+
473
537
  maybe_area = 1 if "area" in self._column else 0
538
+ maybe_area_km2 = 1 if is_maybe_km2() else 0
474
539
  maybe_length = (
475
540
  1 if any(x in self._column for x in ["meter", "metre", "leng"]) else 0
476
541
  )
@@ -479,7 +544,10 @@ class Map:
479
544
  col_not_present = 0
480
545
  for gdf in self._gdfs:
481
546
  if self._column not in gdf:
482
- if maybe_area:
547
+ if maybe_area_km2 and unit_is_meters(gdf):
548
+ gdf["area_km2"] = gdf.area / 1_000_000
549
+ maybe_area_km2 += 1
550
+ elif maybe_area:
483
551
  gdf["area"] = gdf.area
484
552
  maybe_area += 1
485
553
  elif maybe_length:
@@ -492,6 +560,9 @@ class Map:
492
560
  all_nan += 1
493
561
  return True
494
562
 
563
+ if maybe_area_km2 > 1:
564
+ self._column = "area_km2"
565
+ return False
495
566
  if maybe_area > 1:
496
567
  self._column = "area"
497
568
  return False
@@ -500,14 +571,16 @@ class Map:
500
571
  return False
501
572
 
502
573
  if all_nan == len(self._gdfs):
503
- raise ValueError(f"All values are NaN in column {self.column!r}.")
574
+ raise ValueError(
575
+ f"All values are NaN in column {self.column!r}. {self._gdfs}"
576
+ )
504
577
 
505
578
  if col_not_present == len(self._gdfs):
506
579
  raise ValueError(f"{self.column} not found.")
507
580
 
508
581
  return False
509
582
 
510
- def _get_categorical_colors(self) -> None:
583
+ def _make_categories_colors_dict(self) -> None:
511
584
  # custom categorical cmap
512
585
  if not self._cmap and len(self._unique_values) <= len(_CATEGORICAL_CMAP):
513
586
  self._categories_colors_dict = {
@@ -529,6 +602,7 @@ class Map:
529
602
  for i, category in enumerate(self._unique_values)
530
603
  }
531
604
 
605
+ def _fix_nans(self) -> None:
532
606
  if any(self._nan_idx):
533
607
  self._gdf[self._column] = self._gdf[self._column].fillna(self.nan_label)
534
608
  self._categories_colors_dict[self.nan_label] = self.nan_color
@@ -549,7 +623,7 @@ class Map:
549
623
  If 'scheme' is not specified, the jenks_breaks function is used, which is
550
624
  much faster than the one from Mapclassifier.
551
625
  """
552
- if not len(gdf.loc[~self._nan_idx, column]):
626
+ if not len(gdf.loc[list(~self._nan_idx), column]):
553
627
  return np.array([0])
554
628
 
555
629
  n_classes = (
@@ -565,29 +639,26 @@ class Map:
565
639
  n_classes = len(self._unique_values)
566
640
 
567
641
  if self.scheme == "jenks":
568
- try:
569
- bins = jenks_breaks(
570
- gdf.loc[~self._nan_idx, column], n_classes=n_classes
571
- )
572
- bins = self._add_minmax_to_bins(bins)
573
- except Exception:
574
- pass
642
+ bins = jenks_breaks(
643
+ gdf.loc[list(~self._nan_idx), column], n_classes=n_classes
644
+ )
575
645
  else:
576
646
  binning = classify(
577
- np.asarray(gdf.loc[~self._nan_idx, column]),
647
+ np.asarray(gdf.loc[list(~self._nan_idx), column]),
578
648
  scheme=self.scheme,
579
- k=self._k,
649
+ # k=self._k,
650
+ k=n_classes,
580
651
  )
581
652
  bins = binning.bins
582
- bins = self._add_minmax_to_bins(bins)
653
+
654
+ bins = self._add_minmax_to_bins(bins)
583
655
 
584
656
  unique_bins = list({round(bin_, 5) for bin_ in bins})
585
657
  unique_bins.sort()
586
658
 
587
- if self._k == len(self._unique_values) - 1:
588
- return np.array(unique_bins)
589
-
590
- if len(unique_bins) == len(self._unique_values):
659
+ if self._k == len(self._unique_values) - 1 or len(unique_bins) == len(
660
+ self._unique_values
661
+ ):
591
662
  return np.array(unique_bins)
592
663
 
593
664
  if len(unique_bins) == len(bins) - 1:
@@ -624,6 +695,8 @@ class Map:
624
695
 
625
696
  def _classify_from_bins(self, gdf: GeoDataFrame, bins: np.ndarray) -> np.ndarray:
626
697
  """Place the column values into groups."""
698
+ bins = bins.copy()
699
+
627
700
  # if equal lenght, convert to integer and check for equality
628
701
  if len(bins) == len(self._unique_values):
629
702
  if gdf[self._column].isna().all():
@@ -638,6 +711,14 @@ class Map:
638
711
  if len(bins) == self._k + 1:
639
712
  bins = bins[1:]
640
713
 
714
+ if (
715
+ self.legend
716
+ and self.legend.rounding
717
+ and (self.legend.rounding or 1) <= 0
718
+ ):
719
+ bins[0] = bins[0] - 1
720
+ bins[-1] = bins[-1] + 1
721
+
641
722
  if gdf[self._column].isna().all():
642
723
  return np.repeat(len(bins), len(gdf))
643
724
 
@@ -686,7 +767,8 @@ class Map:
686
767
  @cmap.setter
687
768
  def cmap(self, new_value: str) -> None:
688
769
  self._cmap = new_value
689
- self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
770
+ if not self._is_categorical:
771
+ self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
690
772
 
691
773
  @property
692
774
  def gdf(self) -> GeoDataFrame:
@@ -738,3 +820,24 @@ class Map:
738
820
  return self[key]
739
821
  except (KeyError, ValueError, IndexError, AttributeError):
740
822
  return default
823
+
824
+
825
+ def _determine_best_name(obj: Any, column: str | None, i: int) -> str:
826
+ try:
827
+ # Frame 3: actual object name Frame 2: maps.py:explore(). Frame 1: __init__. Frame 0: this function.
828
+ return str(get_object_name(obj, start=3))
829
+ except ValueError:
830
+ if isinstance(obj, GeoSeries) and obj.name:
831
+ return str(obj.name)
832
+ elif isinstance(obj, GeoDataFrame) and len(obj.columns) == 2 and not column:
833
+ series = obj.drop(columns=obj._geometry_column_name).iloc[:, 0]
834
+ if (
835
+ len(series.unique()) == 1
836
+ and mean(isinstance(x, str) for x in series) > 0.5
837
+ ):
838
+ return str(next(iter(series)))
839
+ elif series.name:
840
+ return str(series.name)
841
+ else:
842
+ # generic label e.g. Image(1)
843
+ return f"{obj.__class__.__name__}({i})"