From 920e13e3555741bd49de91bfda73f93fdc2144c1 Mon Sep 17 00:00:00 2001 From: Qiusheng Wu Date: Mon, 5 Aug 2024 13:16:17 -0400 Subject: [PATCH] Add support for displaying field data (#106) --- docs/examples/field_data.ipynb | 148 +++++++++++++++++++++++++++++++++ hypercoast/common.py | 108 ++++++++++++++++++++++++ hypercoast/hypercoast.py | 50 +++++++++++ mkdocs.yml | 1 + 4 files changed, 307 insertions(+) create mode 100644 docs/examples/field_data.ipynb diff --git a/docs/examples/field_data.ipynb b/docs/examples/field_data.ipynb new file mode 100644 index 00000000..3ac80e72 --- /dev/null +++ b/docs/examples/field_data.ipynb @@ -0,0 +1,148 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![image](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/opengeos/HyperCoast/blob/main/docs/examples/field_data.ipynb)\n", + "\n", + "# Visualizing Spectral Data from Field Measurements\n", + "\n", + "This notebook demonstrates how to visualize spectral data from field measurements. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install \"hypercoast[extra]\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import hypercoast\n", + "import pandas as pd" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Download a sample filed dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "url = \"https://github.com/opengeos/datasets/releases/download/hypercoast/pace_sample_points.csv\"\n", + "data = pd.read_csv(url)\n", + "data.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Download PACE data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "url = \"https://github.com/opengeos/datasets/releases/download/hypercoast/PACE_OCI.20240730T181157.L2.OC_AOP.V2_0.NRT.nc\"\n", + "filepath = \"../../private/data/PACE_OCI.20240730T181157.L2.OC_AOP.V2_0.NRT.nc\"\n", + "hypercoast.download_file(url, filepath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Read the PACE dataset." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataset = hypercoast.read_pace(filepath)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Run the following cell to show the map. Click on the markers to see the spectral data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = hypercoast.Map(center=[27.235094, -87.791748], zoom=6)\n", + "\n", + "m.add_basemap(\"Hybrid\")\n", + "wavelengths = [450, 550, 650]\n", + "m.add_pace(\n", + " dataset, wavelengths, indexes=[3, 2, 1], vmin=0, vmax=0.02, layer_name=\"PACE\"\n", + ")\n", + "m.add(\"spectral\")\n", + "\n", + "m.add_field_data(\n", + " data,\n", + " x_col=\"wavelength\",\n", + " y_col_prefix=\"(\",\n", + " x_label=\"Wavelength (nm)\",\n", + " y_label=\"Reflectance\",\n", + " use_marker_cluster=True,\n", + ")\n", + "m.set_center(-87.791748, 27.235094, zoom=6)\n", + "m" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![](https://i.imgur.com/Q8pBQXg.png)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hyper", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/hypercoast/common.py b/hypercoast/common.py index 0259af88..e33396e8 100644 --- a/hypercoast/common.py +++ b/hypercoast/common.py @@ -1007,3 +1007,111 @@ def save_geotiff(file_path, image, profile): image, profile = load_geotiff(input_file) pca_image = perform_pca(image, n_components, **kwargs) save_geotiff(output_file, pca_image, profile) + + +def show_field_data( + data: Union[str], + x_col: str = "wavelength", + y_col_prefix: str = "(", + x_label: str = "Wavelengths (nm)", + y_label: str = "Reflectance", + use_marker_cluster: bool = True, + min_width: int = 400, + max_width: int = 600, + min_height: int = 200, + max_height: int = 250, + layer_name: str = "Marker Cluster", + m: object = None, + center: Tuple[float, float] = (20, 0), + zoom: int = 2, +): + """ + Displays field data on a map with interactive markers and popups showing time series data. + + Args: + data (Union[str, pd.DataFrame]): Path to the CSV file or a pandas DataFrame containing the data. + x_col (str): Column name to use for the x-axis of the charts. Default is "wavelength". + y_col_prefix (str): Prefix to identify the columns that contain the location-specific data. Default is "(". + x_label (str): Label for the x-axis of the charts. Default is "Wavelengths (nm)". + y_label (str): Label for the y-axis of the charts. Default is "Reflectance". + use_marker_cluster (bool): Whether to use marker clustering. Default is True. + min_width (int): Minimum width of the popup. Default is 400. + max_width (int): Maximum width of the popup. Default is 600. + min_height (int): Minimum height of the popup. Default is 200. + max_height (int): Maximum height of the popup. Default is 250. + layer_name (str): Name of the marker cluster layer. Default is "Marker Cluster". + m (Map, optional): An ipyleaflet Map instance to add the markers to. Default is None. + center (Tuple[float, float]): Center of the map as a tuple of (latitude, longitude). Default is (20, 0). + zoom (int): Zoom level of the map. Default is 2. + + Returns: + Map: An ipyleaflet Map with the added markers and popups. + """ + import pandas as pd + import matplotlib.pyplot as plt + from ipyleaflet import Map, Marker, Popup, MarkerCluster + from ipywidgets import Output, VBox + + # Read the CSV file + if isinstance(data, str): + data = pd.read_csv(data) + elif isinstance(data, pd.DataFrame): + pass + else: + raise ValueError("data must be a path to a CSV file or a pandas DataFrame") + + # Extract locations from columns + locations = [col for col in data.columns if col.startswith(y_col_prefix)] + coordinates = [tuple(map(float, loc.strip("()").split())) for loc in locations] + + # Create the map + if m is None: + m = Map(center=center, zoom=zoom) + + # Function to create the chart + def create_chart(data, title): + fig, ax = plt.subplots(figsize=(10, 6)) # Adjust the figure size here + ax.plot(data[x_col], data["values"]) + ax.set_title(title) + ax.set_xlabel(x_label) + ax.set_ylabel(y_label) + output = Output() # Adjust the output widget size here + with output: + plt.show() + return output + + # Define a callback function to create and show the popup + def callback_with_popup_creation(location, values): + def f(**kwargs): + marker_center = kwargs["coordinates"] + output = create_chart(values, f"Location: {location}") + popup = Popup( + location=marker_center, + child=VBox([output]), + min_width=min_width, + max_width=max_width, + min_height=min_height, + max_height=max_height, + ) + m.add_layer(popup) + + return f + + markers = [] + + # Add points to the map + for i, coord in enumerate(coordinates): + location = f"{coord}" + values = pd.DataFrame({x_col: data[x_col], "values": data[locations[i]]}) + marker = Marker(location=coord, title=location, name=f"Marker {i + 1}") + marker.on_click(callback_with_popup_creation(location, values)) + markers.append(marker) + + if use_marker_cluster: + marker_cluster = MarkerCluster(markers=markers, name=layer_name) + m.add_layer(marker_cluster) + else: + for marker in markers: + m.add_layer(marker) + + return m diff --git a/hypercoast/hypercoast.py b/hypercoast/hypercoast.py index 537cc8b0..8691b219 100644 --- a/hypercoast/hypercoast.py +++ b/hypercoast/hypercoast.py @@ -824,3 +824,53 @@ def find_nearest_indices( self.cog_layer_dict[layer_name]["vis_bands"] = vis_bands except Exception as e: print(e) + + def add_field_data( + self, + data: Union[str], + x_col: str = "wavelength", + y_col_prefix: str = "(", + x_label: str = "Wavelengths (nm)", + y_label: str = "Reflectance", + use_marker_cluster: bool = True, + min_width: int = 400, + max_width: int = 600, + min_height: int = 200, + max_height: int = 250, + layer_name: str = "Marker Cluster", + **kwargs, + ): + """ + Displays field data on a map with interactive markers and popups showing time series data. + + Args: + data (Union[str, pd.DataFrame]): Path to the CSV file or a pandas DataFrame containing the data. + x_col (str): Column name to use for the x-axis of the charts. Default is "wavelength". + y_col_prefix (str): Prefix to identify the columns that contain the location-specific data. Default is "(". + x_label (str): Label for the x-axis of the charts. Default is "Wavelengths (nm)". + y_label (str): Label for the y-axis of the charts. Default is "Reflectance". + use_marker_cluster (bool): Whether to use marker clustering. Default is True. + min_width (int): Minimum width of the popup. Default is 400. + max_width (int): Maximum width of the popup. Default is 600. + min_height (int): Minimum height of the popup. Default is 200. + max_height (int): Maximum height of the popup. Default is 250. + layer_name (str): Name of the marker cluster layer. Default is "Marker Cluster". + + Returns: + Map: An ipyleaflet Map with the added markers and popups. + """ + show_field_data( + data, + x_col, + y_col_prefix, + x_label=x_label, + y_label=y_label, + use_marker_cluster=use_marker_cluster, + min_width=min_width, + max_width=max_width, + min_height=min_height, + max_height=max_height, + layer_name=layer_name, + m=self, + **kwargs, + ) diff --git a/mkdocs.yml b/mkdocs.yml index df8d837f..5555cfdd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -106,6 +106,7 @@ nav: - examples/pace_oci_l2.ipynb - examples/multispectral.ipynb - examples/pca.ipynb + - examples/field_data.ipynb - Workshops: - workshops/emit.ipynb - API Reference: