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.
- sgis/__init__.py +20 -9
- sgis/debug_config.py +24 -0
- sgis/exceptions.py +2 -2
- sgis/geopandas_tools/bounds.py +33 -36
- sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +38 -19
- sgis/geopandas_tools/duplicates.py +29 -8
- sgis/geopandas_tools/general.py +263 -100
- sgis/geopandas_tools/geometry_types.py +4 -4
- sgis/geopandas_tools/neighbors.py +19 -15
- sgis/geopandas_tools/overlay.py +2 -2
- sgis/geopandas_tools/point_operations.py +5 -5
- sgis/geopandas_tools/polygon_operations.py +510 -105
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +29 -12
- sgis/helpers.py +3 -3
- sgis/io/dapla_functions.py +238 -19
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +27 -12
- sgis/maps/explore.py +450 -65
- sgis/maps/legend.py +177 -76
- sgis/maps/map.py +206 -103
- sgis/maps/maps.py +178 -105
- sgis/maps/thematicmap.py +243 -83
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/closing_network_holes.py +2 -2
- sgis/networkanalysis/cutting_lines.py +15 -8
- sgis/networkanalysis/directednetwork.py +1 -1
- sgis/networkanalysis/finding_isolated_networks.py +15 -8
- sgis/networkanalysis/networkanalysis.py +17 -19
- sgis/networkanalysis/networkanalysisrules.py +1 -1
- sgis/networkanalysis/traveling_salesman.py +1 -1
- sgis/parallel/parallel.py +64 -27
- sgis/raster/__init__.py +0 -6
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +54 -8
- sgis/raster/image_collection.py +3257 -0
- sgis/raster/indices.py +17 -5
- sgis/raster/raster.py +138 -243
- sgis/raster/sentinel_config.py +120 -0
- sgis/raster/zonal.py +0 -1
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.4.dist-info/RECORD +62 -0
- sgis/raster/methods_as_functions.py +0 -0
- sgis/raster/torchgeo.py +0 -171
- ssb_sgis-1.0.2.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/thematicmap.py
CHANGED
|
@@ -10,8 +10,10 @@ import numpy as np
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
from geopandas import GeoDataFrame
|
|
12
12
|
|
|
13
|
+
from .legend import LEGEND_KWARGS
|
|
13
14
|
from .legend import ContinousLegend
|
|
14
15
|
from .legend import Legend
|
|
16
|
+
from .legend import prettify_bins
|
|
15
17
|
from .map import Map
|
|
16
18
|
|
|
17
19
|
# the geopandas._explore raises a deprication warning. Ignoring for now.
|
|
@@ -20,60 +22,132 @@ warnings.filterwarnings(
|
|
|
20
22
|
)
|
|
21
23
|
pd.options.mode.chained_assignment = None
|
|
22
24
|
|
|
25
|
+
MAP_KWARGS = {
|
|
26
|
+
"bins",
|
|
27
|
+
"title",
|
|
28
|
+
"title_fontsize",
|
|
29
|
+
"size",
|
|
30
|
+
"cmap",
|
|
31
|
+
"cmap_start",
|
|
32
|
+
"cmap_stop",
|
|
33
|
+
"scheme",
|
|
34
|
+
"k",
|
|
35
|
+
"column",
|
|
36
|
+
"title_color",
|
|
37
|
+
"facecolor",
|
|
38
|
+
"labelcolor",
|
|
39
|
+
"nan_color",
|
|
40
|
+
"title_kwargs",
|
|
41
|
+
"bg_gdf_color",
|
|
42
|
+
"title_position",
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
|
|
24
46
|
class ThematicMap(Map):
|
|
25
|
-
"""Class for
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
maximum value for the color groups.
|
|
40
|
-
cmap (str): Colormap of the plot. See:
|
|
47
|
+
"""Class for making static maps.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
*gdfs: One or more GeoDataFrames.
|
|
51
|
+
column: The name of the column to plot.
|
|
52
|
+
title: Title of the plot.
|
|
53
|
+
title_position: Title position. Either "center" (default), "left" or "right".
|
|
54
|
+
size: Width and height of the plot in inches. Fontsize of title and legend is
|
|
55
|
+
adjusted accordingly. Defaults to 25.
|
|
56
|
+
dark: If False (default), the background will be white and the text black. If
|
|
57
|
+
True, the background will be black and the text white. When True, the
|
|
58
|
+
default cmap is "viridis", and when False, the default is red to purple
|
|
59
|
+
(RdPu).
|
|
60
|
+
cmap: Colormap of the plot. See:
|
|
41
61
|
https://matplotlib.org/stable/tutorials/colors/colormaps.html
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
scheme: How to devide numeric values into categories. Defaults to
|
|
63
|
+
"naturalbreaks".
|
|
64
|
+
k: Number of color groups.
|
|
65
|
+
bins: For numeric columns. List of numbers that define the
|
|
66
|
+
maximum value for the color groups.
|
|
67
|
+
nan_label: Label for missing data.
|
|
68
|
+
legend_kwargs: dictionary with attributes for the legend. E.g.:
|
|
69
|
+
title: Legend title. Defaults to the column name.
|
|
70
|
+
rounding: If positive number, it will round floats to n decimals.
|
|
71
|
+
If negative, eg. -2, the number 3429 is rounded to 3400.
|
|
72
|
+
By default, the rounding depends on the column's maximum value
|
|
73
|
+
and standard deviation.
|
|
74
|
+
position: The legend's x and y position in the plot. By default, it's
|
|
75
|
+
decided dynamically by finding the space with most distance to
|
|
76
|
+
the geometries. To be specified as a tuple of
|
|
77
|
+
x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
|
|
78
|
+
in the bottom right corner, (0.2, 0.8) for the upper left corner.
|
|
79
|
+
pretty_labels: Whether to capitalize words in text categories.
|
|
80
|
+
label_suffix: For numeric columns. The text to put after each number
|
|
81
|
+
in the legend labels. Defaults to None.
|
|
82
|
+
label_sep: For numeric columns. Text to put in between the two numbers
|
|
83
|
+
in each color group in the legend. Defaults to '-'.
|
|
84
|
+
thousand_sep: For numeric columns. Separator between each thousand for
|
|
85
|
+
large numbers. Defaults to None, meaning no separator.
|
|
86
|
+
decimal_mark: For numeric columns. Text to use as decimal point.
|
|
87
|
+
Defaults to None, meaning '.' (dot) unless 'thousand_sep' is
|
|
88
|
+
'.'. In this case, ',' (comma) will be used as decimal mark.
|
|
89
|
+
**kwargs: Additional attributes for the map. E.g.:
|
|
90
|
+
title_color (str): Color of the title font.
|
|
91
|
+
title_fontsize (int): Color of the title font.
|
|
92
|
+
cmap_start (int): Start position for the color palette.
|
|
93
|
+
cmap_stop (int): End position for the color palette.
|
|
94
|
+
facecolor (str): Background color.
|
|
95
|
+
labelcolor (str): Color for the labels.
|
|
96
|
+
nan_color: Color for missing data.
|
|
45
97
|
|
|
46
98
|
Examples:
|
|
47
|
-
|
|
99
|
+
---------
|
|
48
100
|
>>> import sgis as sg
|
|
49
|
-
>>> points = sg.random_points(100).pipe(sg.buff, np.random.rand(100))
|
|
50
|
-
>>> points2 = sg.random_points(100).pipe(sg.buff, np.random.rand(100))
|
|
101
|
+
>>> points = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
|
|
102
|
+
>>> points2 = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
|
|
103
|
+
|
|
51
104
|
|
|
52
105
|
Simple plot with legend and title.
|
|
53
106
|
|
|
54
|
-
>>> m = sg.ThematicMap(points, points2, "area")
|
|
55
|
-
>>> m.title = "Area of random circles"
|
|
107
|
+
>>> m = sg.ThematicMap(points, points2, column="area", title="Area of random circles")
|
|
56
108
|
>>> m.plot()
|
|
57
109
|
|
|
58
|
-
Plot with custom legend units (label_suffix) and separator
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
>>> m
|
|
63
|
-
|
|
110
|
+
Plot with custom legend units (label_suffix) and thousand separator.
|
|
111
|
+
And with rounding set to -2, meaning e.g. 3429 is rounded to 3400.
|
|
112
|
+
If rounding was set to positive 2, 3429 would be rounded to 3429.00.
|
|
113
|
+
|
|
114
|
+
>>> m = sg.ThematicMap(
|
|
115
|
+
... points,
|
|
116
|
+
... points2,
|
|
117
|
+
... column="area",
|
|
118
|
+
... title = "Area of random circles",
|
|
119
|
+
... legend_kwargs=dict(
|
|
120
|
+
... rounding=-2,
|
|
121
|
+
... thousand_sep=" ",
|
|
122
|
+
... label_sep="to",
|
|
123
|
+
... ),
|
|
124
|
+
... )
|
|
64
125
|
>>> m.plot()
|
|
65
126
|
|
|
66
|
-
With custom bins and
|
|
67
|
-
|
|
68
|
-
>>> m = sg.ThematicMap(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...
|
|
73
|
-
...
|
|
74
|
-
...
|
|
75
|
-
...
|
|
76
|
-
...
|
|
127
|
+
With custom bins for the categories, and other customizations.
|
|
128
|
+
|
|
129
|
+
>>> m = sg.ThematicMap(
|
|
130
|
+
... points,
|
|
131
|
+
... points2,
|
|
132
|
+
... column="area",
|
|
133
|
+
... cmap="Greens",
|
|
134
|
+
... cmap_start=50,
|
|
135
|
+
... cmap_stop=255,
|
|
136
|
+
... nan_label="Missing",
|
|
137
|
+
... title = "Area of random circles",
|
|
138
|
+
... bins = [5000, 10000, 15000, 20000],
|
|
139
|
+
... title_kwargs=dict(
|
|
140
|
+
... loc="left",
|
|
141
|
+
... y=0.93,
|
|
142
|
+
... x=0.025,
|
|
143
|
+
... ),
|
|
144
|
+
... legend_kwargs=dict(
|
|
145
|
+
... thousand_sep=" ",
|
|
146
|
+
... label_sep="to",
|
|
147
|
+
... decimal_mark=".",
|
|
148
|
+
... label_suffix="m2",
|
|
149
|
+
... ),
|
|
150
|
+
... )
|
|
77
151
|
>>> m.plot()
|
|
78
152
|
"""
|
|
79
153
|
|
|
@@ -81,37 +155,105 @@ class ThematicMap(Map):
|
|
|
81
155
|
self,
|
|
82
156
|
*gdfs: GeoDataFrame,
|
|
83
157
|
column: str | None = None,
|
|
158
|
+
title: str | None = None,
|
|
159
|
+
title_position: tuple[float, float] | None = None,
|
|
84
160
|
size: int = 25,
|
|
85
|
-
|
|
161
|
+
dark: bool = False,
|
|
162
|
+
cmap: str | None = None,
|
|
163
|
+
scheme: str = "naturalbreaks",
|
|
164
|
+
k: int = 5,
|
|
165
|
+
bins: tuple[float] | None = None,
|
|
166
|
+
nan_label: str = "Missing",
|
|
167
|
+
legend_kwargs: dict | None = None,
|
|
168
|
+
title_kwargs: dict | None = None,
|
|
169
|
+
legend: bool = False,
|
|
170
|
+
**kwargs,
|
|
86
171
|
) -> None:
|
|
87
|
-
"""Initialiser.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
default cmap is "viridis", and when False, the default is red to purple
|
|
97
|
-
(RdPu).
|
|
172
|
+
"""Initialiser."""
|
|
173
|
+
super().__init__(
|
|
174
|
+
*gdfs,
|
|
175
|
+
column=column,
|
|
176
|
+
scheme=scheme,
|
|
177
|
+
k=k,
|
|
178
|
+
bins=bins,
|
|
179
|
+
nan_label=nan_label,
|
|
180
|
+
)
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
182
|
+
if not legend:
|
|
183
|
+
self.legend = None
|
|
101
184
|
|
|
185
|
+
self.title = title
|
|
102
186
|
self._size = size
|
|
103
|
-
self.
|
|
187
|
+
self._dark = dark
|
|
188
|
+
self.title_kwargs = title_kwargs or {}
|
|
189
|
+
if title_position and "position" in self.title_kwargs:
|
|
190
|
+
raise TypeError(
|
|
191
|
+
"Specify either 'title_position' or title_kwargs position, not both."
|
|
192
|
+
)
|
|
193
|
+
if title_position or "position" in self.title_kwargs:
|
|
194
|
+
position = self.title_kwargs.pop("position", title_position)
|
|
195
|
+
error_mess = (
|
|
196
|
+
"legend_kwargs position should be a two length tuple/list with two numbers between "
|
|
197
|
+
"0 and 1 (x, y position)"
|
|
198
|
+
)
|
|
199
|
+
if not hasattr(position, "__len__"):
|
|
200
|
+
raise TypeError(error_mess)
|
|
201
|
+
if len(position) != 2:
|
|
202
|
+
raise ValueError(error_mess)
|
|
203
|
+
x, y = position
|
|
204
|
+
if "loc" not in self.title_kwargs:
|
|
205
|
+
if x < 0.4:
|
|
206
|
+
self.title_kwargs["loc"] = "left"
|
|
207
|
+
elif x > 0.6:
|
|
208
|
+
self.title_kwargs["loc"] = "right"
|
|
209
|
+
else:
|
|
210
|
+
self.title_kwargs["loc"] = "center"
|
|
211
|
+
|
|
212
|
+
self.title_kwargs["x"], self.title_kwargs["y"] = x, y
|
|
104
213
|
self.background_gdfs = []
|
|
105
214
|
|
|
106
|
-
|
|
215
|
+
legend_kwargs = legend_kwargs or {}
|
|
107
216
|
|
|
108
|
-
self.
|
|
217
|
+
self._title_fontsize = self._size * 1.9
|
|
109
218
|
|
|
110
|
-
|
|
219
|
+
black = kwargs.pop("black", None)
|
|
220
|
+
self._dark = self._dark or black
|
|
221
|
+
|
|
222
|
+
if not self.cmap and not self._is_categorical:
|
|
111
223
|
self._choose_cmap()
|
|
112
224
|
|
|
225
|
+
self._dark_or_light()
|
|
113
226
|
self._create_legend()
|
|
114
227
|
|
|
228
|
+
if cmap:
|
|
229
|
+
self._cmap = cmap
|
|
230
|
+
|
|
231
|
+
for key, value in kwargs.items():
|
|
232
|
+
if key not in MAP_KWARGS:
|
|
233
|
+
raise TypeError(
|
|
234
|
+
f"{self.__class__.__name__} got an unexpected keyword argument {key}"
|
|
235
|
+
)
|
|
236
|
+
try:
|
|
237
|
+
setattr(self, key, value)
|
|
238
|
+
except Exception:
|
|
239
|
+
setattr(self, f"_{key}", value)
|
|
240
|
+
|
|
241
|
+
for key, value in legend_kwargs.items():
|
|
242
|
+
if key not in LEGEND_KWARGS:
|
|
243
|
+
raise TypeError(
|
|
244
|
+
f"{self.__class__.__name__} legend_kwargs got an unexpected key {key}"
|
|
245
|
+
)
|
|
246
|
+
if self.legend is not None:
|
|
247
|
+
try:
|
|
248
|
+
setattr(self.legend, key, value)
|
|
249
|
+
except Exception:
|
|
250
|
+
setattr(self.legend, f"_{key}", value)
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def valid_keywords(self) -> set[str]:
|
|
254
|
+
"""List all valid keywords for the class initialiser."""
|
|
255
|
+
return MAP_KWARGS
|
|
256
|
+
|
|
115
257
|
def change_cmap(self, cmap: str, start: int = 0, stop: int = 256) -> "ThematicMap":
|
|
116
258
|
"""Change the color palette of the plot.
|
|
117
259
|
|
|
@@ -177,7 +319,7 @@ class ThematicMap(Map):
|
|
|
177
319
|
else:
|
|
178
320
|
kwargs = self._prepare_continous_plot(kwargs)
|
|
179
321
|
if self.legend:
|
|
180
|
-
if not self.legend.
|
|
322
|
+
if not self.legend.rounding:
|
|
181
323
|
self.legend._rounding = self.legend._get_rounding(
|
|
182
324
|
array=self._gdf.loc[~self._nan_idx, self._column]
|
|
183
325
|
)
|
|
@@ -194,15 +336,15 @@ class ThematicMap(Map):
|
|
|
194
336
|
self._gdf, k=self._k + bool(len(self._nan_idx))
|
|
195
337
|
)
|
|
196
338
|
|
|
197
|
-
if __test:
|
|
198
|
-
return
|
|
199
|
-
|
|
200
339
|
self._prepare_plot(**kwargs)
|
|
201
340
|
|
|
202
341
|
if self.legend:
|
|
203
342
|
self.ax = self.legend._actually_add_legend(ax=self.ax)
|
|
204
343
|
|
|
205
|
-
self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
|
|
344
|
+
self.ax = self._gdf.plot(legend=include_legend, ax=self.ax, **kwargs)
|
|
345
|
+
|
|
346
|
+
if __test:
|
|
347
|
+
return self
|
|
206
348
|
|
|
207
349
|
def save(self, path: str) -> None:
|
|
208
350
|
"""Save figure as image file.
|
|
@@ -238,9 +380,13 @@ class ThematicMap(Map):
|
|
|
238
380
|
if hasattr(self, "_background_gdfs"):
|
|
239
381
|
self._actually_add_background()
|
|
240
382
|
|
|
241
|
-
if
|
|
383
|
+
if self.title:
|
|
242
384
|
self.ax.set_title(
|
|
243
|
-
self.title,
|
|
385
|
+
self.title,
|
|
386
|
+
**(
|
|
387
|
+
dict(fontsize=self.title_fontsize, color=self.title_color)
|
|
388
|
+
| self.title_kwargs
|
|
389
|
+
),
|
|
244
390
|
)
|
|
245
391
|
|
|
246
392
|
def _prepare_continous_plot(self, kwargs: dict) -> dict:
|
|
@@ -257,6 +403,13 @@ class ThematicMap(Map):
|
|
|
257
403
|
return kwargs
|
|
258
404
|
|
|
259
405
|
else:
|
|
406
|
+
if self.legend and self.legend.rounding and self.legend.rounding < 0:
|
|
407
|
+
self.bins = prettify_bins(self.bins, self.legend.rounding)
|
|
408
|
+
self.bins = list({round(bin_, 5) for bin_ in self.bins})
|
|
409
|
+
self.bins.sort()
|
|
410
|
+
# self.legend._rounding_was = self.legend.rounding
|
|
411
|
+
# self.legend.rounding = None
|
|
412
|
+
|
|
260
413
|
classified = self._classify_from_bins(self._gdf, bins=self.bins)
|
|
261
414
|
classified_sequential = self._push_classification(classified)
|
|
262
415
|
n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
|
|
@@ -264,10 +417,13 @@ class ThematicMap(Map):
|
|
|
264
417
|
self._bins_unique_values = self._make_bin_value_dict(
|
|
265
418
|
self._gdf, classified_sequential
|
|
266
419
|
)
|
|
420
|
+
|
|
267
421
|
colorarray = self._unique_colors[classified_sequential]
|
|
268
422
|
kwargs["color"] = colorarray
|
|
269
423
|
|
|
270
|
-
if
|
|
424
|
+
if (
|
|
425
|
+
self.legend and self.legend.rounding
|
|
426
|
+
): # not self.legend._rounding_has_been_set:
|
|
271
427
|
self.bins = self.legend._set_rounding(
|
|
272
428
|
bins=self.bins, rounding=self.legend._rounding
|
|
273
429
|
)
|
|
@@ -279,10 +435,13 @@ class ThematicMap(Map):
|
|
|
279
435
|
|
|
280
436
|
def _prepare_categorical_plot(self, kwargs: dict) -> dict:
|
|
281
437
|
"""Map values to colors."""
|
|
282
|
-
self.
|
|
283
|
-
|
|
438
|
+
self._make_categories_colors_dict()
|
|
439
|
+
if self._gdf is not None and len(self._gdf):
|
|
440
|
+
self._fix_nans()
|
|
284
441
|
|
|
285
|
-
|
|
442
|
+
if self._gdf is not None:
|
|
443
|
+
colorarray = self._gdf["color"]
|
|
444
|
+
kwargs["color"] = colorarray
|
|
286
445
|
return kwargs
|
|
287
446
|
|
|
288
447
|
def _actually_add_legend(self) -> None:
|
|
@@ -309,8 +468,10 @@ class ThematicMap(Map):
|
|
|
309
468
|
|
|
310
469
|
def _create_legend(self) -> None:
|
|
311
470
|
"""Instantiate the Legend class."""
|
|
471
|
+
if self.legend is None:
|
|
472
|
+
return
|
|
312
473
|
kwargs = {}
|
|
313
|
-
if self.
|
|
474
|
+
if self._dark:
|
|
314
475
|
kwargs["facecolor"] = "#0f0f0f"
|
|
315
476
|
kwargs["labelcolor"] = "#fefefe"
|
|
316
477
|
kwargs["title_color"] = "#fefefe"
|
|
@@ -322,7 +483,7 @@ class ThematicMap(Map):
|
|
|
322
483
|
|
|
323
484
|
def _choose_cmap(self) -> None:
|
|
324
485
|
"""Kwargs is to catch start and stop points for the cmap in __init__."""
|
|
325
|
-
if self.
|
|
486
|
+
if self._dark:
|
|
326
487
|
self._cmap = "viridis"
|
|
327
488
|
self.cmap_start = 0
|
|
328
489
|
self.cmap_stop = 256
|
|
@@ -352,8 +513,8 @@ class ThematicMap(Map):
|
|
|
352
513
|
ax = fig.add_subplot(1, 1, 1)
|
|
353
514
|
return fig, ax
|
|
354
515
|
|
|
355
|
-
def
|
|
356
|
-
if self.
|
|
516
|
+
def _dark_or_light(self) -> None:
|
|
517
|
+
if self._dark:
|
|
357
518
|
self.facecolor, self.title_color, self.bg_gdf_color = (
|
|
358
519
|
"#0f0f0f",
|
|
359
520
|
"#fefefe",
|
|
@@ -367,23 +528,22 @@ class ThematicMap(Map):
|
|
|
367
528
|
self.facecolor, self.title_color, self.bg_gdf_color = (
|
|
368
529
|
"#fefefe",
|
|
369
530
|
"#0f0f0f",
|
|
370
|
-
"#
|
|
531
|
+
"#dbdbdb",
|
|
371
532
|
)
|
|
372
533
|
self.nan_color = "#c2c2c2"
|
|
373
534
|
if not self._is_categorical:
|
|
374
535
|
self.change_cmap("RdPu", start=23)
|
|
375
536
|
|
|
376
|
-
self._create_legend()
|
|
377
|
-
|
|
378
537
|
@property
|
|
379
|
-
def
|
|
538
|
+
def dark(self) -> bool:
|
|
380
539
|
"""Whether to use dark background and light text colors."""
|
|
381
|
-
return self.
|
|
540
|
+
return self._dark
|
|
382
541
|
|
|
383
|
-
@
|
|
384
|
-
def
|
|
385
|
-
self.
|
|
386
|
-
self.
|
|
542
|
+
@dark.setter
|
|
543
|
+
def dark(self, new_value: bool):
|
|
544
|
+
self._dark = new_value
|
|
545
|
+
self._dark_or_light()
|
|
546
|
+
self._create_legend()
|
|
387
547
|
|
|
388
548
|
@property
|
|
389
549
|
def title_fontsize(self) -> int:
|
|
@@ -5,6 +5,7 @@ from igraph import Graph
|
|
|
5
5
|
from shapely import force_2d
|
|
6
6
|
from shapely import reverse
|
|
7
7
|
from shapely import unary_union
|
|
8
|
+
from shapely import union_all
|
|
8
9
|
from shapely.geometry import MultiPoint
|
|
9
10
|
from shapely.geometry import Point
|
|
10
11
|
from shapely.ops import nearest_points
|
|
@@ -113,7 +114,11 @@ def _service_area(
|
|
|
113
114
|
else:
|
|
114
115
|
snapped_origin: Point = nearest_points(
|
|
115
116
|
nodes_union,
|
|
116
|
-
|
|
117
|
+
union_all(
|
|
118
|
+
origins.loc[
|
|
119
|
+
origins["temp_idx"] == idx, "geometry"
|
|
120
|
+
].geometry.values
|
|
121
|
+
),
|
|
117
122
|
)[0]
|
|
118
123
|
|
|
119
124
|
within = sfilter(within, snapped_origin.buffer(0.01))
|
|
@@ -44,7 +44,7 @@ def close_network_holes(
|
|
|
44
44
|
NetworkAnalysis. These values must be filled before analysis.
|
|
45
45
|
|
|
46
46
|
Examples:
|
|
47
|
-
|
|
47
|
+
---------
|
|
48
48
|
Read road data with small gaps.
|
|
49
49
|
|
|
50
50
|
>>> import sgis as sg
|
|
@@ -157,7 +157,7 @@ def close_network_holes_to_deadends(
|
|
|
157
157
|
The input GeoDataFrame with new lines added.
|
|
158
158
|
|
|
159
159
|
Examples:
|
|
160
|
-
|
|
160
|
+
---------
|
|
161
161
|
Read road data with small gaps.
|
|
162
162
|
|
|
163
163
|
>>> import sgis as sg
|
|
@@ -52,7 +52,7 @@ def split_lines_by_nearest_point(
|
|
|
52
52
|
ValueError: If the crs of the input data differs.
|
|
53
53
|
|
|
54
54
|
Examples:
|
|
55
|
-
|
|
55
|
+
---------
|
|
56
56
|
>>> from sgis import read_parquet_url, split_lines_by_nearest_point
|
|
57
57
|
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
58
58
|
>>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
|
|
@@ -77,7 +77,7 @@ def split_lines_by_nearest_point(
|
|
|
77
77
|
"""
|
|
78
78
|
PRECISION = 1e-6
|
|
79
79
|
|
|
80
|
-
if not len(gdf):
|
|
80
|
+
if not len(gdf) or not len(points):
|
|
81
81
|
return gdf
|
|
82
82
|
|
|
83
83
|
if (points.crs is not None and gdf.crs is not None) and not points.crs.equals(
|
|
@@ -86,10 +86,14 @@ def split_lines_by_nearest_point(
|
|
|
86
86
|
raise ValueError("crs mismatch:", points.crs, "and", gdf.crs)
|
|
87
87
|
|
|
88
88
|
if get_geom_type(gdf) != "line":
|
|
89
|
-
raise ValueError(
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"'gdf' should only have line geometriess. Got {gdf.geom_type.value_counts()}"
|
|
91
|
+
)
|
|
90
92
|
|
|
91
93
|
if get_geom_type(points) != "point":
|
|
92
|
-
raise ValueError(
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"'points' should only have point geometries. Got {points.geom_type.value_counts()}"
|
|
96
|
+
)
|
|
93
97
|
|
|
94
98
|
gdf = gdf.copy()
|
|
95
99
|
|
|
@@ -230,9 +234,12 @@ def _change_line_endpoint(
|
|
|
230
234
|
.values
|
|
231
235
|
)
|
|
232
236
|
|
|
233
|
-
|
|
237
|
+
is_line = relevant_lines.groupby(level=0).size() > 1
|
|
238
|
+
relevant_lines_mapped = (
|
|
239
|
+
relevant_lines.loc[is_line].groupby(level=0)["geometry"].agg(LineString)
|
|
240
|
+
)
|
|
234
241
|
|
|
235
|
-
gdf.loc[
|
|
242
|
+
gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
|
|
236
243
|
|
|
237
244
|
return gdf
|
|
238
245
|
|
|
@@ -255,7 +262,7 @@ def cut_lines(
|
|
|
255
262
|
This method is time consuming for large networks and low 'max_length'.
|
|
256
263
|
|
|
257
264
|
Examples:
|
|
258
|
-
|
|
265
|
+
---------
|
|
259
266
|
>>> from sgis import read_parquet_url, cut_lines
|
|
260
267
|
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
261
268
|
>>> roads.length.describe().round(1)
|
|
@@ -327,7 +334,7 @@ def cut_lines_once(
|
|
|
327
334
|
Defaults to False.
|
|
328
335
|
|
|
329
336
|
Examples:
|
|
330
|
-
|
|
337
|
+
---------
|
|
331
338
|
>>> from sgis import cut_lines_once, to_gdf
|
|
332
339
|
>>> import pandas as pd
|
|
333
340
|
>>> from shapely.geometry import LineString
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Functions for Finding network components in a GeoDataFrame of lines."""
|
|
2
2
|
|
|
3
3
|
import networkx as nx
|
|
4
|
+
import pandas as pd
|
|
4
5
|
from geopandas import GeoDataFrame
|
|
5
6
|
|
|
6
7
|
from .nodes import make_node_ids
|
|
@@ -23,7 +24,7 @@ def get_connected_components(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
|
23
24
|
The GeoDataFrame with a new column "connected".
|
|
24
25
|
|
|
25
26
|
Examples:
|
|
26
|
-
|
|
27
|
+
---------
|
|
27
28
|
>>> from sgis import read_parquet_url, get_connected_components
|
|
28
29
|
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
29
30
|
|
|
@@ -76,12 +77,12 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
|
76
77
|
A GeoDataFrame with a new column "component_size".
|
|
77
78
|
|
|
78
79
|
Examples:
|
|
79
|
-
|
|
80
|
+
---------
|
|
80
81
|
>>> from sgis import read_parquet_url, get_component_size
|
|
81
82
|
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
82
83
|
|
|
83
84
|
>>> roads = get_component_size(roads)
|
|
84
|
-
>>> roads
|
|
85
|
+
>>> roads["component_size"].value_counts().head()
|
|
85
86
|
component_size
|
|
86
87
|
79180 85638
|
|
87
88
|
2 1601
|
|
@@ -101,11 +102,17 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
|
101
102
|
graph.add_edges_from(edges)
|
|
102
103
|
components = [list(x) for x in nx.connected_components(graph)]
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
mapper = pd.DataFrame(
|
|
106
|
+
{
|
|
107
|
+
idx: [i, len(component)]
|
|
108
|
+
for i, component in enumerate(components)
|
|
109
|
+
for idx in component
|
|
110
|
+
},
|
|
111
|
+
).transpose()
|
|
112
|
+
mapper.columns = ["component_index", "component_size"]
|
|
113
|
+
|
|
114
|
+
gdf["component_index"] = gdf["source"].map(mapper["component_index"])
|
|
115
|
+
gdf["component_size"] = gdf["source"].map(mapper["component_size"])
|
|
109
116
|
|
|
110
117
|
gdf = gdf.drop(
|
|
111
118
|
["source_wkt", "target_wkt", "source", "target", "n_source", "n_target"], axis=1
|