def extract_foreground(self, **kwargs): img = self.image if self._initialized: bg, fg = self.draw_bg.element, self.draw_fg.element else: self._initialized = True bg, fg = self.bg_paths, self.fg_paths bg, fg = (gv.project(g, projection=img.crs) for g in (bg, fg)) if not len(bg) or not len(fg): return gv.Path([], img.kdims, crs=img.crs) if self.downsample != 1: kwargs = {'dynamic': False} h, w = img.interface.shape(img, gridded=True) kwargs['width'] = int(w * self.downsample) kwargs['height'] = int(h * self.downsample) img = regrid(img, **kwargs) foreground = extract_foreground(img, background=bg, foreground=fg, iterations=self.iterations) with warnings.catch_warnings(): warnings.filterwarnings('ignore') foreground = gv.Path( [contours(foreground, filled=True, levels=1).split()[0].data], kdims=foreground.kdims, crs=foreground.crs) self.result = gv.project(foreground, projection=self.crs) return foreground
def getGeoElementFromData(self, data,vdims=None): arrayGeomType = data.geom_type.unique() for geomType in arrayGeomType: data = data[data['geometry'].apply(lambda x: x.geom_type == geomType)] if geomType in POINT: geomNdOverlay = gv.Points(data, vdims=vdims,crs=ccrs.GOOGLE_MERCATOR, group=POINT[0]) elif geomType in LINESTRING: data['geometry'] = data.simplify(SOFT_SIMPLIFY, True) geomNdOverlay = gv.Path(data, vdims=vdims,crs=ccrs.GOOGLE_MERCATOR, group=LINESTRING[0]) elif geomType in MULTILINESTRING: data['geometry'] = data.simplify(SOFT_SIMPLIFY, True) geomNdOverlay = gv.Path(data, vdims=vdims, crs=ccrs.GOOGLE_MERCATOR, group=MULTILINESTRING[0]) elif geomType in POLYGONE: data['geometry'] = data.simplify(STRONG_SIMPLIFY, True) geomNdOverlay = gv.Polygons(data, vdims=vdims, crs=ccrs.GOOGLE_MERCATOR, group=POLYGONE[0]) elif geomType in MULTIPOLYGONE: data['geometry'] = data.simplify(STRONG_SIMPLIFY, True) geomNdOverlay = gv.Polygons(data, vdims=vdims, crs=ccrs.GOOGLE_MERCATOR, group=MULTIPOLYGONE[0]) elif geomType in GEOMETRYCOLLECTION: data = data.explode() geomNdOverlay = GeomUtil.getGeoElementFromData(self,data=data) else: return None return geomNdOverlay.opts(tools=['hover', 'tap'])
def mapPoint(id=11): #bbox minx, maxx, miny, maxy = boundingBox(nahalal_basin) current_pnt = gv.Points(smp_pnt.query("id=={}".format(id))) pnts = gv.Points(smp_pnt, vdims=["id", "description"]) basin = gv.Contours(nahalal_basin) # hovertool hover = HoverTool(tooltips=[("Point #", "@id"), ("Description", '@description')]) points_map = (( gts.EsriImagery.opts(alpha=0.4) * gts.OSM.opts(alpha=0.4) * basin.opts(color='chocolate', line_width=2, alpha=.5) * gv.Path(kishon).opts(line_width=3.5, alpha=0.4, line_color="darkblue", line_dash="dashed") * gv.Path(nahalal).opts(line_width=2.5, alpha=0.4, line_color="blue") * gv.Path(nahalal_sub).opts( line_width=2, line_dash="dashdot", alpha=0.4, line_color="blue") * pnts.opts(size=13, alpha=0.6, tools=[hover]) * current_pnt.opts(size=20, color="purple") # * gv.Contours(interest_zone).opts(alpha=0) ).opts(width=500, height=375, fontsize={ 'labels': labels_fsize, 'xticks': tick_fsize, 'yticks': tick_fsize }).redim.range(Longitude=(minx, maxx), Latitude=(miny, maxy))) return points_map
def createOverlay(self,**kwargs): traceP = kwargs.get("traceParam") data = traceP.data label = kwargs.get("label") self.param.variable_taille_point.objects = traceP.listeEntier self.param.variable_couleur.objects = data.columns if not self.variable_taille_point: self.silently = True self.variable_taille_point = traceP.listeEntier[0] if not self.variable_couleur: self.silently = True self.variable_couleur = traceP.listeEntier[0] vdims = traceP.listeEntier kdims = list(data.columns)[5:-1] size = self.variable_taille_point arrayGeomType = data.geom_type.unique() for geomType in arrayGeomType: data = data[data['geometry'].apply(lambda x: x.geom_type == geomType)] if geomType in POINT: geomNdOverlay = gv.Points(data, vdims=vdims, crs=crs.GOOGLE_MERCATOR, label=label, group=POINT[0], id=traceP.trace.id).options(size=dim(size).norm()*45) ## Convertir les autres géometry en barycentre elif geomType in POLYGONE: data['geometry'] = data.simplify(STRONG_SIMPLIFY,True) geomNdOverlay = gv.Polygons(data,vdims=vdims, crs=crs.GOOGLE_MERCATOR,label=label, group=POLYGONE[0]) elif geomType in LINESTRING: data['geometry'] = data.simplify(SOFT_SIMPLIFY, True) geomNdOverlay = gv.Path(data, crs=crs.GOOGLE_MERCATOR,label=label, group=LINESTRING[0]) elif geomType in MULTILINESTRING: data['geometry'] = data.simplify(SOFT_SIMPLIFY, True) geomNdOverlay = gv.Path(data, crs=crs.GOOGLE_MERCATOR, label=label, group=MULTILINESTRING[0]) elif geomType in MULTIPOLYGONE: data['geometry'] = data.simplify(STRONG_SIMPLIFY,True) geomNdOverlay = gv.Polygons(data, vdims=vdims, crs=crs.GOOGLE_MERCATOR,label=label, group=MULTIPOLYGONE[0]) else: geomNdOverlay = None overlay = hv.Overlay([geomNdOverlay.opts(tools=['hover', 'tap'],color=self.variable_couleur, cmap='Category20')]) return overlay
def visualize_spatial_extent( self, ): # additional args, basemap, zoom level, cmap, export """ Creates a map displaying the input spatial extent Examples -------- >>> reg_a = ipx.Query('ATL06','path/spatialfile.shp',['2019-02-22','2019-02-28']) # doctest: +SKIP >>> reg_a.visualize_spatial_extent # doctest: +SKIP [visual map output] """ gdf = geospatial.geodataframe(self.extent_type, self._spat_extent) try: from shapely.geometry import Polygon import geoviews as gv gv.extension("bokeh") line_geoms = Polygon(gdf["geometry"][0]).boundary bbox_poly = gv.Path(line_geoms).opts(color="red", line_color="red") tile = gv.tile_sources.EsriImagery.opts(width=500, height=500) return tile * bbox_poly except ImportError: world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) f, ax = plt.subplots(1, figsize=(12, 6)) world.plot(ax=ax, facecolor="lightgray", edgecolor="gray") gdf.plot(ax=ax, color="#FF8C00", alpha=0.7) plt.show()
def make_temporal_vel_pair_path_obj(date): ttt = date.astype('datetime64[D]') kdims = ["Longitude", "Latitude"] vdims = [("e_max", "Velocity changes (%)"), "Pairname"] opts = dict(color='grey', alpha=0.5) temporal_vel_pair_path_obj = gv.Path(data_pair[ttt], crs=ccrs.PlateCarree()) temporal_vel_pair_path_obj = temporal_vel_pair_path_obj.opts(**opts) print("make_temporal_vel_pair_path_obj {}".format(date)) return (temporal_vel_pair_path_obj)
def _map(self, x, y): opts = { 'line_width': 5, 'xaxis': None, 'yaxis': None, 'line_join': 'bevel', 'line_cap': 'round', 'show_frame': False, } selected_path = int( np.clip(np.round(y), 0, self.journey_aggregate['path'].max())) color_path = gv.Path(self.map_path_data, ['lat', 'lon'], ['color', 'path']).opts(color='color', **opts) hover_path = gv.Path(self.map_path_data, ['lat', 'lon'], ['trip_id']).opts(line_alpha=0, tools=['hover'], **opts) return self.map_tiles * (color_path * hover_path).select(path=selected_path)
def extract_foreground(self, **kwargs): img = self.image bg, fg = self.bg_path_view(), self.fg_path_view() self._initialized = True if not len(bg) or not len(fg): return gv.Path([], img.kdims, crs=img.crs) if self.downsample != 1: kwargs = {'dynamic': False} h, w = img.interface.shape(img, gridded=True) kwargs['width'] = int(w*self.downsample) kwargs['height'] = int(h*self.downsample) img = regrid(img, **kwargs) foreground = extract_foreground(img, background=bg, foreground=fg, iterations=self.iterations) foreground = gv.Path([contours(foreground, filled=True, levels=1).split()[0].data], kdims=foreground.kdims, crs=foreground.crs) self.result = gv.project(foreground, projection=self.crs) return foreground
def get_boundary_string_path(mesh, node_list): """Convert a node list into path object Args: mesh: AdhMesh object node_list: List of nodes numbers with which to generate the path Nodes should be one-based since AdhMesh.verts index is one-based. Returns: Geoviews path object """ path = gv.Path(mesh.verts.loc[node_list], crs=mesh.projection.get_crs()) return path
def __init__(self, mapdata): '''initialize a SubwayMap object Args: mapdata (SubwayMapData): container-class for stations and lines dataframes with implemented observer pattern. This is necessary for data binding of the view to the viewmodel. ''' Stream.__init__(self) #create an initial map stations, lines = mapdata.stationsdf, mapdata.linesdf self.pipe = Pipe(data=[]) self.subway_map = gv.Path(lines, vdims=['color']).opts( projection=crs.LambertConformal(), height=800, width=800, color='color') * gv.DynamicMap(self.callback, streams=[self.pipe]) self.pipe.send(stations) #bind changes in the stationsdf to pipe.send mapdata.bind_to_stationsdf(self.pipe.send) self.mapdata = mapdata
def view_device_trips(self): """plot the trips for the selected device, each in its own color""" # get the rows for the selected device only single = self.trip_table[self.trip_table["device_id"] == self.device_selector] # create a colormap for each trajectory explicit_map = {} for idx, traj in enumerate(single['traj_id']): explicit_map[traj] = cc.palette.glasbey_bw[idx] # construct plots for each trip path_plts = [] for traj in single['traj_id']: geom = single[single['traj_id'] == traj]['geometry'] path_plts.append( gv.Path( geom, crs=ccrs.GOOGLE_MERCATOR).opts(color=explicit_map[traj])) # overlay the plots on tiles plt = gvts.CartoLight() * hv.Overlay(path_plts) return plt.opts(height=self.plot_height, width=self.plot_width)
def update_all(pn_date_value): if isinstance(pn_date_value, datetime.date): # si on a choisi le widget DatePicker pn_date_value = (pn_date_value - datetime.date(1950, 1, 1)).days hv_polygons = [] hv_exterieur = [] for (d, color), d_unique in zip(datasets, datasets_unique): mask = d.time == pn_date_value sub_lon = ((d.contour_lon_s[mask] + 180) % 360) - 180 sub_lat = d.contour_lat_s[mask] sub_data = np.moveaxis(np.array([sub_lon, sub_lat]), [0], [2]) polygons = [Polygon(poly) for poly in sub_data] dct_data = {name: d[name][mask] for name in hover_info} dct_data["geometry"] = polygons gpd = geopandas.GeoDataFrame(dct_data) opts_common = dict(responsive=True) hv_polygons.append( gv.Polygons(gpd, vdims=hover_info).opts(line_color=color, **opts_common)) sub_lon = ((d.contour_lon_e[mask] + 180) % 360) - 180 sub_lat = d.contour_lat_e[mask] _lon = flatten_line_matrix(sub_lon) _lat = flatten_line_matrix(sub_lat) hv_exterieur.append( gv.Path([np.array([_lon, _lat]).T]).opts(color=color, alpha=0.70, line_dash="dashed", **opts_common)) return gv.Overlay([*hv_polygons, *hv_exterieur]).opts(responsive=True, tools=["hover"])
def interactive_map(v1, options): # print(options) import cartopy.crs as ccrs import holoviews as hv from holoviews import opts, dim import geoviews as gv import geoviews.feature as gf import simplekml hv.extension("bokeh", "matplotlib") vname = v1.columns.tolist()[0] stretch = [0, 100] tres = np.median(np.diff(v1.index.tolist())) tres = 1 * int(tres.total_seconds() / 60) # vf1 = dataset.ts_aggregate_timebins(v1.to_frame(), time_bin=tres, operations={'': np.nanmean}, index_position='middle') # come up with leg coloring leg_series = dataset.add_legs_index(v1)["leg"] vcol = dataset.ts_aggregate_timebins( v1, time_bin=tres, operations={"": options["resampling_operation"]}, index_position="initial", ) vcol.columns = ["color"] min_ = np.percentile(vcol.dropna(), stretch[0]) max_ = np.percentile(vcol.dropna(), stretch[1]) # print(min_,max_) vcol[vcol < min_] = min_ vcol[vcol > max_] = max_ coordinates_raw = dataset.read_standard_dataframe( options["gps_file"])[["latitude", "longitude"]] # mode_ = lambda x : stats.mode(x)[0] # Deal with coordinates coordinates = dataset.ts_aggregate_timebins( coordinates_raw, time_bin=int(np.floor(options["map_temporal_aggregation"] * 60)), operations={"": np.nanmedian}, index_position="initial", ) # Resample and merge coordinates + data to_plot = pd.merge(coordinates, v1, left_index=True, right_index=True) to_plot = pd.merge( to_plot, pd.DataFrame(data=to_plot.index.tolist(), columns=["date"], index=to_plot.index), left_index=True, right_index=True, ) to_plot = to_plot.dropna() if options["kml_file"]: kml_ = simplekml.Kml() fol = kml_.newfolder(name="LV_KML") colz = (to_plot.loc[:, vname] - to_plot.loc[:, vname].min()) / ( to_plot.loc[:, vname].max() - to_plot.loc[:, vname].min()) colz = np.floor(255 * plt.cm.Spectral_r(colz.values)).astype(int) c = 0 for lat, lon, val, date in to_plot.values: # print(lat, lon, val, date) # print(row[1].loc['LV#11']) pnt = fol.newpoint(name=str(date), coords=[(lon, lat)]) pnt.style.iconstyle.icon.href = ( "http://maps.google.com/mapfiles/kml/shapes/shaded_dot.png") pnt.style.iconstyle.scale = 1 # Icon thrice as big pnt.style.iconstyle.color = simplekml.Color.rgb( colz[c, 0], colz[c, 1], colz[c, 2], 255) pnt.style.labelstyle.scale = 0.5 c += 1 kml_.save(options["kml_file"]) # colorbar limits min_v1 = min_ # np.percentile(to_plot.loc[:,vname].dropna(), stretch[0]) max_v1 = max_ # np.percentile(to_plot.loc[:,vname].dropna(), stretch[1]) # Create geoviews datasets v1_tomap = hv.Dataset( to_plot.loc[:, ["longitude", "latitude", "date", vname]], kdims=["longitude", "latitude"], vdims=[hv.Dimension(vname, range=(min_v1, max_v1))], group=vname, ) points_v1 = v1_tomap.to(gv.Points, kdims=["longitude", "latitude"]) gps_track = gv.Dataset(coordinates_raw) track = gv.Path(gps_track, kdims=["longitude", "latitude"]) # land_ = gf.land#.options(facecolor='red') point_map_v1 = points_v1.opts( projection=ccrs.SouthPolarStereo(), cmap=options["colormap"], size=5, tools=["hover"], # ['hover'], width=500, height=400, color_index=2, colorbar=True, ) track_map = track.opts(projection=ccrs.SouthPolarStereo()).opts( color="black") return (gf.land * gf.coastline * track_map * point_map_v1).opts( title=vname, width=options["figsize"][0], height=options["figsize"][1])
def mesh(self, tiles=False, **kwargs): x = kwargs.get("x", self._obj.SCHISM_hgrid_node_x[:].values) y = kwargs.get("y", self._obj.SCHISM_hgrid_node_y[:].values) tes = kwargs.get("tri3", self._obj.SCHISM_hgrid_face_nodes.values) width = kwargs.get("width", 800) height = kwargs.get("height", 600) opts.defaults(opts.WMTS(width=width, height=height)) tile = gv.WMTS("https://b.tile.openstreetmap.org/{Z}/{X}/{Y}.png") nodes = pd.DataFrame({"longitude": x, "latitude": y}) points = gv.operation.project_points(gv.Points(nodes), projection=ccrs.PlateCarree()) if tes.shape[1] == 3: elems = pd.DataFrame(tes, columns=["a", "b", "c"]) if elems.min().min() > 0: elems = elems - 1 trimesh = gv.TriMesh((elems, points)).edgepaths if tiles: return tile * datashade( trimesh, precompute=True, cmap=["black"]) else: return datashade(trimesh, precompute=True, cmap=["green"]).opts(width=width, height=height) else: # there are quads elems = pd.DataFrame(tes, columns=["a", "b", "c", "d"]) if elems.min().min() > 0: elems = elems - 1 quads = elems.loc[~elems.d.isna()].copy() quads = quads.reset_index(drop=True) ap = nodes.loc[quads.a, ["longitude", "latitude"]] bp = nodes.loc[quads.b, ["longitude", "latitude"]] cp = nodes.loc[quads.c, ["longitude", "latitude"]] dp = nodes.loc[quads.d, ["longitude", "latitude"]] quads["ap"] = ap.values.tolist() quads["bp"] = bp.values.tolist() quads["cp"] = cp.values.tolist() quads["dp"] = dp.values.tolist() n = 2 al = quads.ap + quads.bp + quads.cp + quads.dp + quads.ap coords = [[l[i:i + n] for i in range(0, len(l), n)] for l in al] quads["coordinates"] = coords qpolys = pygeos.polygons(quads.coordinates.to_list()) df = gp.GeoDataFrame(geometry=qpolys) df_ = spatialpandas.GeoDataFrame(df) q = gv.Path(df_) qdf = dynspread( rasterize(q, precompute=True).options(cmap=["black"])) triangles = elems.loc[elems.d.isna()] trimesh = gv.TriMesh((triangles, points)).edgepaths wireframe = dynspread( rasterize(trimesh, precompute=True).opts(cmap=["black"])) if tiles: return tile * wireframe * qdf else: return wireframe.opts(cmap=["green"]) * qdf.opts( cmap=["green"])
fdir_d = os.getcwd() + "/data/Cuencas/Regiones_Hidrologicas_Administrativas/" fdir_r = os.getcwd() + "/results/sequia/" fname = "rha250kgw.shp" # Si no existe la carpeta, la crea. if not os.path.exists(fdir_r): os.mkdir(fdir_r) # Se cargan las regiones hidrológico administrativas. gdf = gpd.read_file(fdir_d + fname) # Se obtiene el contorno de las cuencas- gdf["boundary"] = gdf.boundary # Se selecciona la Cuenca del Valle de México. cuenca = gv.Path( gdf[gdf["ORG_CUENCA"] == "Aguas del Valle de México"]).opts( color = "black") # Número de puntos de grid a revisar en dirección de la longitud y la latitud. n = 4 lon = np.empty((1, n)) lat = np.empty((n, 1)) lonp = np.empty((1, n)) latp = np.empty((n, 1)) # Pivotes iniciales de lon y lat. lon_0 = -99.75 lat_0 = 20.75 # Número de punto de grid con respecto a los archivos mexico_cru. lonp_0 = 38
def mapDate(df): smp_pnt_df = smp_pnt.merge(df, on='id').dropna().reset_index(drop=True) basin = gv.Contours(nahalal_basin) # bbox minx, maxx, miny, maxy = boundingBox(nahalal_basin) #valid results if len(smp_pnt_df) > 0: #clean column names smp_pnt_df.columns = smp_pnt_df.columns.map( lambda x: x.replace('-', '_')) pol = smp_pnt_df.columns[-1] print(pol) cmap, symmetric, pname = cmapAndPName(pol) # cmap/symmetric if pol == 'pH': smp_pnt_df['pH0'] = smp_pnt_df['pH'] - 7 #vdims vdims = ["id", "description", pol] if pol == 'pH': vdims = vdims + ['pH0'] #pnts pnts = gv.Points(smp_pnt_df, vdims=vdims) if pol == 'pH': pnts = gv.Points(smp_pnt_df, vdims=vdims) hover = HoverTool(tooltips=[ ("Point #", "@id"), ("Description", '@description'), #("{}".format(yLabel(pol)), '(@{})'.format(pol))] ("{}".format(yLabel(pol)).replace('_', '-'), '@{}'.format(pol)) ]) ##map points_map = ((gts.EsriImagery.opts(alpha=0.4) * gts.OSM.opts(alpha=0.4) * basin.opts(color='chocolate', line_width=2, alpha=.5) * gv.Path(kishon).opts(line_width=3.5, alpha=0.4, line_color="darkblue", line_dash="dashed") * gv.Path(nahalal).opts( line_width=2.5, alpha=0.4, line_color="blue") * gv.Path(nahalal_sub).opts(line_width=2, line_dash="dashdot", alpha=0.4, line_color="blue") * pnts.opts(size=15, alpha=0.8, tools=[hover], color_index=pname, cmap=cmap, line_color='purple', symmetric=symmetric)).opts( width=500, height=375, fontsize={ 'labels': labels_fsize, 'xticks': tick_fsize, 'yticks': tick_fsize }).redim.range(Longitude=(minx, maxx), Latitude=(miny, maxy))) else: points_map = (( gts.EsriImagery.opts(alpha=0.4) * gts.OSM.opts(alpha=0.4) * basin.opts(color='chocolate', line_width=1.5, alpha=.5) * gv.Path(kishon).opts(line_width=3.5, alpha=0.4, line_color="darkblue", line_dash="dashed") * gv.Path(nahalal).opts(line_width=2.5, alpha=0.4, line_color="blue") * gv.Path(nahalal_sub).opts(line_width=2, line_dash="dashdot", alpha=0.4, line_color="blue")).opts( width=500, height=375, fontsize={ 'labels': labels_fsize, 'xticks': tick_fsize, 'yticks': tick_fsize }).redim.range(Longitude=(minx, maxx), Latitude=(miny, maxy))) return points_map
ant_flights = gpd.read_file(DATA_DIR.joinpath('ant_flights.geojson')) # Define low-res mask for Antarctica gdf_mask = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")) gdf_mask = gdf_mask[gdf_mask.continent == 'Antarctica'] gdf_mask = gdf_mask.to_crs(epsg=3031) ant_flights = gpd.clip(ant_flights, gdf_mask) # %% # Map of core and flightline locations Ant_bnds = gv.Shape.from_shapefile(str(ant_path), crs=ANT_proj).opts(projection=ANT_proj, width=2000, height=2000) flight_plt = gv.Path(ant_flights, crs=ANT_proj).opts(projection=ANT_proj, alpha=0.2, color='black', line_width=2.5) cores_plt = gv.Points(data=core_locs[core_locs['Duration'] >= 20], vdims=['Name'], crs=ANT_proj).opts(projection=ANT_proj, color='blue', size=10, tools=['hover']) ant2k_plt = gv.Points(data=ant2k_locs, vdims=['Site'], crs=ANT_proj).opts(projection=ANT_proj, color='red', size=10, tools=['hover']) Ant_bnds * flight_plt * cores_plt * ant2k_plt
opts = dict(colorbar=True, cmap="bwr_r") spi_ds = spi_ds.sel(time=list(df_min.loc[0:4, "time"])) spi = gv.Dataset(spi_ds, kdims=kdims, vdims=spi_vdims) spei_ds = spei_ds.sel(time=list(df_min.loc[0:4, "time"])) spei = gv.Dataset(spei_ds, kdims=kdims, vdims=spei_vdims) fdir = path + "Cuencas/Regiones Hidrológicas Administrativas " fdir += "(Organismos de Cuencas) Coordenadas Geográficas/" fname = "rha250kgw.shp" gdf = gpd.read_file(fdir + fname) gdf["boundary"] = gdf.boundary cuenca = gv.Path(gdf[gdf["ORG_CUENCA"] == "Aguas del Valle de México"]).opts( color="black", linewidth=1.25) fdir = path + "Cuencas/Contorno de México 1-4,000,000/" fname = "conto4mgw.shp" gdf = gpd.read_file(fdir + fname) mexico = gv.Path(gdf).opts(color="black", linewidth=1.25) img_spi = (spi.to(gv.Image, ["lon", "lat"]).opts(**opts) * gf.coastline * cuenca * mexico) hv.save(img_spi, out_path + "spi.html") ''' spi = spi.to(gv.Image, ["lon", "lat"]).opts(**opts) for date in df_min["time"]: img_spi = spi.select(time = date) * gf.coastline * cuenca * mexico #img_spei = spei.select(time = date).to(gv.Image, ["lon", "lat"]).opts(