ssb-sgis 1.0.2__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 (42) hide show
  1. sgis/__init__.py +10 -6
  2. sgis/exceptions.py +2 -2
  3. sgis/geopandas_tools/bounds.py +17 -15
  4. sgis/geopandas_tools/buffer_dissolve_explode.py +24 -5
  5. sgis/geopandas_tools/conversion.py +15 -6
  6. sgis/geopandas_tools/duplicates.py +2 -2
  7. sgis/geopandas_tools/general.py +9 -5
  8. sgis/geopandas_tools/geometry_types.py +3 -3
  9. sgis/geopandas_tools/neighbors.py +3 -3
  10. sgis/geopandas_tools/point_operations.py +2 -2
  11. sgis/geopandas_tools/polygon_operations.py +5 -5
  12. sgis/geopandas_tools/sfilter.py +3 -3
  13. sgis/helpers.py +3 -3
  14. sgis/io/read_parquet.py +1 -1
  15. sgis/maps/examine.py +16 -2
  16. sgis/maps/explore.py +370 -57
  17. sgis/maps/legend.py +164 -72
  18. sgis/maps/map.py +184 -90
  19. sgis/maps/maps.py +92 -90
  20. sgis/maps/thematicmap.py +236 -83
  21. sgis/networkanalysis/closing_network_holes.py +2 -2
  22. sgis/networkanalysis/cutting_lines.py +3 -3
  23. sgis/networkanalysis/directednetwork.py +1 -1
  24. sgis/networkanalysis/finding_isolated_networks.py +2 -2
  25. sgis/networkanalysis/networkanalysis.py +7 -7
  26. sgis/networkanalysis/networkanalysisrules.py +1 -1
  27. sgis/networkanalysis/traveling_salesman.py +1 -1
  28. sgis/parallel/parallel.py +39 -19
  29. sgis/raster/__init__.py +0 -6
  30. sgis/raster/cube.py +51 -5
  31. sgis/raster/image_collection.py +2560 -0
  32. sgis/raster/indices.py +14 -5
  33. sgis/raster/raster.py +131 -236
  34. sgis/raster/sentinel_config.py +104 -0
  35. sgis/raster/zonal.py +0 -1
  36. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +1 -1
  37. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  38. sgis/raster/methods_as_functions.py +0 -0
  39. sgis/raster/torchgeo.py +0 -171
  40. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  41. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +0 -0
  42. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +0 -0
