From 7864cb1d7ba7b6f08d9ce0f62f8c3c3fdad483a0 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Tue, 31 Dec 2024 16:31:44 -0800 Subject: [PATCH 01/13] dexplore function --- environment.yml | 4 +- geosnap/visualize/mapping.py | 168 +++++++++++++++++++++++++++++++++-- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/environment.yml b/environment.yml index ad3ac0f8..1118712b 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,6 @@ name: geosnap channels: - conda-forge - - defaults dependencies: - pandas - giddy >=2.2.1 @@ -20,8 +19,9 @@ dependencies: - contextily - mapclassify - spopt >=0.3.0 - - s3fs - segregation >=2.0 - pyproj >=3 - pandana - pooch + - lonboard + - osmnx >=2.0 diff --git a/geosnap/visualize/mapping.py b/geosnap/visualize/mapping.py index f9cc56cd..93af4ec0 100644 --- a/geosnap/visualize/mapping.py +++ b/geosnap/visualize/mapping.py @@ -1,4 +1,5 @@ """functions for choropleth mapping timeseries data.""" + import os import re import tempfile @@ -9,6 +10,8 @@ import mapclassify.classifiers as classifiers import matplotlib.pyplot as plt import numpy as np +from mapclassify.util import get_color_array +from matplotlib import colormaps from matplotlib.animation import ArtistAnimation, PillowWriter schemes = {} @@ -16,11 +19,165 @@ schemes[classifier.lower()] = getattr(classifiers, classifier) -__all__ = [ - "animate_timeseries", - "gif_from_path", - "plot_timeseries", -] +__all__ = ["animate_timeseries", "gif_from_path", "plot_timeseries", "dexplore"] + + +def dexplore( + gdf, + 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. + + """ + try: + 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') + if cmap is None: + cmap = "Set1" if categorical else "viridis" + if map_kwargs is None: + map_kwargs = dict() + if classify_kwargs is None: + classify_kwargs = dict() + if layer_kwargs is None: + layer_kwargs = dict() + if isinstance(elevation, str): + if elevation in gdf.columns: + elevation = gdf[elevation] + else: + raise ValueError( + f"the designated height column {elevation} is not in the dataframe" + ) + + # only polygons have z + if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): + layer_kwargs["get_elevation"] = elevation + layer_kwargs["extruded"] = extruded + layer_kwargs["elevation_scale"] = elevation_scale + layer_kwargs["wireframe"] = wireframe + + LINE = False # set color of lines, not fill_color + if ["LineString", "MultiLineString"] in gdf.geometry.geom_type.unique(): + LINE = True + if color: + if LINE: + layer_kwargs["get_color"] = color + else: + layer_kwargs["get_fill_color"] = color + if column is not None: + if column not in gdf.columns: + raise ValueError(f"the designated column {column} is not in the dataframe") + if categorical: + color_array = _get_categorical_cmap(gdf[column].values, cmap) + elif scheme is None: + color_array = apply_continuous_cmap( + values=gdf[column], cmap=colormaps[cmap], alpha=alpha + ) + else: + color_array = get_color_array( + gdf[column], + scheme=scheme, + k=k, + cmap=cmap, + alpha=alpha, + nan_color=nan_color, + **classify_kwargs, + ) + + if LINE: + layer_kwargs["get_color"] = color_array + + else: + layer_kwargs["get_fill_color"] = color_array + m = viz( + gdf, + polygon_kwargs=layer_kwargs, + scatterplot_kwargs=layer_kwargs, + path_kwargs=layer_kwargs, + map_kwargs=map_kwargs, + ) + + return m + + +def _get_categorical_cmap(categories, cmap): + colors = colormaps[cmap].colors + colors = (np.array(colors) * 255).astype(int) + unique_cats = np.unique(categories) + n_cats = len(unique_cats) + n_colors = colors.shape[0] + if n_cats > n_colors: + warn( + "the number of unique categories exceeds the number of available colors", + stacklevel=3, + ) + floor = (n_cats // n_colors) + 1 + colors = np.vstack([colors] * floor) + temp_cmap = dict(zip(unique_cats, colors)) + fill_color = apply_categorical_cmap(categories, temp_cmap) + return fill_color def gif_from_path( @@ -378,7 +535,6 @@ def animate_timeseries( colors = temp[color_col] if color_col is not None else None if categorical: - temp.plot( column, categorical=True, From c2a1dae3b3a06858b4c457820f0d4a675047e7dc Mon Sep 17 00:00:00 2001 From: knaaptime Date: Thu, 2 Jan 2025 13:43:46 -0800 Subject: [PATCH 02/13] 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) From ed1ad523eb91e4f2a01fe16d4a5a6371c53c5cec Mon Sep 17 00:00:00 2001 From: knaaptime Date: Thu, 2 Jan 2025 18:02:48 -0800 Subject: [PATCH 03/13] categoricals: resample and handle nans --- geosnap/visualize/mapping.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/geosnap/visualize/mapping.py b/geosnap/visualize/mapping.py index b25d30ff..2635a68c 100644 --- a/geosnap/visualize/mapping.py +++ b/geosnap/visualize/mapping.py @@ -234,7 +234,7 @@ def _dexplore( if column not in gdf.columns: raise ValueError(f"the designated column {column} is not in the dataframe") if categorical: - color_array = _get_categorical_cmap(gdf[column].values, cmap) + color_array = _get_categorical_cmap(gdf[column], cmap, nan_color) elif scheme is None: # minmax scale the column first, matplotlib needs 0-1 transformed = (gdf[column] - np.nanmin(gdf[column])) / ( @@ -270,15 +270,28 @@ def _dexplore( return m -def _get_categorical_cmap(categories, cmap): - # this already sits inside a conditional import from lonboard - from lonboard.colormap import apply_categorical_cmap +def _get_categorical_cmap(categories, cmap, nan_color): + try: + from lonboard.colormap import apply_categorical_cmap + except ImportError as e: + raise ImportError( + "this function requres the lonboard package to be installed" + ) from e - colors = colormaps[cmap].colors - colors = (np.array(colors) * 255).astype(int) - unique_cats = np.unique(categories) + categories = categories.copy() + unique_cats = categories.dropna().unique() n_cats = len(unique_cats) + + colors = colormaps[cmap].resampled(n_cats).colors + colors = (np.array(colors) * 255).astype(int) + colors = np.vstack([colors, nan_color]) + + cat_ints = list(range(1, n_cats + 1)) n_colors = colors.shape[0] + nan_place = n_cats + 1 + cat_to_int = dict(zip(unique_cats, cat_ints)) + categories = categories.fillna(nan_place) + categories = categories.replace(cat_to_int).astype(int) if n_cats > n_colors: warn( "the number of unique categories exceeds the number of available colors", @@ -286,7 +299,9 @@ def _get_categorical_cmap(categories, cmap): ) floor = (n_cats // n_colors) + 1 colors = np.vstack([colors] * floor) - temp_cmap = dict(zip(unique_cats, colors)) + print(colors.shape) + cat_ints.append(nan_place) + temp_cmap = dict(zip(cat_ints, colors)) fill_color = apply_categorical_cmap(categories, temp_cmap) return fill_color From ded4f383941caf3417b46bf335eff2b1cca7e726 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 3 Jan 2025 00:08:08 -0800 Subject: [PATCH 04/13] tiles and linear maps for cats --- geosnap/visualize/interactive.py | 315 +++++++++++++++++++++++++++++++ geosnap/visualize/mapping.py | 288 +--------------------------- 2 files changed, 316 insertions(+), 287 deletions(-) create mode 100644 geosnap/visualize/interactive.py diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py new file mode 100644 index 00000000..929ac1a3 --- /dev/null +++ b/geosnap/visualize/interactive.py @@ -0,0 +1,315 @@ +from warnings import warn + +import geopandas as gpd +import numpy as np +import pandas as pd +from mapclassify.util import get_color_array +from matplotlib import colormaps +from matplotlib.colors import LinearSegmentedColormap + +__all__ = ["GeosnapAccessor"] + + +# 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, + tiles="CartoDB Darkmatter", + ): + """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 + single or array of colors passed to `lonboard.Layer` object (get_color if + input dataframe is linestring, or get_fill_color otherwise. By default None + wireframe : bool, optional + whether to use wireframe styling in deckgl, by default False + tiles : str or lonboard.basemap + either a known string {"CartoDB Positron", "CartoDB Positron No Label", + "CartoDB Darkmatter", "CartoDB Darkmatter No Label", "CartoDB Voyager", + "CartoDB Voyager No Label"} or a lonboard.basemap object, or a string to a + maplibre style basemap. + + 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, + tiles, + ) + + +def _dexplore( + gdf, + 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, + tiles="CartoDB Darkmatter", +): + """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. + + """ + try: + from lonboard import basemap, viz + from lonboard.colormap import apply_continuous_cmap + except ImportError as e: + raise ImportError( + "you must have the lonboard package installed to use this function" + ) from e + providers = { + "CartoDB Positron": basemap.CartoBasemap.Positron, + "CartoDB Positron No Label": basemap.CartoBasemap.PositronNoLabels, + "CartoDB Darkmatter": basemap.CartoBasemap.DarkMatter, + "CartoDB Darkmatter No Label": basemap.CartoBasemap.DarkMatterNoLabels, + "CartoDB Voyager": basemap.CartoBasemap.Voyager, + "CartoDB Voyager No Label": basemap.CartoBasemap.VoyagerNoLabels, + } + if cmap is None: + cmap = "Set1" if categorical else "viridis" + if map_kwargs is None: + map_kwargs = dict() + if classify_kwargs is None: + classify_kwargs = dict() + if layer_kwargs is None: + layer_kwargs = dict() + if isinstance(elevation, str): + if elevation in gdf.columns: + elevation = gdf[elevation] + else: + raise ValueError( + f"the designated height column {elevation} is not in the dataframe" + ) + + # only polygons have z + if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): + layer_kwargs["get_elevation"] = elevation + layer_kwargs["extruded"] = extruded + layer_kwargs["elevation_scale"] = elevation_scale + layer_kwargs["wireframe"] = wireframe + + LINE = False # set color of lines, not fill_color + if ["LineString", "MultiLineString"] in gdf.geometry.geom_type.unique(): + LINE = True + if color: + if LINE: + layer_kwargs["get_color"] = color + else: + layer_kwargs["get_fill_color"] = color + if column is not None: + if column not in gdf.columns: + raise ValueError(f"the designated column {column} is not in the dataframe") + if categorical: + color_array = _get_categorical_cmap(gdf[column], cmap, nan_color) + 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=transformed, cmap=colormaps[cmap], alpha=alpha + ) + else: + color_array = get_color_array( + gdf[column], + scheme=scheme, + k=k, + cmap=cmap, + alpha=alpha, + nan_color=nan_color, + **classify_kwargs, + ) + + if LINE: + layer_kwargs["get_color"] = color_array + + else: + layer_kwargs["get_fill_color"] = color_array + if tiles: + map_kwargs["basemap_style"] = providers[tiles] + m = viz( + gdf, + polygon_kwargs=layer_kwargs, + scatterplot_kwargs=layer_kwargs, + path_kwargs=layer_kwargs, + map_kwargs=map_kwargs, + ) + + return m + + +def _get_categorical_cmap(categories, cmap, nan_color): + try: + from lonboard.colormap import apply_categorical_cmap + except ImportError as e: + raise ImportError( + "this function requres the lonboard package to be installed" + ) from e + + categories = categories.copy() + unique_cats = categories.dropna().unique() + n_cats = len(unique_cats) + if isinstance(colormaps[cmap], LinearSegmentedColormap): + colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats))) + else: + colors = colormaps[cmap].resampled(n_cats).colors + colors = (np.array(colors) * 255).astype(int) + colors = np.vstack([colors, nan_color]) + + cat_ints = list(range(1, n_cats + 1)) + n_colors = colors.shape[0] + nan_place = n_cats + 1 + cat_to_int = dict(zip(unique_cats, cat_ints)) + categories = categories.fillna(nan_place) + categories = categories.replace(cat_to_int).astype(int) + if n_cats > n_colors: + warn( + "the number of unique categories exceeds the number of available colors", + stacklevel=3, + ) + floor = (n_cats // n_colors) + 1 + colors = np.vstack([colors] * floor) + print(colors.shape) + cat_ints.append(nan_place) + temp_cmap = dict(zip(cat_ints, colors)) + fill_color = apply_categorical_cmap(categories, temp_cmap) + return fill_color diff --git a/geosnap/visualize/mapping.py b/geosnap/visualize/mapping.py index 2635a68c..c5af3a37 100644 --- a/geosnap/visualize/mapping.py +++ b/geosnap/visualize/mapping.py @@ -7,13 +7,9 @@ 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 schemes = {} @@ -21,289 +17,7 @@ schemes[classifier.lower()] = getattr(classifiers, classifier) -__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( - gdf, - 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. - - """ - try: - from lonboard import viz - from lonboard.colormap import apply_categorical_cmap, apply_continuous_cmap - except ImportError as e: - 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: - map_kwargs = dict() - if classify_kwargs is None: - classify_kwargs = dict() - if layer_kwargs is None: - layer_kwargs = dict() - if isinstance(elevation, str): - if elevation in gdf.columns: - elevation = gdf[elevation] - else: - raise ValueError( - f"the designated height column {elevation} is not in the dataframe" - ) - - # only polygons have z - if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): - layer_kwargs["get_elevation"] = elevation - layer_kwargs["extruded"] = extruded - layer_kwargs["elevation_scale"] = elevation_scale - layer_kwargs["wireframe"] = wireframe - - LINE = False # set color of lines, not fill_color - if ["LineString", "MultiLineString"] in gdf.geometry.geom_type.unique(): - LINE = True - if color: - if LINE: - layer_kwargs["get_color"] = color - else: - layer_kwargs["get_fill_color"] = color - if column is not None: - if column not in gdf.columns: - raise ValueError(f"the designated column {column} is not in the dataframe") - if categorical: - color_array = _get_categorical_cmap(gdf[column], cmap, nan_color) - 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=transformed, cmap=colormaps[cmap], alpha=alpha - ) - else: - color_array = get_color_array( - gdf[column], - scheme=scheme, - k=k, - cmap=cmap, - alpha=alpha, - nan_color=nan_color, - **classify_kwargs, - ) - - if LINE: - layer_kwargs["get_color"] = color_array - - else: - layer_kwargs["get_fill_color"] = color_array - m = viz( - gdf, - polygon_kwargs=layer_kwargs, - scatterplot_kwargs=layer_kwargs, - path_kwargs=layer_kwargs, - map_kwargs=map_kwargs, - ) - - return m - - -def _get_categorical_cmap(categories, cmap, nan_color): - try: - from lonboard.colormap import apply_categorical_cmap - except ImportError as e: - raise ImportError( - "this function requres the lonboard package to be installed" - ) from e - - categories = categories.copy() - unique_cats = categories.dropna().unique() - n_cats = len(unique_cats) - - colors = colormaps[cmap].resampled(n_cats).colors - colors = (np.array(colors) * 255).astype(int) - colors = np.vstack([colors, nan_color]) - - cat_ints = list(range(1, n_cats + 1)) - n_colors = colors.shape[0] - nan_place = n_cats + 1 - cat_to_int = dict(zip(unique_cats, cat_ints)) - categories = categories.fillna(nan_place) - categories = categories.replace(cat_to_int).astype(int) - if n_cats > n_colors: - warn( - "the number of unique categories exceeds the number of available colors", - stacklevel=3, - ) - floor = (n_cats // n_colors) + 1 - colors = np.vstack([colors] * floor) - print(colors.shape) - cat_ints.append(nan_place) - temp_cmap = dict(zip(cat_ints, colors)) - fill_color = apply_categorical_cmap(categories, temp_cmap) - return fill_color +__all__ = ["animate_timeseries", "gif_from_path", "plot_timeseries"] def gif_from_path( From daa5318bd57a5d2068acb0ad2da1b960880d883d Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 3 Jan 2025 00:17:50 -0800 Subject: [PATCH 05/13] errant print --- geosnap/visualize/interactive.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 929ac1a3..fbd72e25 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -51,7 +51,7 @@ def explore( 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 + name of matplotlib colormap to use, by default None scheme : str, optional name of a classification scheme defined by mapclassify.Classifier, by default None @@ -308,7 +308,6 @@ def _get_categorical_cmap(categories, cmap, nan_color): ) floor = (n_cats // n_colors) + 1 colors = np.vstack([colors] * floor) - print(colors.shape) cat_ints.append(nan_place) temp_cmap = dict(zip(cat_ints, colors)) fill_color = apply_categorical_cmap(categories, temp_cmap) From 8f74af2432c81c7303b4df28f739d81862465f29 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 3 Jan 2025 00:31:25 -0800 Subject: [PATCH 06/13] comment --- geosnap/visualize/interactive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index fbd72e25..0947d6cb 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -10,7 +10,8 @@ __all__ = ["GeosnapAccessor"] -# these methods +# not sure what this namespace should be. I usually alias +# the visualize module as gvz @pd.api.extensions.register_dataframe_accessor("gvz") class GeosnapAccessor: def __init__(self, pandas_obj): @@ -19,7 +20,6 @@ def __init__(self, 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") From 0443e2aa281ad7a358f50596597240fadd1d5527 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 3 Jan 2025 00:50:04 -0800 Subject: [PATCH 07/13] allow passing m --- geosnap/visualize/interactive.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 0947d6cb..a9369d21 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -41,6 +41,7 @@ def explore( color=None, wireframe=False, tiles="CartoDB Darkmatter", + m=None ): """explore a dataframe using lonboard and deckgl @@ -117,6 +118,7 @@ def explore( color, wireframe, tiles, + m ) @@ -138,6 +140,7 @@ def _dexplore( color=None, wireframe=False, tiles="CartoDB Darkmatter", + m=None ): """explore a dataframe using lonboard and deckgl @@ -182,6 +185,9 @@ def _dexplore( _description_, by default None wireframe : bool, optional whether to use wireframe styling in deckgl, by default False + m : lonboard.Map + a lonboard.Map instance to render the new layer on. If None (default), a new Map + will be generated. Returns ------- @@ -266,15 +272,17 @@ def _dexplore( layer_kwargs["get_fill_color"] = color_array if tiles: map_kwargs["basemap_style"] = providers[tiles] - m = viz( + new_m = viz( gdf, polygon_kwargs=layer_kwargs, scatterplot_kwargs=layer_kwargs, path_kwargs=layer_kwargs, map_kwargs=map_kwargs, ) + if m is not None: + new_m = m.add_layer(new_m) - return m + return new_m def _get_categorical_cmap(categories, cmap, nan_color): From 28501c6efa36c27104c83e612a4aac76ab9e0888 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Fri, 3 Jan 2025 13:27:00 -0800 Subject: [PATCH 08/13] alpha for cats --- geosnap/visualize/__init__.py | 5 +++-- geosnap/visualize/interactive.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/geosnap/visualize/__init__.py b/geosnap/visualize/__init__.py index e52887aa..7c4a7bee 100644 --- a/geosnap/visualize/__init__.py +++ b/geosnap/visualize/__init__.py @@ -1,4 +1,5 @@ +from .descriptives import plot_violins_by_cluster +from .interactive import * +from .mapping import * from .seq import indexplot_seq from .transitions import * -from .mapping import * -from .descriptives import plot_violins_by_cluster diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index a9369d21..7d27adb8 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -5,7 +5,6 @@ import pandas as pd from mapclassify.util import get_color_array from matplotlib import colormaps -from matplotlib.colors import LinearSegmentedColormap __all__ = ["GeosnapAccessor"] @@ -41,7 +40,7 @@ def explore( color=None, wireframe=False, tiles="CartoDB Darkmatter", - m=None + m=None, ): """explore a dataframe using lonboard and deckgl @@ -118,7 +117,7 @@ def explore( color, wireframe, tiles, - m + m, ) @@ -140,7 +139,7 @@ def _dexplore( color=None, wireframe=False, tiles="CartoDB Darkmatter", - m=None + m=None, ): """explore a dataframe using lonboard and deckgl @@ -245,7 +244,7 @@ def _dexplore( if column not in gdf.columns: raise ValueError(f"the designated column {column} is not in the dataframe") if categorical: - color_array = _get_categorical_cmap(gdf[column], cmap, nan_color) + color_array = _get_categorical_cmap(gdf[column], cmap, nan_color, alpha) elif scheme is None: # minmax scale the column first, matplotlib needs 0-1 transformed = (gdf[column] - np.nanmin(gdf[column])) / ( @@ -285,7 +284,7 @@ def _dexplore( return new_m -def _get_categorical_cmap(categories, cmap, nan_color): +def _get_categorical_cmap(categories, cmap, nan_color, alpha): try: from lonboard.colormap import apply_categorical_cmap except ImportError as e: @@ -296,10 +295,8 @@ def _get_categorical_cmap(categories, cmap, nan_color): categories = categories.copy() unique_cats = categories.dropna().unique() n_cats = len(unique_cats) - if isinstance(colormaps[cmap], LinearSegmentedColormap): - colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats))) - else: - colors = colormaps[cmap].resampled(n_cats).colors + + colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha) colors = (np.array(colors) * 255).astype(int) colors = np.vstack([colors, nan_color]) From 46ff54d667a0230213f8cdc8d22a75ac346519fd Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sun, 5 Jan 2025 17:18:15 -0800 Subject: [PATCH 09/13] use categoricals for categories --- geosnap/visualize/interactive.py | 34 ++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 7d27adb8..96a12999 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -195,7 +195,7 @@ def _dexplore( """ try: - from lonboard import basemap, viz + from lonboard import Map, basemap, viz from lonboard.colormap import apply_continuous_cmap except ImportError as e: raise ImportError( @@ -209,8 +209,7 @@ def _dexplore( "CartoDB Voyager": basemap.CartoBasemap.Voyager, "CartoDB Voyager No Label": basemap.CartoBasemap.VoyagerNoLabels, } - if cmap is None: - cmap = "Set1" if categorical else "viridis" + if map_kwargs is None: map_kwargs = dict() if classify_kwargs is None: @@ -243,6 +242,10 @@ def _dexplore( if column is not None: if column not in gdf.columns: raise ValueError(f"the designated column {column} is not in the dataframe") + if gdf[column].dtype in ["O", "category"]: + categorical = True + if cmap is None: + cmap = "Set1" if categorical else "viridis" if categorical: color_array = _get_categorical_cmap(gdf[column], cmap, nan_color, alpha) elif scheme is None: @@ -292,20 +295,25 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): "this function requres the lonboard package to be installed" ) from e - categories = categories.copy() + cat_codes = pd.Series(pd.Categorical(categories).codes, dtype="category") + BOTTOM = False if -1 in cat_codes else True + + # nans are encoded as -1 OR largest category depending on input type + # re-encode to always be last category + cat_codes = cat_codes.cat.rename_categories({-1: len(cat_codes.unique()) - 1}) + unique_cats = categories.dropna().unique() n_cats = len(unique_cats) colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha) colors = (np.array(colors) * 255).astype(int) - colors = np.vstack([colors, nan_color]) + if BOTTOM: + colors = np.vstack([colors, nan_color]) + else: + colors = np.vstack([nan_color, colors]) - cat_ints = list(range(1, n_cats + 1)) n_colors = colors.shape[0] - nan_place = n_cats + 1 - cat_to_int = dict(zip(unique_cats, cat_ints)) - categories = categories.fillna(nan_place) - categories = categories.replace(cat_to_int).astype(int) + if n_cats > n_colors: warn( "the number of unique categories exceeds the number of available colors", @@ -313,7 +321,7 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): ) floor = (n_cats // n_colors) + 1 colors = np.vstack([colors] * floor) - cat_ints.append(nan_place) - temp_cmap = dict(zip(cat_ints, colors)) - fill_color = apply_categorical_cmap(categories, temp_cmap) + + temp_cmap = dict(zip(range(n_cats + 1), colors)) + fill_color = apply_categorical_cmap(cat_codes, temp_cmap) return fill_color From ee3d76af312b79282b13a30abfb2b641e347bd4a Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sun, 5 Jan 2025 20:03:11 -0800 Subject: [PATCH 10/13] input checks --- geosnap/visualize/interactive.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 96a12999..51c385d5 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -82,7 +82,8 @@ def explore( None nan_color : list-like, optional color used to shade NaN observations formatted as an RGBA list, by - default [255, 255, 255, 255] + default [255, 255, 255, 255]. If no alpha channel is passed it is assumed to + be 255. color : str or array-like, optional single or array of colors passed to `lonboard.Layer` object (get_color if input dataframe is linestring, or get_fill_color otherwise. By default None @@ -160,7 +161,7 @@ def _dexplore( 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 + 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 @@ -179,7 +180,8 @@ def _dexplore( 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] + default [255, 255, 255, 255]. If no alpha channel is passed it is assumed to be + 255. color : str or array-like, optional _description_, by default None wireframe : bool, optional @@ -223,7 +225,21 @@ def _dexplore( raise ValueError( f"the designated height column {elevation} is not in the dataframe" ) + if not pd.api.types.is_numeric_dtype(elevation): + raise ValueError("elevation must be a numeric data type") + if not pd.api.types.is_list_like(nan_color): + raise ValueError("nan_color must be an iterable of 3 or 4 values") + + if len(nan_color) != 4: + if len(nan_color) == 3: + nan_color = np.append(nan_color, [255]) + else: + raise ValueError("nan_color must be an iterable of 3 or 4 values") + if cmap not in colormaps: + raise ValueError( + f"`cmap` must be one of {list(colormaps.keys())} but {cmap} was passed" + ) # only polygons have z if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): layer_kwargs["get_elevation"] = elevation From d3f108025be58e20708929ef920ffcf439dd792d Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sun, 5 Jan 2025 21:44:01 -0800 Subject: [PATCH 11/13] move cmap check --- geosnap/visualize/interactive.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 51c385d5..f81b9601 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -236,10 +236,7 @@ def _dexplore( nan_color = np.append(nan_color, [255]) else: raise ValueError("nan_color must be an iterable of 3 or 4 values") - if cmap not in colormaps: - raise ValueError( - f"`cmap` must be one of {list(colormaps.keys())} but {cmap} was passed" - ) + # only polygons have z if ["Polygon", "MultiPolygon"] in gdf.geometry.geom_type.unique(): layer_kwargs["get_elevation"] = elevation @@ -260,6 +257,10 @@ def _dexplore( raise ValueError(f"the designated column {column} is not in the dataframe") if gdf[column].dtype in ["O", "category"]: categorical = True + if cmap is not None and cmap not in colormaps: + raise ValueError( + f"`cmap` must be one of {list(colormaps.keys())} but {cmap} was passed" + ) if cmap is None: cmap = "Set1" if categorical else "viridis" if categorical: @@ -312,7 +313,6 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): ) from e cat_codes = pd.Series(pd.Categorical(categories).codes, dtype="category") - BOTTOM = False if -1 in cat_codes else True # nans are encoded as -1 OR largest category depending on input type # re-encode to always be last category @@ -323,10 +323,8 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha) colors = (np.array(colors) * 255).astype(int) - if BOTTOM: - colors = np.vstack([colors, nan_color]) - else: - colors = np.vstack([nan_color, colors]) + colors = np.vstack([colors, nan_color]) + n_colors = colors.shape[0] From 23f52a8d4303ffe043948fb195607c7eeae00305 Mon Sep 17 00:00:00 2001 From: knaaptime Date: Sun, 5 Jan 2025 22:06:46 -0800 Subject: [PATCH 12/13] tab20 as default for cats --- geosnap/visualize/interactive.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index f81b9601..66c7301e 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -262,7 +262,7 @@ def _dexplore( f"`cmap` must be one of {list(colormaps.keys())} but {cmap} was passed" ) if cmap is None: - cmap = "Set1" if categorical else "viridis" + cmap = "tab20" if categorical else "viridis" if categorical: color_array = _get_categorical_cmap(gdf[column], cmap, nan_color, alpha) elif scheme is None: @@ -313,21 +313,15 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): ) from e cat_codes = pd.Series(pd.Categorical(categories).codes, dtype="category") - # nans are encoded as -1 OR largest category depending on input type # re-encode to always be last category cat_codes = cat_codes.cat.rename_categories({-1: len(cat_codes.unique()) - 1}) - unique_cats = categories.dropna().unique() n_cats = len(unique_cats) - colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha) colors = (np.array(colors) * 255).astype(int) colors = np.vstack([colors, nan_color]) - - n_colors = colors.shape[0] - if n_cats > n_colors: warn( "the number of unique categories exceeds the number of available colors", @@ -335,7 +329,6 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): ) floor = (n_cats // n_colors) + 1 colors = np.vstack([colors] * floor) - temp_cmap = dict(zip(range(n_cats + 1), colors)) fill_color = apply_categorical_cmap(cat_codes, temp_cmap) return fill_color From 80f7a429864e125026ebce34f5988d83e657c37a Mon Sep 17 00:00:00 2001 From: knaaptime Date: Mon, 6 Jan 2025 11:19:33 -0800 Subject: [PATCH 13/13] skip color len check --- geosnap/visualize/interactive.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/geosnap/visualize/interactive.py b/geosnap/visualize/interactive.py index 66c7301e..d176db36 100644 --- a/geosnap/visualize/interactive.py +++ b/geosnap/visualize/interactive.py @@ -65,7 +65,7 @@ def explore( 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 + elevation_scale : float, optional constant scaler multiplied by elevation valuer, by default 1 alpha : float, optional alpha (opacity) parameter in the range (0,1) passed to @@ -321,14 +321,6 @@ def _get_categorical_cmap(categories, cmap, nan_color, alpha): colors = colormaps[cmap].resampled(n_cats)(list(range(n_cats)), alpha) colors = (np.array(colors) * 255).astype(int) colors = np.vstack([colors, nan_color]) - n_colors = colors.shape[0] - if n_cats > n_colors: - warn( - "the number of unique categories exceeds the number of available colors", - stacklevel=3, - ) - floor = (n_cats // n_colors) + 1 - colors = np.vstack([colors] * floor) temp_cmap = dict(zip(range(n_cats + 1), colors)) fill_color = apply_categorical_cmap(cat_codes, temp_cmap) return fill_color