From c27ffb1f418918eb55cac38bd99938463559e925 Mon Sep 17 00:00:00 2001 From: Yun Wu Date: Wed, 18 Dec 2024 15:45:21 +0800 Subject: [PATCH 1/5] Add a historic time series plot type --- ecoscope/plotting/plot.py | 90 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/ecoscope/plotting/plot.py b/ecoscope/plotting/plot.py index a8647081..2c212e54 100644 --- a/ecoscope/plotting/plot.py +++ b/ecoscope/plotting/plot.py @@ -5,9 +5,9 @@ from ecoscope.base.utils import color_tuple_to_css try: - from sklearn.neighbors import KernelDensity import plotly.graph_objs as go from plotly.subplots import make_subplots + from sklearn.neighbors import KernelDensity except ModuleNotFoundError: raise ModuleNotFoundError( 'Missing optional dependencies required by this module. \ @@ -386,3 +386,91 @@ def pie_chart( fig = go.Figure(data=go.Pie(labels=labels, values=values, **style_kwargs), layout=layout_kwargs) return fig + + +def draw_historic_timeseries( + df: pd.DataFrame, + current_value_column: str, + current_value_title: str, + historic_min_column: str = None, + historic_max_column: str = None, + historic_band_title: str = "Historic Min-Max", + historic_mean_column: str = None, + historic_mean_title: str = "Historic Mean", + layout_kwargs: dict = None, +): + # todo: update doc + """ + Creates a timeseries plot compared with historical values + Parameters + ---------- + df: pd.Dataframe + The data to plot + value_column: str + The name of the dataframe column to pull slice values from + If the column contains non-numeric values, it is assumed to be categorical + and the pie slices will be a count of the occurrences of the category + label_column: str + The name of the dataframe column to label slices with, required if the data in value_column is numeric + style_kwargs: dict + Additional style kwargs passed to go.Pie() + layout_kwargs: dict + Additional kwargs passed to plotly.go.Figure(layout) + Returns + ------- + fig : plotly.graph_objects.Figure + The plotly bar chart + """ + + fig = go.Figure(layout=layout_kwargs) + + # add the upper bound + if historic_max_column and historic_min_column: + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[historic_max_column], + fill=None, + mode="lines", + line_color="green", + name="", + showlegend=False, + ) + ) + + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[historic_min_column], + fill="tonexty", + mode="lines", + line_color="green", + name=historic_band_title, + ) + ) + + if historic_mean_column: + # add the historic mean + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[historic_mean_column], + fill=None, + mode="lines", + line=dict(color="green", dash="dot"), + name=historic_mean_title, + ) + ) + + # add current NDVI values + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[current_value_column], + fill=None, + mode="lines", + line_color="navy", + name=current_value_title, + ) + ) + return fig From 87332c618ae5f2b79be041eef3e23e0f90c0c421 Mon Sep 17 00:00:00 2001 From: Yun Wu Date: Wed, 18 Dec 2024 17:14:09 +0800 Subject: [PATCH 2/5] doc --- ecoscope/plotting/plot.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/ecoscope/plotting/plot.py b/ecoscope/plotting/plot.py index 2c212e54..6fbc4d2d 100644 --- a/ecoscope/plotting/plot.py +++ b/ecoscope/plotting/plot.py @@ -399,21 +399,26 @@ def draw_historic_timeseries( historic_mean_title: str = "Historic Mean", layout_kwargs: dict = None, ): - # todo: update doc """ Creates a timeseries plot compared with historical values Parameters ---------- df: pd.Dataframe The data to plot - value_column: str + current_value_column: str The name of the dataframe column to pull slice values from - If the column contains non-numeric values, it is assumed to be categorical - and the pie slices will be a count of the occurrences of the category - label_column: str - The name of the dataframe column to label slices with, required if the data in value_column is numeric - style_kwargs: dict - Additional style kwargs passed to go.Pie() + current_value_title: str + The title shown in the plot legend for current value + historic_min_column: str + The name of the dataframe column to pull historic min values from. historic_min_column and historic_max_column should exist together. + historic_max_column: str + The name of the dataframe column to pull historic max values from. historic_min_column and historic_max_column should exist together. + historic_band_title: str + The title shown in the plot legend for historic band + historic_mean_column: str + The name of the dataframe column to pull historic mean values from + current_value_title: str + The title shown in the plot legend for historic mean values layout_kwargs: dict Additional kwargs passed to plotly.go.Figure(layout) Returns @@ -424,8 +429,8 @@ def draw_historic_timeseries( fig = go.Figure(layout=layout_kwargs) - # add the upper bound if historic_max_column and historic_min_column: + # add the upper bound fig.add_trace( go.Scatter( x=df.img_date, @@ -437,7 +442,8 @@ def draw_historic_timeseries( showlegend=False, ) ) - + + # lower band fig.add_trace( go.Scatter( x=df.img_date, @@ -473,4 +479,5 @@ def draw_historic_timeseries( name=current_value_title, ) ) + return fig From a5e167d61ddf6e792536203da3c9e70438cd6ab9 Mon Sep 17 00:00:00 2001 From: Yun Wu Date: Wed, 18 Dec 2024 18:12:44 +0800 Subject: [PATCH 3/5] unittest --- ecoscope/io/earthranger.py | 11 ++++---- tests/test_ecoplot.py | 52 +++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/ecoscope/io/earthranger.py b/ecoscope/io/earthranger.py index f15fafce..acb3891d 100644 --- a/ecoscope/io/earthranger.py +++ b/ecoscope/io/earthranger.py @@ -3,25 +3,24 @@ import math import typing +import ecoscope import geopandas as gpd import numpy as np import pandas as pd import pytz import requests -from erclient.client import ERClient, ERClientException, ERClientNotFound -from shapely.geometry import shape -from tqdm.auto import tqdm - -import ecoscope from ecoscope.io.earthranger_utils import ( clean_kwargs, clean_time_cols, dataframe_to_dict, format_iso_time, + pack_columns, to_gdf, to_hex, - pack_columns, ) +from erclient.client import ERClient, ERClientException, ERClientNotFound +from shapely.geometry import shape +from tqdm.auto import tqdm class EarthRangerIO(ERClient): diff --git a/tests/test_ecoplot.py b/tests/test_ecoplot.py index e71b69ae..9df05efd 100644 --- a/tests/test_ecoplot.py +++ b/tests/test_ecoplot.py @@ -1,9 +1,19 @@ -import pytest import numpy as np import pandas as pd -from ecoscope.plotting.plot import EcoPlotData, ecoplot, mcp, nsd, speed, stacked_bar_chart, pie_chart -from ecoscope.base import Trajectory +import pytest + from ecoscope.analysis.classifier import apply_color_map +from ecoscope.base import Trajectory +from ecoscope.plotting.plot import ( + EcoPlotData, + draw_historic_timeseries, + ecoplot, + mcp, + nsd, + pie_chart, + speed, + stacked_bar_chart, +) @pytest.fixture @@ -175,3 +185,39 @@ def test_pie_chart_numerical(chart_df): "rgba(0, 0, 255, 1.0)", "rgba(255, 255, 255, 1.0)", ) + + +def test_draw_historic_timeseries(): + df = pd.DataFrame( + { + "min": [0.351723, 0.351723, 0.303219, 0.303219, 0.342149, 0.342149], + "max": [0.667335, 0.667335, 0.708183, 0.708183, 0.727095, 0.727095], + "mean": [0.472017, 0.472017, 0.543062, 0.543062, 0.555547, 0.555547], + "NDVI": [0.609656, 0.671868, 0.680008, 0.586662, 0.612170, np.nan], + "img_date": pd.to_datetime( + [ + "2023-11-09", + "2023-11-25", + "2023-12-11", + "2023-12-27", + "2024-01-09", + "2024-01-25", + ] + ), + } + ) + + chart = draw_historic_timeseries( + df, + current_value_column="NDVI", + current_value_title="NDVI", + historic_min_column="min", + historic_max_column="max", + historic_mean_column="mean", + ) + + assert len(chart.data) == 4 + assert chart.data[0].showlegend is False + assert chart.data[1].name == "Historic Min-Max" + assert chart.data[2].name == "Historic Mean" + assert chart.data[3].name == "NDVI" From f972c84105ccd6c5a2eefb9a0892dac36f697ffc Mon Sep 17 00:00:00 2001 From: Yun Wu Date: Wed, 18 Dec 2024 21:57:10 +0800 Subject: [PATCH 4/5] add style parameters --- ecoscope/plotting/plot.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/ecoscope/plotting/plot.py b/ecoscope/plotting/plot.py index 6fbc4d2d..00dc0a8c 100644 --- a/ecoscope/plotting/plot.py +++ b/ecoscope/plotting/plot.py @@ -398,6 +398,9 @@ def draw_historic_timeseries( historic_mean_column: str = None, historic_mean_title: str = "Historic Mean", layout_kwargs: dict = None, + upper_lower_band_style: dict = {"mode": "lines", "line_color": "green"}, + historic_mean_style: dict = {"mode": "lines", "line": {"color": "green", "dash": "dot"}}, + current_value_style: dict = {"mode": "lines", "line_color": "navy"}, ): """ Creates a timeseries plot compared with historical values @@ -410,9 +413,11 @@ def draw_historic_timeseries( current_value_title: str The title shown in the plot legend for current value historic_min_column: str - The name of the dataframe column to pull historic min values from. historic_min_column and historic_max_column should exist together. + The name of the dataframe column to pull historic min values from. + historic_min_column and historic_max_column should exist together. historic_max_column: str - The name of the dataframe column to pull historic max values from. historic_min_column and historic_max_column should exist together. + The name of the dataframe column to pull historic max values from. + historic_min_column and historic_max_column should exist together. historic_band_title: str The title shown in the plot legend for historic band historic_mean_column: str @@ -436,22 +441,20 @@ def draw_historic_timeseries( x=df.img_date, y=df[historic_max_column], fill=None, - mode="lines", - line_color="green", name="", showlegend=False, + **upper_lower_band_style, ) ) - + # lower band fig.add_trace( go.Scatter( x=df.img_date, y=df[historic_min_column], fill="tonexty", - mode="lines", - line_color="green", name=historic_band_title, + **upper_lower_band_style, ) ) @@ -462,9 +465,8 @@ def draw_historic_timeseries( x=df.img_date, y=df[historic_mean_column], fill=None, - mode="lines", - line=dict(color="green", dash="dot"), name=historic_mean_title, + **historic_mean_style, ) ) @@ -474,9 +476,8 @@ def draw_historic_timeseries( x=df.img_date, y=df[current_value_column], fill=None, - mode="lines", - line_color="navy", name=current_value_title, + **current_value_style, ) ) From 45f63ef65f0c3571a8d7a86a93f1c7cec2f107b7 Mon Sep 17 00:00:00 2001 From: Yun Wu Date: Thu, 19 Dec 2024 11:02:37 +0800 Subject: [PATCH 5/5] nit --- ecoscope/plotting/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecoscope/plotting/plot.py b/ecoscope/plotting/plot.py index 00dc0a8c..1f530b65 100644 --- a/ecoscope/plotting/plot.py +++ b/ecoscope/plotting/plot.py @@ -470,7 +470,7 @@ def draw_historic_timeseries( ) ) - # add current NDVI values + # add current values fig.add_trace( go.Scatter( x=df.img_date,