sgis/maps/legend.py CHANGED
@@ -26,6 +26,82 @@ warnings.filterwarnings(
26
26
  pd.options.mode.chained_assignment = None
27
27
 
28
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
+
29
105
  class Legend:
30
106
  """Holds the general attributes of the legend in the ThematicMap class.
31
107
 
@@ -36,27 +112,8 @@ class Legend:
36
112
  If a numeric column is used, additional attributes can be found in the
37
113
  ContinousLegend class.
38
114
 
39
- Attributes:
40
- title: Legend title. Defaults to the column name if used in the
41
- ThematicMap class.
42
- position: The legend's x and y position in the plot, specified as a tuple of
43
- x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
44
- in the bottom right corner, (0.2, 0.8) for the upper left corner.
45
- fontsize: Text size of the legend labels. Defaults to the size of
46
- the ThematicMap class.
47
- title_fontsize: Text size of the legend title. Defaults to the
48
- size * 1.2 of the ThematicMap class.
49
- markersize: Size of the color circles in the legend. Defaults to the size of
50
- the ThematicMap class.
51
- framealpha: Transparency of the legend background.
52
- edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
53
- kwargs: Stores additional keyword arguments taken by the matplotlib legend
54
- method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
55
- 'm' is the name of the ThematicMap instance. See here:
56
- https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
57
-
58
115
  Examples:
59
- --------
116
+ ---------
60
117
  Create ten points with a numeric column from 0 to 9.
61
118
 
62
119
  >>> import sgis as sg
@@ -89,38 +146,31 @@ class Legend:
89
146
  9 POINT (0.75000 0.25000) 9
90
147
 
91
148
  Creating the ThematicMap instance will also create the legend. Since we
92
- specify a numeric column, a ContinousLegend instance is created.
93
-
94
- >>> 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()
95
166
  >>> m.legend
96
167
  <sgis.maps.legend.ContinousLegend object at 0x00000222206738D0>
97
-
98
- Changing the attributes that apply to both numeric and categorical columns.
99
-
100
- >>> m.legend.title = "Meters"
101
- >>> m.legend.title_fontsize = 11
102
- >>> m.legend.fontsize = 9
103
- >>> m.legend.markersize = 7.5
104
- >>> m.legend.position = (0.35, 0.28)
105
- >>> m.plot()
106
-
107
- Additional matplotlib keyword arguments can be specified as kwargs.
108
-
109
- >>> m.legend.kwargs["labelcolor"] = "red"
110
-
111
- Since we are using a numeric column, the legend is of type ContinousLegend.
112
- We can therefore also access the attributes that only apply to numeric columns.
113
-
114
- >>> m.label_sep = "to"
115
- >>> m.label_suffix = "num"
116
- >>> m.rounding = 2
117
- >>> m.plot()
118
-
119
168
  """
120
169
 
121
170
  def __init__(
122
171
  self,
123
172
  title: str | None = None,
173
+ pretty_labels: bool = True,
124
174
  labels: list[str] | None = None,
125
175
  position: tuple[float] | None = None,
126
176
  markersize: int | None = None,
@@ -135,6 +185,8 @@ class Legend:
135
185
  Args:
136
186
  title: Legend title. Defaults to the column name if used in the
137
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.
138
190
  labels: Labels of the categories.
139
191
  position: The legend's x and y position in the plot, specified as a tuple of
140
192
  x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
@@ -163,6 +215,7 @@ class Legend:
163
215
  self._fontsize = fontsize
164
216
  self._markersize = markersize
165
217
 
218
+ self.pretty_labels = pretty_labels
166
219
  self.framealpha = framealpha
167
220
  self.edgecolor = edgecolor
168
221
  self.width = kwargs.pop("width", 0.1)
@@ -172,9 +225,23 @@ class Legend:
172
225
 
173
226
  self.labels = labels
174
227
  self._position = position
175
- self.kwargs = kwargs
176
228
  self._position_has_been_set = True if position else False
177
229
 
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)
239
+
240
+ @property
241
+ def valid_keywords(self) -> set[str]:
242
+ """List all valid keywords for the class initialiser."""
243
+ return LEGEND_KWARGS
244
+
178
245
  def _get_legend_sizes(self, size: int | float, kwargs: dict) -> None:
179
246
  """Adjust fontsize and markersize to size kwarg."""
180
247
  if "title_fontsize" in kwargs:
@@ -218,6 +285,8 @@ class Legend:
218
285
 
219
286
  self._patches, self._categories = [], []
220
287
  for category, color in categories_colors.items():
288
+ if self.pretty_labels:
289
+ category = prettify_label(category)
221
290
  if category == nan_label:
222
291
  self._categories.append(nan_label)
223
292
  else:
@@ -236,6 +305,8 @@ class Legend:
236
305
  )
237
306
 
