Skip to content

Commit d2678f1

Browse files
authored
Merge pull request #290 from statisticsnorway/map-hatches
Map hatches
2 parents 94d9fdb + a20e2bb commit d2678f1

File tree

15 files changed

+1013
-466
lines changed

15 files changed

+1013
-466
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "ssb-sgis"
3-
version = "1.2.2"
3+
version = "1.2.3"
44
description = "GIS functions used at Statistics Norway."
55
authors = ["Morten Letnes <morten.letnes@ssb.no>"]
66
license = "MIT"

src/sgis/geopandas_tools/general.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ def to_lines(
643643
f"Point geometries not allowed in 'to_lines'. {geoms.geom_type.value_counts()}"
644644
)
645645

646-
gdf.geometry.loc[:] = geoms
646+
gdf.loc[:, gdf.geometry.name] = geoms
647647

648648
if not split:
649649
return gdf

src/sgis/geopandas_tools/overlay.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,11 @@ def clean_overlay(
172172
if df2.geometry.name != "geometry":
173173
df2 = df2.rename_geometry("geometry")
174174

175-
# to pandas because GeoDataFrame constructor is expensive
175+
# to pandas because GeoDataFrame constructor is slow
176176
df1 = DataFrame(df1).reset_index(drop=True)
177177
df2 = DataFrame(df2).reset_index(drop=True)
178+
df1.geometry.values.crs = None
179+
df2.geometry.values.crs = None
178180

179181
overlayed = (
180182
gpd.GeoDataFrame(

src/sgis/maps/explore.py

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from ..geopandas_tools.geometry_types import get_geom_type
4545
from ..geopandas_tools.geometry_types import to_single_geom_type
4646
from ..helpers import _get_file_system
47+
from ..helpers import dict_zip
4748
from .wms import WmsLoader
4849

4950
try:
@@ -385,12 +386,12 @@ def __init__(
385386

386387
super().__init__(column=column, show=show, **(new_kwargs | new_gdfs))
387388

388-
if self.gdfs is None:
389+
if self._gdfs is None:
389390
return
390391

391392
# stringify or remove columns not renerable by leaflet (list, geometry etc.)
392-
new_gdfs, show_new = [], []
393-
for gdf, show in zip(self.gdfs, self.show, strict=True):
393+
new_gdfs, show_new = {}, {}
394+
for label, gdf, show in dict_zip(self._gdfs, self.show):
394395
try:
395396
gdf = gdf.reset_index()
396397
except Exception:
@@ -420,18 +421,15 @@ def __init__(
420421
gdf.index = gdf.index.astype(str)
421422
except Exception:
422423
pass
423-
new_gdfs.append(to_gdf(gdf))
424-
show_new.append(show)
424+
new_gdfs[label] = to_gdf(gdf)
425+
show_new[label] = show
425426
self._gdfs = new_gdfs
426427
if self._gdfs:
427428
self._gdf = pd.concat(new_gdfs, ignore_index=True)
428429
else:
429-
self._gdf = GeoDataFrame({"geometry": [], self._column: []})
430+
self._gdf = self._get_gdf_template()
430431
self.show = show_new
431432

432-
# if self._show_was_none and len(self._gdfs) > 6:
433-
# self.show = [False] * len(self._gdfs)
434-
435433
if self._is_categorical:
436434
if len(self.gdfs) == 1:
437435
self._split_categories()
@@ -455,7 +453,7 @@ def __len__(self) -> int:
455453
rasters = self.raster_data
456454
except AttributeError:
457455
rasters = self.rasters
458-
return len([gdf for gdf in self._gdfs if len(gdf)]) + len(rasters)
456+
return len([gdf for gdf in self._gdfs.values() if len(gdf)]) + len(rasters)
459457

460458
def __bool__(self) -> bool:
461459
"""True if any gdfs have rows or there are any raster images."""
@@ -473,7 +471,7 @@ def explore(
473471
self.mask = mask if mask is not None else self.mask
474472
if (
475473
self._gdfs
476-
and not any(len(gdf) for gdf in self._gdfs)
474+
and not any(len(gdf) for gdf in self._gdfs.values())
477475
and not len(self.rasters)
478476
):
479477
warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
@@ -498,13 +496,11 @@ def explore(
498496
else center
499497
)
500498

501-
gdfs: tuple[GeoDataFrame] = ()
502-
for gdf in self._gdfs:
499+
for label, gdf in self._gdfs.items():
503500
keep_geom_type = False if get_geom_type(gdf) == "mixed" else True
504501
gdf = gdf.clip(centerpoint.buffer(size), keep_geom_type=keep_geom_type)
505-
gdfs = gdfs + (gdf,)
506-
self._gdfs = gdfs
507-
self._gdf = pd.concat(gdfs, ignore_index=True)
502+
self._gdfs[label] = gdf
503+
self._gdf = pd.concat(self._gdfs.values(), ignore_index=True)
508504

509505
self._get_unique_values()
510506

@@ -558,19 +554,17 @@ def clipmap(
558554
self._update_column()
559555
kwargs.pop("column", None)
560556

561-
gdfs: tuple[GeoDataFrame] = ()
562-
for gdf in self._gdfs:
557+
for label, gdf in self._gdfs.items():
563558
gdf = gdf.clip(self.mask)
564559
collections = gdf.loc[gdf.geom_type == "GeometryCollection"]
565560
if len(collections):
566561
collections = make_all_singlepart(collections)
567562
gdf = pd.concat([gdf, collections], ignore_index=False)
568-
gdfs = gdfs + (gdf,)
569-
self._gdfs = gdfs
563+
self._gdfs[label] = gdf
570564
if self._gdfs:
571-
self._gdf = pd.concat(self._gdfs, ignore_index=True)
565+
self._gdf = pd.concat(self._gdfs.values(), ignore_index=True)
572566
else:
573-
self._gdf = GeoDataFrame({"geometry": [], self._column: []})
567+
self._gdf = self._get_gdf_template()
574568

575569
self._explore(**kwargs)
576570

@@ -638,8 +632,11 @@ def save(self, path: str) -> None:
638632
def _explore(self, **kwargs) -> None:
639633
self.kwargs = self.kwargs | kwargs
640634

641-
if self._show_was_none and len([gdf for gdf in self._gdfs if len(gdf)]) > 6:
642-
self.show = [False] * len(self._gdfs)
635+
if (
636+
self._show_was_none
637+
and len([gdf for gdf in self._gdfs.values() if len(gdf)]) > 6
638+
):
639+
self.show = {label: False for label in self._gdfs}
643640

644641
if self._is_categorical:
645642
self._create_categorical_map()
@@ -662,15 +659,13 @@ def _explore(self, **kwargs) -> None:
662659
display(self.map)
663660

664661
def _split_categories(self) -> None:
665-
new_gdfs, new_labels, new_shows = [], [], []
662+
new_gdfs, new_shows = {}, {}
666663
for cat in self._unique_values:
667664
gdf = self.gdf.loc[self.gdf[self.column] == cat]
668-
new_gdfs.append(gdf)
669-
new_labels.append(cat)
670-
new_shows.append(self.show[0])
665+
new_gdfs[cat] = gdf
666+
new_shows[cat] = next(iter(self.show.values()))
671667
self._gdfs = new_gdfs
672668
self._gdf = pd.concat(new_gdfs, ignore_index=True)
673-
self.labels = new_labels
674669
self.show = new_shows
675670

676671
def _to_single_geom_type(self, gdf: GeoDataFrame) -> GeoDataFrame:
@@ -720,12 +715,11 @@ def _get_bounds(
720715
return gdf.total_bounds
721716

722717
def _create_categorical_map(self) -> None:
723-
self._make_categories_colors_dict()
718+
self._prepare_categorical_plot()
724719
if self._gdf is not None and len(self._gdf):
725-
self._fix_nans()
726720
gdf = self._prepare_gdf_for_map(self._gdf)
727721
else:
728-
gdf = GeoDataFrame({"geometry": [], self._column: []})
722+
gdf = self._get_gdf_template()
729723

730724
self._load_rasters_as_images()
731725

@@ -742,7 +736,7 @@ def _create_categorical_map(self) -> None:
742736
**self.kwargs,
743737
)
744738

745-
for gdf, label, show in zip(self._gdfs, self.labels, self.show, strict=True):
739+
for label, gdf, show in dict_zip(self._gdfs, self.show):
746740
if not len(gdf):
747741
continue
748742

@@ -798,7 +792,10 @@ def _create_continous_map(self):
798792
if self.scheme:
799793
classified = self._classify_from_bins(self._gdf, bins=self.bins)
800794
classified_sequential = self._push_classification(classified)
801-
n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
795+
n_colors = (
796+
len(np.unique(classified_sequential))
797+
- self._gdf[self._column].isna().any()
798+
)
802799
unique_colors = self._get_continous_colors(n=n_colors)
803800

804801
self._load_rasters_as_images()
@@ -824,7 +821,7 @@ def _create_continous_map(self):
824821
index=self.bins,
825822
)
826823

827-
for gdf, label, show in zip(self._gdfs, self.labels, self.show, strict=True):
824+
for (label, gdf), show in zip(self._gdfs.items(), self.show, strict=True):
828825
if not len(gdf):
829826
continue
830827

src/sgis/maps/legend.py

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import numpy as np
1515
import pandas as pd
1616
from geopandas import GeoDataFrame
17-
from matplotlib.lines import Line2D
17+
from matplotlib.patches import Patch
1818
from pandas import Series
1919

2020
from ..geopandas_tools.bounds import bounds_to_points
@@ -50,6 +50,7 @@
5050
"rounding",
5151
"facecolor",
5252
"labelcolor",
53+
"hatch",
5354
}
5455

5556
LOWERCASE_WORDS = {
@@ -265,8 +266,17 @@ def _get_legend_sizes(self, size: int | float, kwargs: dict) -> None:
265266
else:
266267
self._markersize = size
267268

269+
def _get_patch(self, color, edgecolor="black", **kwargs) -> Patch:
270+
return Patch(
271+
facecolor=color,
272+
edgecolor=edgecolor,
273+
**kwargs,
274+
)
275+
268276
def _prepare_categorical_legend(
269-
self, categories_colors: dict, nan_label: str
277+
self,
278+
categories_colors: dict,
279+
hatch: str,
270280
) -> None:
271281
for attr in self.__dict__.keys():
272282
if attr in self.kwargs:
@@ -279,31 +289,30 @@ def _prepare_categorical_legend(
279289
}
280290
# swap column values with label list and hope it's in the correct order
281291
elif self.labels:
282-
categories_colors = {
283-
label: color
284-
for label, color in zip(
285-
self.labels, categories_colors.values(), strict=True
292+
if len(self.labels) != len(categories_colors):
293+
raise ValueError(
294+
f"Unequal length of labels {self.labels} and categories/colors {categories_colors}"
286295
)
287-
}
296+
categories_colors = dict(
297+
zip(self.labels, categories_colors.values(), strict=True)
298+
)
288299

289300
self._patches, self._categories = [], []
290301
for category, color in categories_colors.items():
291302
if self.pretty_labels:
292303
category = prettify_label(category)
293-
if category == nan_label:
294-
self._categories.append(nan_label)
295-
else:
296-
self._categories.append(category)
304+
self._categories.append(category)
305+
self._patches.append(self._get_patch(color=color, hatch=hatch))
306+
307+
def _add_more_data_to_legend(self, more_data: dict[str, dict]) -> None:
308+
for label, datadict in more_data.items():
309+
if self.pretty_labels:
310+
label = prettify_label(label)
311+
self._categories.append(label)
312+
datadict = {key: value for key, value in datadict.items() if key != "gdf"}
297313
self._patches.append(
298-
Line2D(
299-
[0],
300-
[0],
301-
linestyle="none",
302-
marker="o",
303-
alpha=self.kwargs.get("alpha", 1),
304-
markersize=self._markersize,
305-
markerfacecolor=color,
306-
markeredgewidth=0,
314+
self._get_patch(
315+
**datadict,
307316
)
308317
)
309318

@@ -595,40 +604,34 @@ def _prepare_continous_legend(
595604
self,
596605
bins: list[float],
597606
colors: list[str],
598-
nan_label: str,
599607
bin_values: dict,
608+
nan_label: str,
609+
hatch: str,
600610
) -> None:
601611
# TODO: clean up this messy method
602612

603-
for attr in self.kwargs:
613+
if (
614+
colors is not None
615+
and self.labels is not None
616+
and len(self.labels) != len(colors)
617+
):
618+
raise ValueError(
619+
"Label list must be same length as the number of groups. "
620+
f"Got k={len(colors)} and labels={len(self.labels)}."
621+
f"labels: {', '.join(self.labels)}"
622+
f"colors: {', '.join(colors)}"
623+
f"bins: {bins}"
624+
)
625+
626+
for attr in dict(self.kwargs):
604627
if attr in self.__dict__:
605628
self[attr] = self.kwargs.pop(attr)
606629

607630
self._patches, self._categories = [], []
608-
609631
for color in colors:
610-
self._patches.append(
611-
Line2D(
612-
[0],
613-
[0],
614-
linestyle="none",
615-
marker="o",
616-
alpha=self.kwargs.get("alpha", 1),
617-
markersize=self._markersize,
618-
markerfacecolor=color,
619-
markeredgewidth=0,
620-
)
621-
)
632+
self._patches.append(self._get_patch(color=color, hatch=hatch))
622633

623634
if self.labels:
624-
if len(self.labels) != len(colors):
625-
raise ValueError(
626-
"Label list must be same length as the number of groups. "
627-
f"Got k={len(colors)} and labels={len(self.labels)}."
628-
f"labels: {', '.join(self.labels)}"
629-
f"colors: {', '.join(colors)}"
630-
f"bins: {bins}"
631-
)
632635
self._categories = self.labels
633636

634637
elif len(bins) == len(colors):
@@ -703,6 +706,9 @@ def _prepare_continous_legend(
703706
label = self._get_two_value_label(min_rounded, max_rounded)
704707
self._categories.append(label)
705708

709+
# if nan_label:
710+
# self._categories.append(nan_label)
711+
706712
def _get_two_value_label(self, value1: int | float, value2: int | float) -> str:
707713
return (
708714
f"{value1} {self.label_suffix} {self.label_sep} "

0 commit comments

Comments
 (0)