From c2a1dae3b3a06858b4c457820f0d4a675047e7dc Mon Sep 17 00:00:00 2001 From: knaaptime Date: Thu, 2 Jan 2025 13:43:46 -0800 Subject: [PATCH] dexplore as extension class --- geosnap/visualize/mapping.py | 119 +++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/geosnap/visualize/mapping.py b/geosnap/visualize/mapping.py index 93af4ec0..b25d30ff 100644 --- a/geosnap/visualize/mapping.py +++ b/geosnap/visualize/mapping.py @@ -7,9 +7,11 @@ from warnings import warn import contextily as ctx +import geopandas as gpd import mapclassify.classifiers as classifiers import matplotlib.pyplot as plt import numpy as np +import pandas as pd from mapclassify.util import get_color_array from matplotlib import colormaps from matplotlib.animation import ArtistAnimation, PillowWriter @@ -19,10 +21,110 @@ schemes[classifier.lower()] = getattr(classifiers, classifier) -__all__ = ["animate_timeseries", "gif_from_path", "plot_timeseries", "dexplore"] +__all__ = ["animate_timeseries", "gif_from_path", "plot_timeseries", "_dexplore"] + + +# these methods +@pd.api.extensions.register_dataframe_accessor("gvz") +class GeosnapAccessor: + def __init__(self, pandas_obj): + self._validate(pandas_obj) + self._obj = pandas_obj + + @staticmethod + def _validate(obj): + # verify there is a column latitude and a column longitude + if not isinstance(obj, gpd.GeoDataFrame): + raise AttributeError("must be a geodataframe") + + def explore( + self, + column=None, + cmap=None, + scheme=None, + k=6, + categorical=False, + elevation=None, + extruded=False, + elevation_scale=1, + alpha=1, + layer_kwargs=None, + map_kwargs=None, + classify_kwargs=None, + nan_color=[255, 255, 255, 255], + color=None, + wireframe=False, + ): + """explore a dataframe using lonboard and deckgl + + Parameters + ---------- + gdf : geopandas.GeoDataFrame + dataframe to visualize + column : str, optional + name of column on dataframe to visualize on map, by default None + cmap : str, optional + name of matplotlib colormap to , by default None + scheme : str, optional + name of a classification scheme defined by mapclassify.Classifier, by default + None + k : int, optional + number of classes to generate, by default 6 + categorical : bool, optional + whether the data should be treated as categorical or continuous, by default + False + elevation : str or array, optional + name of column on the dataframe used to extrude each geometry or an array-like + in the same order as observations, by default None + extruded : bool, optional + whether to extrude geometries using the z-dimension, by default False + elevation_scale : int, optional + constant scaler multiplied by elevation valuer, by default 1 + alpha : float, optional + alpha (opacity) parameter in the range (0,1) passed to + mapclassify.util.get_color_array, by default 1 + layer_kwargs : dict, optional + additional keyword arguments passed to lonboard.viz layer arguments (either + polygon_kwargs, scatterplot_kwargs, or path_kwargs, depending on input + geometry type), by default None + map_kwargs : dict, optional + additional keyword arguments passed to lonboard.viz map_kwargs, by default None + classify_kwargs : dict, optional + additional keyword arguments passed to `mapclassify.classify`, by default None + nan_color : list-like, optional + color used to shade NaN observations formatted as an RGBA list, by + default [255, 255, 255, 255] + color : str or array-like, optional + _description_, by default None + wireframe : bool, optional + whether to use wireframe styling in deckgl, by default False + + Returns + ------- + lonboard.Map + a lonboard map with geodataframe included as a Layer object. + """ + return _dexplore( + self._obj, + column, + cmap, + scheme, + k, + categorical, + elevation, + extruded, + elevation_scale, + alpha, + layer_kwargs, + map_kwargs, + classify_kwargs, + nan_color, + color, + wireframe, + ) -def dexplore( +def _dexplore( gdf, column=None, cmap=None, @@ -94,7 +196,9 @@ def dexplore( from lonboard import viz from lonboard.colormap import apply_categorical_cmap, apply_continuous_cmap except ImportError as e: - raise ImportError from e('you must have the lonboard package installed to use this function') + raise ImportError( + "you must have the lonboard package installed to use this function" + ) from e if cmap is None: cmap = "Set1" if categorical else "viridis" if map_kwargs is None: @@ -132,8 +236,12 @@ def dexplore( if categorical: color_array = _get_categorical_cmap(gdf[column].values, cmap) elif scheme is None: + # minmax scale the column first, matplotlib needs 0-1 + transformed = (gdf[column] - np.nanmin(gdf[column])) / ( + np.nanmax(gdf[column]) - np.nanmin(gdf[column]) + ) color_array = apply_continuous_cmap( - values=gdf[column], cmap=colormaps[cmap], alpha=alpha + values=transformed, cmap=colormaps[cmap], alpha=alpha ) else: color_array = get_color_array( @@ -163,6 +271,9 @@ def dexplore( def _get_categorical_cmap(categories, cmap): + # this already sits inside a conditional import from lonboard + from lonboard.colormap import apply_categorical_cmap + colors = colormaps[cmap].colors colors = (np.array(colors) * 255).astype(int) unique_cats = np.unique(categories)