ssb-sgis 1.0.0__py3-none-any.whl → 1.0.2__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/__init__.py +97 -115
- sgis/exceptions.py +3 -1
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +75 -38
- sgis/geopandas_tools/buffer_dissolve_explode.py +38 -34
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +149 -101
- sgis/geopandas_tools/duplicates.py +31 -17
- sgis/geopandas_tools/general.py +76 -48
- sgis/geopandas_tools/geometry_types.py +21 -7
- sgis/geopandas_tools/neighbors.py +20 -8
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +9 -8
- sgis/geopandas_tools/polygon_operations.py +48 -56
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +14 -14
- sgis/helpers.py +114 -56
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +39 -26
- sgis/maps/explore.py +112 -66
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +124 -65
- sgis/maps/map.py +66 -41
- sgis/maps/maps.py +31 -29
- sgis/maps/thematicmap.py +46 -33
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +20 -62
- sgis/networkanalysis/cutting_lines.py +55 -43
- sgis/networkanalysis/directednetwork.py +15 -7
- sgis/networkanalysis/finding_isolated_networks.py +4 -3
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +72 -54
- sgis/networkanalysis/networkanalysisrules.py +20 -16
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +5 -2
- sgis/parallel/parallel.py +337 -127
- sgis/raster/__init__.py +6 -0
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +280 -208
- sgis/raster/cubebase.py +15 -29
- sgis/raster/indices.py +3 -7
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/raster.py +313 -127
- sgis/raster/torchgeo.py +58 -37
- sgis/raster/zonal.py +38 -13
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +89 -18
- ssb_sgis-1.0.2.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- ssb_sgis-1.0.0.dist-info/RECORD +0 -63
sgis/maps/httpserver.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import webbrowser
|
|
3
|
-
from http.server import BaseHTTPRequestHandler
|
|
3
|
+
from http.server import BaseHTTPRequestHandler
|
|
4
|
+
from http.server import HTTPServer
|
|
4
5
|
|
|
5
|
-
from IPython.display import HTML
|
|
6
|
+
from IPython.display import HTML
|
|
7
|
+
from IPython.display import display
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def run_html_server(contents: str | None = None, port: int = 3000):
|
|
10
|
+
def run_html_server(contents: str | None = None, port: int = 3000) -> None:
|
|
9
11
|
"""Run a simple, temporary http web server for serving static HTML content."""
|
|
10
12
|
if "JUPYTERHUB_SERVICE_PREFIX" in os.environ:
|
|
11
13
|
# Create a link using the https://github.com/jupyterhub/jupyter-server-proxy
|
|
12
|
-
display_address = os.environ["JUPYTERHUB_SERVICE_PREFIX"] + "proxy/{}/"
|
|
13
|
-
port
|
|
14
|
-
)
|
|
14
|
+
display_address = os.environ["JUPYTERHUB_SERVICE_PREFIX"] + f"proxy/{port}/"
|
|
15
15
|
display_content = HTML(
|
|
16
16
|
f"""
|
|
17
17
|
<p>Click <a href='{display_address}'>here</a> to open in browser.</p>
|
|
@@ -20,15 +20,15 @@ def run_html_server(contents: str | None = None, port: int = 3000):
|
|
|
20
20
|
)
|
|
21
21
|
else:
|
|
22
22
|
display_address = f"http://localhost:{port}"
|
|
23
|
-
display_content = (
|
|
24
|
-
f"
|
|
25
|
-
|
|
23
|
+
display_content = HTML(
|
|
24
|
+
f"""
|
|
25
|
+
<p>Click <a href='http://localhost:{port}'>here</a> to open in browser.</p>
|
|
26
|
+
<p>Click <a href='http://localhost:{port}/stop'>here</a> to stop.<p>"
|
|
27
|
+
"""
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
class HTTPServerRequestHandler(BaseHTTPRequestHandler):
|
|
29
|
-
"""
|
|
30
|
-
A handler of request for the server, hosting static content.
|
|
31
|
-
"""
|
|
31
|
+
"""A handler of request for the server, hosting static content."""
|
|
32
32
|
|
|
33
33
|
allow_reuse_address = True
|
|
34
34
|
|
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
|
|
@@ -52,25 +55,38 @@ class Legend:
|
|
|
52
55
|
'm' is the name of the ThematicMap instance. See here:
|
|
53
56
|
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
|
|
54
57
|
|
|
55
|
-
Examples
|
|
58
|
+
Examples:
|
|
56
59
|
--------
|
|
57
|
-
Create ten
|
|
60
|
+
Create ten points with a numeric column from 0 to 9.
|
|
58
61
|
|
|
59
62
|
>>> import sgis as sg
|
|
60
|
-
>>> points = sg.
|
|
63
|
+
>>> points = sg.to_gdf(
|
|
64
|
+
... [
|
|
65
|
+
... (0, 1),
|
|
66
|
+
... (1, 0),
|
|
67
|
+
... (1, 1),
|
|
68
|
+
... (0, 0),
|
|
69
|
+
... (0.5, 0.5),
|
|
70
|
+
... (0.5, 0.25),
|
|
71
|
+
... (0.25, 0.25),
|
|
72
|
+
... (0.75, 0.75),
|
|
73
|
+
... (0.25, 0.75),
|
|
74
|
+
... (0.75, 0.25),
|
|
75
|
+
... ]
|
|
76
|
+
... )
|
|
61
77
|
>>> points["number"] = range(10)
|
|
62
78
|
>>> points
|
|
63
|
-
|
|
64
|
-
0 POINT (0.
|
|
65
|
-
1 POINT (
|
|
66
|
-
2 POINT (
|
|
67
|
-
3 POINT (0.
|
|
68
|
-
4 POINT (0.
|
|
69
|
-
5 POINT (0.
|
|
70
|
-
6 POINT (0.
|
|
71
|
-
7 POINT (0.
|
|
72
|
-
8 POINT (0.
|
|
73
|
-
9 POINT (0.
|
|
79
|
+
geometry number
|
|
80
|
+
0 POINT (0.00000 1.00000) 0
|
|
81
|
+
1 POINT (1.00000 0.00000) 1
|
|
82
|
+
2 POINT (1.00000 1.00000) 2
|
|
83
|
+
3 POINT (0.00000 0.00000) 3
|
|
84
|
+
4 POINT (0.50000 0.50000) 4
|
|
85
|
+
5 POINT (0.50000 0.25000) 5
|
|
86
|
+
6 POINT (0.25000 0.25000) 6
|
|
87
|
+
7 POINT (0.75000 0.75000) 7
|
|
88
|
+
8 POINT (0.25000 0.75000) 8
|
|
89
|
+
9 POINT (0.75000 0.25000) 9
|
|
74
90
|
|
|
75
91
|
Creating the ThematicMap instance will also create the legend. Since we
|
|
76
92
|
specify a numeric column, a ContinousLegend instance is created.
|
|
@@ -113,7 +129,30 @@ class Legend:
|
|
|
113
129
|
framealpha: float = 1.0,
|
|
114
130
|
edgecolor: str = "#0f0f0f",
|
|
115
131
|
**kwargs,
|
|
116
|
-
):
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Initialiser.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
title: Legend title. Defaults to the column name if used in the
|
|
137
|
+
ThematicMap class.
|
|
138
|
+
labels: Labels of the categories.
|
|
139
|
+
position: The legend's x and y position in the plot, specified as a tuple of
|
|
140
|
+
x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
|
|
141
|
+
in the bottom right corner, (0.2, 0.8) for the upper left corner.
|
|
142
|
+
fontsize: Text size of the legend labels. Defaults to the size of
|
|
143
|
+
the ThematicMap class.
|
|
144
|
+
title_fontsize: Text size of the legend title. Defaults to the
|
|
145
|
+
size * 1.2 of the ThematicMap class.
|
|
146
|
+
markersize: Size of the color circles in the legend. Defaults to the size of
|
|
147
|
+
the ThematicMap class.
|
|
148
|
+
framealpha: Transparency of the legend background.
|
|
149
|
+
edgecolor: Color of the legend border. Defaults to #0f0f0f (almost black).
|
|
150
|
+
kwargs: Stores additional keyword arguments taken by the matplotlib legend
|
|
151
|
+
method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
|
|
152
|
+
'm' is the name of the ThematicMap instance. See here:
|
|
153
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
|
|
154
|
+
|
|
155
|
+
"""
|
|
117
156
|
self.title = title
|
|
118
157
|
|
|
119
158
|
if "size" in kwargs:
|
|
@@ -136,9 +175,8 @@ class Legend:
|
|
|
136
175
|
self.kwargs = kwargs
|
|
137
176
|
self._position_has_been_set = True if position else False
|
|
138
177
|
|
|
139
|
-
def _get_legend_sizes(self, size, kwargs):
|
|
178
|
+
def _get_legend_sizes(self, size: int | float, kwargs: dict) -> None:
|
|
140
179
|
"""Adjust fontsize and markersize to size kwarg."""
|
|
141
|
-
|
|
142
180
|
if "title_fontsize" in kwargs:
|
|
143
181
|
self._title_fontsize = kwargs["title_fontsize"]
|
|
144
182
|
self._title_fontsize_has_been_set = True
|
|
@@ -157,7 +195,9 @@ class Legend:
|
|
|
157
195
|
else:
|
|
158
196
|
self._markersize = size
|
|
159
197
|
|
|
160
|
-
def _prepare_categorical_legend(
|
|
198
|
+
def _prepare_categorical_legend(
|
|
199
|
+
self, categories_colors: dict, nan_label: str
|
|
200
|
+
) -> None:
|
|
161
201
|
for attr in self.__dict__.keys():
|
|
162
202
|
if attr in self.kwargs:
|
|
163
203
|
self[attr] = self.kwargs.pop(attr)
|
|
@@ -195,7 +235,7 @@ class Legend:
|
|
|
195
235
|
)
|
|
196
236
|
)
|
|
197
237
|
|
|
198
|
-
def _actually_add_legend(self, ax):
|
|
238
|
+
def _actually_add_legend(self, ax: matplotlib.axes.Axes) -> matplotlib.axes.Axes:
|
|
199
239
|
legend = ax.legend(
|
|
200
240
|
self._patches,
|
|
201
241
|
self._categories,
|
|
@@ -215,7 +255,9 @@ class Legend:
|
|
|
215
255
|
|
|
216
256
|
return ax
|
|
217
257
|
|
|
218
|
-
def _get_best_legend_position(
|
|
258
|
+
def _get_best_legend_position(
|
|
259
|
+
self, gdf: GeoDataFrame, k: int
|
|
260
|
+
) -> tuple[float, float]:
|
|
219
261
|
minx, miny, maxx, maxy = gdf.total_bounds
|
|
220
262
|
diffx = maxx - minx
|
|
221
263
|
diffy = maxy - miny
|
|
@@ -243,51 +285,58 @@ class Legend:
|
|
|
243
285
|
|
|
244
286
|
return bestx_01, besty_01
|
|
245
287
|
|
|
246
|
-
def __getitem__(self, item):
|
|
288
|
+
def __getitem__(self, item: str) -> Any:
|
|
289
|
+
"""Get attribute with square brackets."""
|
|
247
290
|
return getattr(self, item)
|
|
248
291
|
|
|
249
|
-
def __setitem__(self, key, value):
|
|
292
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
293
|
+
"""Set attribute with square brackets."""
|
|
250
294
|
setattr(self, key, value)
|
|
251
295
|
|
|
252
|
-
def get(self, key, default=None):
|
|
296
|
+
def get(self, key: Any, default: Any = None) -> Any:
|
|
297
|
+
"""Get value of an attribute of the Legend."""
|
|
253
298
|
try:
|
|
254
299
|
return self[key]
|
|
255
300
|
except (KeyError, ValueError, IndexError, AttributeError):
|
|
256
301
|
return default
|
|
257
302
|
|
|
258
303
|
@property
|
|
259
|
-
def position(self):
|
|
304
|
+
def position(self) -> tuple[float, float]:
|
|
305
|
+
"""Legend position in x, y."""
|
|
260
306
|
return self._position
|
|
261
307
|
|
|
262
308
|
@position.setter
|
|
263
|
-
def position(self, new_value:
|
|
309
|
+
def position(self, new_value: tuple[float, float]) -> None:
|
|
264
310
|
self._position = new_value
|
|
265
311
|
self._position_has_been_set = True
|
|
266
312
|
|
|
267
313
|
@property
|
|
268
|
-
def title_fontsize(self):
|
|
314
|
+
def title_fontsize(self) -> int:
|
|
315
|
+
"""Legend title fontsize."""
|
|
269
316
|
return self._title_fontsize
|
|
270
317
|
|
|
271
318
|
@title_fontsize.setter
|
|
272
|
-
def title_fontsize(self, new_value:
|
|
319
|
+
def title_fontsize(self, new_value: int) -> None:
|
|
273
320
|
self._title_fontsize = new_value
|
|
274
321
|
self._title_fontsize_has_been_set = True
|
|
275
322
|
|
|
276
323
|
@property
|
|
277
|
-
def fontsize(self):
|
|
324
|
+
def fontsize(self) -> int:
|
|
325
|
+
"""Legend fontsize."""
|
|
278
326
|
return self._fontsize
|
|
279
327
|
|
|
280
328
|
@fontsize.setter
|
|
281
|
-
def fontsize(self, new_value:
|
|
329
|
+
def fontsize(self, new_value: int) -> None:
|
|
282
330
|
self._fontsize = new_value
|
|
283
331
|
self._fontsize_has_been_set = True
|
|
284
332
|
|
|
285
333
|
@property
|
|
286
|
-
def markersize(self):
|
|
334
|
+
def markersize(self) -> int:
|
|
335
|
+
"""Legend markersize."""
|
|
287
336
|
return self._markersize
|
|
288
337
|
|
|
289
338
|
@markersize.setter
|
|
290
|
-
def markersize(self, new_value:
|
|
339
|
+
def markersize(self, new_value: int) -> None:
|
|
291
340
|
self._markersize = new_value
|
|
292
341
|
self._markersize_has_been_set = True
|
|
293
342
|
|
|
@@ -321,7 +370,7 @@ class ContinousLegend(Legend):
|
|
|
321
370
|
unless 'thousand_sep' is '.'. In this case, ',' (comma) will be used as
|
|
322
371
|
decimal mark.
|
|
323
372
|
|
|
324
|
-
Examples
|
|
373
|
+
Examples:
|
|
325
374
|
--------
|
|
326
375
|
Create ten random points with a numeric column from 0 to 9.
|
|
327
376
|
|
|
@@ -386,7 +435,36 @@ class ContinousLegend(Legend):
|
|
|
386
435
|
thousand_sep: str | None = None,
|
|
387
436
|
decimal_mark: str | None = None,
|
|
388
437
|
**kwargs,
|
|
389
|
-
):
|
|
438
|
+
) -> None:
|
|
439
|
+
"""Initialiser.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
labels: To manually set labels. If set, all other labeling attributes are
|
|
443
|
+
ignored. Should be given as a list of strings with the same length as
|
|
444
|
+
the number of color groups.
|
|
445
|
+
pretty_labels: If False (default), the minimum and maximum values of each
|
|
446
|
+
color group will be used as legend labels. If True, the labels will end
|
|
447
|
+
with the maximum value, but start at 1 + the maximum value of the previous
|
|
448
|
+
group. The labels will be correct but inaccurate.
|
|
449
|
+
label_suffix: The text to put after each number in the legend labels.
|
|
450
|
+
Defaults to None.
|
|
451
|
+
label_sep: Text to put in between the two numbers in each color group in
|
|
452
|
+
the legend. Defaults to '-'.
|
|
453
|
+
rounding: Number of decimals in the labels. By default, the rounding
|
|
454
|
+
depends on the column's maximum value and standard deviation.
|
|
455
|
+
OBS: The bins will not be rounded, meaning the labels might be wrong
|
|
456
|
+
if not bins are set manually.
|
|
457
|
+
thousand_sep: Separator between each thousand for large numbers. Defaults to
|
|
458
|
+
None, meaning no separator.
|
|
459
|
+
decimal_mark: Text to use as decimal point. Defaults to None, meaning '.' (dot)
|
|
460
|
+
unless 'thousand_sep' is '.'. In this case, ',' (comma) will be used as
|
|
461
|
+
decimal mark.
|
|
462
|
+
kwargs: Stores additional keyword arguments taken by the matplotlib legend
|
|
463
|
+
method. Specify this as e.g. m.legend.kwargs["labelcolor"] = "red", where
|
|
464
|
+
'm' is the name of the ThematicMap instance. See here:
|
|
465
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html
|
|
466
|
+
|
|
467
|
+
"""
|
|
390
468
|
super().__init__(**kwargs)
|
|
391
469
|
|
|
392
470
|
self.pretty_labels = pretty_labels
|
|
@@ -423,13 +501,13 @@ class ContinousLegend(Legend):
|
|
|
423
501
|
return int(abs(np.log10(std_))) + 1
|
|
424
502
|
|
|
425
503
|
@staticmethod
|
|
426
|
-
def _set_rounding(bins, rounding: int | float):
|
|
504
|
+
def _set_rounding(bins, rounding: int | float) -> list[int | float]:
|
|
427
505
|
if rounding == 0:
|
|
428
|
-
return [int(round(
|
|
506
|
+
return [int(round(bin_, 0)) for bin_ in bins]
|
|
429
507
|
else:
|
|
430
|
-
return [round(
|
|
508
|
+
return [round(bin_, rounding) for bin_ in bins]
|
|
431
509
|
|
|
432
|
-
def _remove_max_legend_value(self):
|
|
510
|
+
def _remove_max_legend_value(self) -> None:
|
|
433
511
|
if not self._legend:
|
|
434
512
|
raise ValueError("Cannot modify legend before it is created.")
|
|
435
513
|
|
|
@@ -439,7 +517,7 @@ class ContinousLegend(Legend):
|
|
|
439
517
|
colors: list[str],
|
|
440
518
|
nan_label: str,
|
|
441
519
|
bin_values: dict,
|
|
442
|
-
):
|
|
520
|
+
) -> None:
|
|
443
521
|
# TODO: clean up this messy method
|
|
444
522
|
|
|
445
523
|
for attr in self.__dict__.keys():
|
|
@@ -491,7 +569,7 @@ class ContinousLegend(Legend):
|
|
|
491
569
|
)
|
|
492
570
|
|
|
493
571
|
else:
|
|
494
|
-
for i, (cat1, cat2) in enumerate(
|
|
572
|
+
for i, (cat1, cat2) in enumerate(itertools.pairwise(bins)):
|
|
495
573
|
if nan_label in str(cat1) or nan_label in str(cat2):
|
|
496
574
|
self._categories.append(nan_label)
|
|
497
575
|
continue
|
|
@@ -527,36 +605,16 @@ class ContinousLegend(Legend):
|
|
|
527
605
|
label = self._two_value_label(min_rounded, max_rounded)
|
|
528
606
|
self._categories.append(label)
|
|
529
607
|
|
|
530
|
-
def
|
|
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):
|
|
608
|
+
def _two_value_label(self, value1: int | float, value2: int | float) -> str:
|
|
551
609
|
return (
|
|
552
610
|
f"{value1} {self.label_suffix} {self.label_sep} "
|
|
553
611
|
f"{value2} {self.label_suffix}"
|
|
554
612
|
)
|
|
555
613
|
|
|
556
|
-
def _one_value_label(self, value1):
|
|
614
|
+
def _one_value_label(self, value1: int | float) -> str:
|
|
557
615
|
return f"{value1} {self.label_suffix}"
|
|
558
616
|
|
|
559
|
-
def _format_number(self, number):
|
|
617
|
+
def _format_number(self, number: int | float) -> int | float:
|
|
560
618
|
if not self.thousand_sep and not self.decimal_mark:
|
|
561
619
|
return number
|
|
562
620
|
|
|
@@ -581,10 +639,11 @@ class ContinousLegend(Legend):
|
|
|
581
639
|
return number
|
|
582
640
|
|
|
583
641
|
@property
|
|
584
|
-
def rounding(self):
|
|
642
|
+
def rounding(self) -> int:
|
|
643
|
+
"""Number rounding."""
|
|
585
644
|
return self._rounding
|
|
586
645
|
|
|
587
646
|
@rounding.setter
|
|
588
|
-
def rounding(self, new_value:
|
|
647
|
+
def rounding(self, new_value: int) -> None:
|
|
589
648
|
self._rounding = new_value
|
|
590
649
|
self._rounding_has_been_set = True
|
sgis/maps/map.py
CHANGED
|
@@ -4,32 +4,31 @@ This module holds the Map class, which is the basis for the Explore class.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import warnings
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
import matplotlib
|
|
9
10
|
import matplotlib.colors as colors
|
|
10
11
|
import numpy as np
|
|
11
12
|
import pandas as pd
|
|
12
|
-
from geopandas import GeoDataFrame
|
|
13
|
+
from geopandas import GeoDataFrame
|
|
14
|
+
from geopandas import GeoSeries
|
|
13
15
|
from jenkspy import jenks_breaks
|
|
14
16
|
from mapclassify import classify
|
|
15
17
|
from shapely import Geometry
|
|
16
18
|
|
|
17
19
|
from ..geopandas_tools.conversion import to_gdf
|
|
18
|
-
from ..geopandas_tools.general import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
rename_geometry_if,
|
|
23
|
-
)
|
|
20
|
+
from ..geopandas_tools.general import _rename_geometry_if
|
|
21
|
+
from ..geopandas_tools.general import clean_geoms
|
|
22
|
+
from ..geopandas_tools.general import drop_inactive_geometry_columns
|
|
23
|
+
from ..geopandas_tools.general import get_common_crs
|
|
24
24
|
from ..helpers import get_object_name
|
|
25
25
|
|
|
26
|
-
|
|
27
26
|
try:
|
|
28
27
|
from torchgeo.datasets.geo import RasterDataset
|
|
29
28
|
except ImportError:
|
|
30
29
|
|
|
31
30
|
class RasterDataset:
|
|
32
|
-
"""Placeholder"""
|
|
31
|
+
"""Placeholder."""
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
# the geopandas._explore raises a deprication warning. Ignoring for now.
|
|
@@ -60,8 +59,16 @@ _CATEGORICAL_CMAP = {
|
|
|
60
59
|
DEFAULT_SCHEME = "quantiles"
|
|
61
60
|
|
|
62
61
|
|
|
63
|
-
def proper_fillna(val, fill_val):
|
|
64
|
-
"""fillna doesn't
|
|
62
|
+
def proper_fillna(val: Any, fill_val: Any) -> Any:
|
|
63
|
+
"""Manually handle missing values when fillna doesn't work as expected.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
val: The value to check and fill.
|
|
67
|
+
fill_val: The value to fill in.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The original value or the filled value if conditions are met.
|
|
71
|
+
"""
|
|
65
72
|
try:
|
|
66
73
|
if "NAType" in val.__class__.__name__:
|
|
67
74
|
return fill_val
|
|
@@ -91,8 +98,20 @@ class Map:
|
|
|
91
98
|
nan_color="#c2c2c2",
|
|
92
99
|
scheme: str = DEFAULT_SCHEME,
|
|
93
100
|
**kwargs,
|
|
94
|
-
):
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Initialiser.
|
|
95
103
|
|
|
104
|
+
Args:
|
|
105
|
+
*gdfs: Variable length GeoDataFrame list.
|
|
106
|
+
column: The column name to work with.
|
|
107
|
+
labels: Tuple of labels for each GeoDataFrame.
|
|
108
|
+
k: Number of bins or classes for classification (default: 5).
|
|
109
|
+
bins: Predefined bins for data classification.
|
|
110
|
+
nan_label: Label for missing data.
|
|
111
|
+
nan_color: Color for missing data.
|
|
112
|
+
scheme: Classification scheme to be used.
|
|
113
|
+
**kwargs: Arbitrary keyword arguments.
|
|
114
|
+
"""
|
|
96
115
|
gdfs, column, kwargs = self._separate_args(gdfs, column, kwargs)
|
|
97
116
|
|
|
98
117
|
self._column = column
|
|
@@ -138,7 +157,7 @@ class Map:
|
|
|
138
157
|
self._gdfs = []
|
|
139
158
|
new_labels = []
|
|
140
159
|
self.show = []
|
|
141
|
-
for label, gdf, show in zip(self.labels, gdfs, show_args):
|
|
160
|
+
for label, gdf, show in zip(self.labels, gdfs, show_args, strict=False):
|
|
142
161
|
if not len(gdf):
|
|
143
162
|
continue
|
|
144
163
|
|
|
@@ -189,7 +208,7 @@ class Map:
|
|
|
189
208
|
)
|
|
190
209
|
|
|
191
210
|
if not any(len(gdf) for gdf in self._gdfs):
|
|
192
|
-
warnings.warn("None of the GeoDataFrames have rows.")
|
|
211
|
+
warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
|
|
193
212
|
self._gdfs = None
|
|
194
213
|
self._is_categorical = True
|
|
195
214
|
self._unique_values = []
|
|
@@ -222,7 +241,7 @@ class Map:
|
|
|
222
241
|
self._nan_idx = self._gdf[self._column].isna()
|
|
223
242
|
self._get_unique_values()
|
|
224
243
|
|
|
225
|
-
def _get_unique_values(self):
|
|
244
|
+
def _get_unique_values(self) -> None:
|
|
226
245
|
if not self._is_categorical:
|
|
227
246
|
self._unique_values = self._get_unique_floats()
|
|
228
247
|
else:
|
|
@@ -254,7 +273,7 @@ class Map:
|
|
|
254
273
|
|
|
255
274
|
return np.sort(np.array(unique.loc[no_duplicates.index]))
|
|
256
275
|
|
|
257
|
-
def _array_to_large_int(self, array):
|
|
276
|
+
def _array_to_large_int(self, array: np.ndarray | pd.Series) -> pd.Series:
|
|
258
277
|
"""Multiply values in float array, then convert to integer."""
|
|
259
278
|
if not isinstance(array, pd.Series):
|
|
260
279
|
array = pd.Series(array)
|
|
@@ -266,9 +285,8 @@ class Map:
|
|
|
266
285
|
|
|
267
286
|
return pd.concat([unique_multiplied, isna]).sort_index()
|
|
268
287
|
|
|
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.
|
|
288
|
+
def _get_multiplier(self, array: np.ndarray) -> None:
|
|
289
|
+
"""Find the number of zeros needed to push the max value of the array above +-1_000_000.
|
|
272
290
|
|
|
273
291
|
Adding this as an attribute to use later in _classify_from_bins.
|
|
274
292
|
"""
|
|
@@ -293,7 +311,7 @@ class Map:
|
|
|
293
311
|
def _add_minmax_to_bins(self, bins: list[float | int]) -> list[float | int]:
|
|
294
312
|
"""If values are outside the bin range, add max and/or min values of array."""
|
|
295
313
|
# make sure they are lists
|
|
296
|
-
bins = [
|
|
314
|
+
bins = [bin_ for bin_ in bins]
|
|
297
315
|
|
|
298
316
|
if min(bins) > 0 and min(self._gdf.loc[~self._nan_idx, self._column]) < min(
|
|
299
317
|
bins
|
|
@@ -371,9 +389,8 @@ class Map:
|
|
|
371
389
|
|
|
372
390
|
return gdfs, column, kwargs
|
|
373
391
|
|
|
374
|
-
def _prepare_continous_map(self):
|
|
392
|
+
def _prepare_continous_map(self) -> None:
|
|
375
393
|
"""Create bins if not already done and adjust k if needed."""
|
|
376
|
-
|
|
377
394
|
if self.scheme is None:
|
|
378
395
|
return
|
|
379
396
|
|
|
@@ -408,7 +425,9 @@ class Map:
|
|
|
408
425
|
gdfs.append(gdf)
|
|
409
426
|
self._gdfs = gdfs
|
|
410
427
|
|
|
411
|
-
def _to_common_crs_and_one_geom_col(
|
|
428
|
+
def _to_common_crs_and_one_geom_col(
|
|
429
|
+
self, gdfs: list[GeoDataFrame]
|
|
430
|
+
) -> list[GeoDataFrame]:
|
|
412
431
|
"""Need common crs and max one geometry column."""
|
|
413
432
|
crs_list = list({gdf.crs for gdf in gdfs if gdf.crs is not None})
|
|
414
433
|
if crs_list:
|
|
@@ -416,7 +435,7 @@ class Map:
|
|
|
416
435
|
new_gdfs = []
|
|
417
436
|
for gdf in gdfs:
|
|
418
437
|
gdf = gdf.reset_index(drop=True)
|
|
419
|
-
gdf = drop_inactive_geometry_columns(gdf).pipe(
|
|
438
|
+
gdf = drop_inactive_geometry_columns(gdf).pipe(_rename_geometry_if)
|
|
420
439
|
if crs_list:
|
|
421
440
|
try:
|
|
422
441
|
gdf = gdf.to_crs(self.crs)
|
|
@@ -530,7 +549,6 @@ class Map:
|
|
|
530
549
|
If 'scheme' is not specified, the jenks_breaks function is used, which is
|
|
531
550
|
much faster than the one from Mapclassifier.
|
|
532
551
|
"""
|
|
533
|
-
|
|
534
552
|
if not len(gdf.loc[~self._nan_idx, column]):
|
|
535
553
|
return np.array([0])
|
|
536
554
|
|
|
@@ -563,7 +581,7 @@ class Map:
|
|
|
563
581
|
bins = binning.bins
|
|
564
582
|
bins = self._add_minmax_to_bins(bins)
|
|
565
583
|
|
|
566
|
-
unique_bins = list({round(
|
|
584
|
+
unique_bins = list({round(bin_, 5) for bin_ in bins})
|
|
567
585
|
unique_bins.sort()
|
|
568
586
|
|
|
569
587
|
if self._k == len(self._unique_values) - 1:
|
|
@@ -577,7 +595,7 @@ class Map:
|
|
|
577
595
|
|
|
578
596
|
return np.array(bins)
|
|
579
597
|
|
|
580
|
-
def change_cmap(self, cmap: str, start: int = 0, stop: int = 256):
|
|
598
|
+
def change_cmap(self, cmap: str, start: int = 0, stop: int = 256) -> "Map":
|
|
581
599
|
"""Change the color palette of the plot.
|
|
582
600
|
|
|
583
601
|
Args:
|
|
@@ -606,7 +624,6 @@ class Map:
|
|
|
606
624
|
|
|
607
625
|
def _classify_from_bins(self, gdf: GeoDataFrame, bins: np.ndarray) -> np.ndarray:
|
|
608
626
|
"""Place the column values into groups."""
|
|
609
|
-
|
|
610
627
|
# if equal lenght, convert to integer and check for equality
|
|
611
628
|
if len(bins) == len(self._unique_values):
|
|
612
629
|
if gdf[self._column].isna().all():
|
|
@@ -647,11 +664,12 @@ class Map:
|
|
|
647
664
|
return np.array([rank_dict[val] for val in classified])
|
|
648
665
|
|
|
649
666
|
@property
|
|
650
|
-
def k(self):
|
|
667
|
+
def k(self) -> int:
|
|
668
|
+
"""Number of bins."""
|
|
651
669
|
return self._k
|
|
652
670
|
|
|
653
671
|
@k.setter
|
|
654
|
-
def k(self, new_value:
|
|
672
|
+
def k(self, new_value: int) -> None:
|
|
655
673
|
if not self._is_categorical and new_value > len(self._unique_values):
|
|
656
674
|
raise ValueError(
|
|
657
675
|
"'k' cannot be greater than the number of unique values in the column.'"
|
|
@@ -661,54 +679,61 @@ class Map:
|
|
|
661
679
|
self._k = int(new_value)
|
|
662
680
|
|
|
663
681
|
@property
|
|
664
|
-
def cmap(self):
|
|
682
|
+
def cmap(self) -> str:
|
|
683
|
+
"""Colormap."""
|
|
665
684
|
return self._cmap
|
|
666
685
|
|
|
667
686
|
@cmap.setter
|
|
668
|
-
def cmap(self, new_value:
|
|
687
|
+
def cmap(self, new_value: str) -> None:
|
|
669
688
|
self._cmap = new_value
|
|
670
689
|
self.change_cmap(cmap=new_value, start=self.cmap_start, stop=self.cmap_stop)
|
|
671
690
|
|
|
672
691
|
@property
|
|
673
|
-
def gdf(self):
|
|
692
|
+
def gdf(self) -> GeoDataFrame:
|
|
693
|
+
"""All GeoDataFrames concated."""
|
|
674
694
|
return self._gdf
|
|
675
695
|
|
|
676
696
|
@gdf.setter
|
|
677
|
-
def gdf(self, _):
|
|
697
|
+
def gdf(self, _) -> None:
|
|
678
698
|
raise ValueError(
|
|
679
699
|
"Cannot change 'gdf' after init. Put the GeoDataFrames into "
|
|
680
700
|
"the class initialiser."
|
|
681
701
|
)
|
|
682
702
|
|
|
683
703
|
@property
|
|
684
|
-
def gdfs(self):
|
|
704
|
+
def gdfs(self) -> list[GeoDataFrame]:
|
|
705
|
+
"""All GeoDataFrames as a list."""
|
|
685
706
|
return self._gdfs
|
|
686
707
|
|
|
687
708
|
@gdfs.setter
|
|
688
|
-
def gdfs(self, _):
|
|
709
|
+
def gdfs(self, _) -> None:
|
|
689
710
|
raise ValueError(
|
|
690
711
|
"Cannot change 'gdfs' after init. Put the GeoDataFrames into "
|
|
691
712
|
"the class initialiser."
|
|
692
713
|
)
|
|
693
714
|
|
|
694
715
|
@property
|
|
695
|
-
def column(self):
|
|
716
|
+
def column(self) -> str | None:
|
|
717
|
+
"""Column to use as colormap."""
|
|
696
718
|
return self._column
|
|
697
719
|
|
|
698
720
|
@column.setter
|
|
699
|
-
def column(self, _):
|
|
721
|
+
def column(self, _) -> None:
|
|
700
722
|
raise ValueError(
|
|
701
723
|
"Cannot change 'column' after init. Specify 'column' in the "
|
|
702
724
|
"class initialiser."
|
|
703
725
|
)
|
|
704
726
|
|
|
705
|
-
def __setitem__(self, item, new_item):
|
|
727
|
+
def __setitem__(self, item: Any, new_item: Any) -> None:
|
|
728
|
+
"""Set an attribute with square brackets."""
|
|
706
729
|
return setattr(self, item, new_item)
|
|
707
730
|
|
|
708
|
-
def __getitem__(self, item):
|
|
731
|
+
def __getitem__(self, item: Any) -> Any:
|
|
732
|
+
"""Get an attribute with square brackets."""
|
|
709
733
|
return getattr(self, item)
|
|
710
734
|
|
|
711
|
-
def get(self, key, default=None):
|
|
735
|
+
def get(self, key: Any, default: Any | None = None) -> Any:
|
|
736
|
+
"""Get an attribute with default value if not present."""
|
|
712
737
|
try:
|
|
713
738
|
return self[key]
|
|
714
739
|
except (KeyError, ValueError, IndexError, AttributeError):
|