diff --git a/ecoscope/io/earthranger.py b/ecoscope/io/earthranger.py index e282ccdb..90cddd03 100644 --- a/ecoscope/io/earthranger.py +++ b/ecoscope/io/earthranger.py @@ -3,26 +3,25 @@ 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, filter_bad_geojson, 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/ecoscope/plotting/plot.py b/ecoscope/plotting/plot.py index a8647081..1f530b65 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,99 @@ 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, + 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 + Parameters + ---------- + df: pd.Dataframe + The data to plot + current_value_column: str + The name of the dataframe column to pull slice values from + 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 + ------- + fig : plotly.graph_objects.Figure + The plotly bar chart + """ + + fig = go.Figure(layout=layout_kwargs) + + if historic_max_column and historic_min_column: + # add the upper bound + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[historic_max_column], + fill=None, + 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", + name=historic_band_title, + **upper_lower_band_style, + ) + ) + + if historic_mean_column: + # add the historic mean + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[historic_mean_column], + fill=None, + name=historic_mean_title, + **historic_mean_style, + ) + ) + + # add current values + fig.add_trace( + go.Scatter( + x=df.img_date, + y=df[current_value_column], + fill=None, + name=current_value_title, + **current_value_style, + ) + ) + + return fig 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"