238
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)
239
310
  legend = ax.legend(
240
311
  self._patches,
241
312
  self._categories,
@@ -360,10 +431,6 @@ class ContinousLegend(Legend):
360
431
  Defaults to None.
361
432
  label_sep: Text to put in between the two numbers in each color group in
362
433
  the legend. Defaults to '-'.
363
- rounding: Number of decimals in the labels. By default, the rounding
364
- depends on the column's maximum value and standard deviation.
365
- OBS: The bins will not be rounded, meaning the labels might be wrong
366
- if not bins are set manually.
367
434
  thousand_sep: Separator between each thousand for large numbers. Defaults to
368
435
  None, meaning no separator.
369
436
  decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
@@ -371,7 +438,7 @@ class ContinousLegend(Legend):
371
438
  decimal mark.
372
439
 
373
440
  Examples:
374
- --------
441
+ ---------
375
442
  Create ten random points with a numeric column from 0 to 9.
376
443
 
377
444
  >>> import sgis as sg
@@ -428,7 +495,7 @@ class ContinousLegend(Legend):
428
495
  def __init__(
429
496
  self,
430
497
  labels: list[str] | None = None,
431
- pretty_labels: bool = False,
498
+ pretty_labels: bool = True,
432
499
  label_suffix: str | None = None,
433
500
  label_sep: str = "-",
434
501
  rounding: int | None = None,
@@ -474,7 +541,7 @@ class ContinousLegend(Legend):
474
541
  self.label_sep = label_sep
475
542
  self.label_suffix = "" if not label_suffix else label_suffix
476
543
  self._rounding = rounding
477
- self._rounding_has_been_set = True if rounding else False
544
+ # self._rounding_has_been_set = True if rounding else False
478
545
 
479
546
  def _get_rounding(self, array: Series | np.ndarray) -> int:
480
547
  def isinteger(x):
@@ -502,8 +569,10 @@ class ContinousLegend(Legend):
502
569
 
503
570
  @staticmethod
504
571
  def _set_rounding(bins, rounding: int | float) -> list[int | float]:
505
- if rounding == 0:
572
+ if not rounding:
506
573
  return [int(round(bin_, 0)) for bin_ in bins]
574
+ elif rounding <= 0:
575
+ return [int(round(bin_, rounding)) for bin_ in bins]
507
576
  else:
508
577
  return [round(bin_, rounding) for bin_ in bins]
509
578
 
@@ -520,8 +589,8 @@ class ContinousLegend(Legend):
520
589
  ) -> None:
521
590
  # TODO: clean up this messy method
522
591
 
523
- for attr in self.__dict__.keys():
524
- if attr in self.kwargs:
592
+ for attr in self.kwargs:
593
+ if attr in self.__dict__:
525
594
  self[attr] = self.kwargs.pop(attr)
526
595
 
527
596
  self._patches, self._categories = [], []
@@ -544,9 +613,10 @@ class ContinousLegend(Legend):
544
613
  if len(self.labels) != len(colors):
545
614
  raise ValueError(
546
615
  "Label list must be same length as the number of groups. "
547
- f"Got k={len(colors)} and labels={len(colors)}."
616
+ f"Got k={len(colors)} and labels={len(self.labels)}."
548
617
  f"labels: {', '.join(self.labels)}"
549
618
  f"colors: {', '.join(colors)}"
619
+ f"bins: {bins}"
550
620
  )
551
621
  self._categories = self.labels
552
622
 
@@ -576,42 +646,59 @@ class ContinousLegend(Legend):
576
646
 
577
647
  min_ = np.min(bin_values[i])
578
648
  max_ = np.max(bin_values[i])
579
- min_rounded = self._set_rounding([min_], self._rounding)[0]
580
- max_rounded = self._set_rounding([max_], self._rounding)[0]
649
+
581
650
  if self.pretty_labels:
582
- if i != 0 and self._rounding == 0:
583
- cat1 = int(cat1 + 1)
584
- elif i != 0:
585
- 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)
586
669
 
587
670
  cat1 = self._format_number(cat1)
588
671
  cat2 = self._format_number(cat2)
589
672
 
590
673
  if min_ == max_:
591
- label = self._two_value_label(cat1, cat2)
674
+ label = self._get_two_value_label(cat1, cat2)
592
675
  self._categories.append(label)
593
676
  continue
594
677
 
595
- label = self._two_value_label(cat1, cat2)
678
+ label = self._get_two_value_label(cat1, cat2)
596
679
  self._categories.append(label)
597
680
 
598
- 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_:
599
686
  min_rounded = self._format_number(min_rounded)
600
- label = self._one_value_label(min_rounded)
687
+ label = self._get_one_value_label(min_rounded)
601
688
  self._categories.append(label)
602
689
  else:
603
690
  min_rounded = self._format_number(min_rounded)
604
691
  max_rounded = self._format_number(max_rounded)
605
- label = self._two_value_label(min_rounded, max_rounded)
692
+ label = self._get_two_value_label(min_rounded, max_rounded)
606
693
  self._categories.append(label)
607
694
 
608
- def _two_value_label(self, value1: int | float, value2: int | float) -> str:
695
+ def _get_two_value_label(self, value1: int | float, value2: int | float) -> str:
609
696
  return (
610
697
  f"{value1} {self.label_suffix} {self.label_sep} "
611
698
  f"{value2} {self.label_suffix}"
612
699
  )
613
700
 
614
- def _one_value_label(self, value1: int | float) -> str:
701
+ def _get_one_value_label(self, value1: int | float) -> str:
615
702
  return f"{value1} {self.label_suffix}"
616
703
 
617
704
  def _format_number(self, number: int | float) -> int | float:
@@ -640,10 +727,15 @@ class ContinousLegend(Legend):
640
727
 
641
728
  @property
642
729
  def rounding(self) -> int:
643
- """Number rounding."""
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
+ """
644
737
  return self._rounding
645
738
 
646
739
  @rounding.setter
647
740
  def rounding(self, new_value: int) -> None:
648
741
  self._rounding = new_value
649
- self._rounding_has_been_set = True