From 32cb98446e9ed36413bee2ba1a6d4e8e320c4905 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:04:22 +0100 Subject: [PATCH 01/28] Add interactive chart if screen >800px and existing --- scripts/overview.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/scripts/overview.php b/scripts/overview.php index 29bfee391..23c2f41b7 100644 --- a/scripts/overview.php +++ b/scripts/overview.php @@ -12,6 +12,7 @@ set_timezone(); $myDate = date('Y-m-d'); $chart = "Combo-$myDate.png"; +$interactivechart = "interactive_daily_plot.html"; $db = new SQLite3('./scripts/birds.db', SQLITE3_OPEN_READONLY); $db->busyTimeout(1000); @@ -320,10 +321,20 @@ function setModalText(iter, title, text, authorlink, photolink, licenseurl) { $dividedrefresh = 1; } $time = time(); -if (file_exists('./Charts/'.$chart)) { - echo ""; -} +$interactivechart_path = './Charts/' . $interactivechart; +$chart_path = './Charts/' . $chart; +if (file_exists($interactivechart_path)) { + $html_content = file_get_contents($interactivechart_path); + echo $html_content; +} elseif (file_exists($chart_path)) { + echo ""; +} ?> +
From e0908f62ea1307f8e0873927b8bab2e57725a403 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 30 Oct 2024 09:04:47 +0100 Subject: [PATCH 02/28] Create dynamic_plot.py --- scripts/dynamic_plot.py | 202 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 scripts/dynamic_plot.py diff --git a/scripts/dynamic_plot.py b/scripts/dynamic_plot.py new file mode 100644 index 000000000..dd256aadd --- /dev/null +++ b/scripts/dynamic_plot.py @@ -0,0 +1,202 @@ +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import numpy as np +from datetime import datetime +import os +from utils.helpers import get_settings # Import to access COLOR_SCHEME + +# Define custom colorscales for light and dark modes +COLOR_SCALES = { + "light": [ + [0.0, '#E0F2E9'], [0.2, '#A3D8A1'], [0.4, '#70BD70'], + [0.6, '#46A846'], [0.8, '#2E7D2E'], [1.0, '#004D00'] + ], + "dark": [ + [0.0, '#F0F0F0'], [0.2, '#BDBDBD'], [0.4, '#969696'], + [0.6, '#737373'], [0.8, '#525252'], [1.0, '#252525'] + ] +} +ALL_HOURS = list(range(24)) + + +def normalize_logarithmic(arr): + """Applies a logarithmic normalization to the array, mapping values between 0.5 and max(arr) to a normalized scale between 0 and 1.""" + arr = arr.astype(float) + min_val = 0.5 + arr = np.clip(arr, min_val, None) + return np.log(arr / min_val) / np.log(np.max(arr) / min_val) if np.max(arr) > min_val else arr - min_val + + +def determine_text_color(z, threshold=0.5): + """Determines text color (black or white) based on normalized value of z.""" + return np.where(z > threshold, 'white', 'black') + + +def add_annotations(fig, text_array, text_colors, col, row, species_list, all_hours, annotations): + """Collects annotations for the heatmap without adding them individually, appending them to the provided annotations list.""" + if col in [1, 2]: # Single-column heatmaps + for i, species in enumerate(species_list): + current_text, current_color = text_array[i, 0], text_colors[i, 0] + if current_text: + annotations.append(dict( + x=0, y=species, text=current_text, showarrow=False, + font=dict(color=current_color, size=10), + xref=f'x{col}', yref=f'y{col}', xanchor='center', yanchor='middle' + )) + elif col == 3: # Multi-column heatmap + for i, species in enumerate(species_list): + for j, hour in enumerate(all_hours): + current_text, current_color = text_array[i, j], text_colors[i, j] + if current_text: + annotations.append(dict( + x=hour, y=species, text=current_text, showarrow=False, + font=dict(color=current_color, size=10), + xref='x3', yref='y3', xanchor='center', yanchor='middle' + )) + + +def create_plotly_heatmap(df_birds, now): + """Creates a Plotly heatmap with annotations based on bird detection data.""" + # Titles and Subtitle + main_title = f"Hourly Overview Updated at {now.strftime('%Y-%m-%d %H:%M:%S')}" + subtitle = f"({df_birds['Com_Name'].nunique()} species today; {len(df_birds)} detections today)" + + # Ensure 'Time' is datetime + if not pd.api.types.is_datetime64_any_dtype(df_birds['Time']): + df_birds['Time'] = pd.to_datetime(df_birds['Time'], unit='ns') + + df_birds['Hour'] = df_birds['Time'].dt.hour + + # Group data and fill missing values + plot_dataframe = df_birds.groupby(['Hour', 'Com_Name']).agg( + Count=('Com_Name', 'count'), + Conf=('Confidence', 'max') + ).reset_index().fillna({'Conf': 0, 'Count': 0}) + + # Fetch color scheme setting and choose corresponding colorscale and text color + conf = get_settings() + color_scheme = conf.get('COLOR_SCHEME', 'light') + color_scale = COLOR_SCALES.get(color_scheme, COLOR_SCALES["light"]) + + # Summarize data for heatmap axes + df_birds_summary = plot_dataframe.groupby('Com_Name').agg( + Count=('Count', 'sum'), + Conf=('Conf', 'max') + ).reset_index() + df_birds_summary = df_birds_summary[df_birds_summary['Count'] > 0] + df_birds_summary.sort_values(by=['Count', 'Conf'], ascending=[False, False], inplace=True) + species_list = df_birds_summary['Com_Name'].tolist() + + # Normalize values and prepare text annotations + z_confidence = normalize_logarithmic(df_birds_summary['Conf'].values.reshape(-1, 1) * 100) + text_confidence = np.char.add(np.round(df_birds_summary['Conf'].values).astype(int).astype(str), ' %') + + z_detections = normalize_logarithmic(df_birds_summary['Count'].values.reshape(-1, 1)) + text_detections = df_birds_summary['Count'].astype(str).values # Use actual counts for annotations + text_color_detections = determine_text_color(z_detections, threshold=0.5, color_scheme=color_scheme) + + df_hourly = plot_dataframe.pivot_table(index='Com_Name', columns='Hour', values='Count', aggfunc='sum').fillna(0) + df_hourly = df_hourly.reindex(species_list).fillna(0).reindex(columns=ALL_HOURS, fill_value=0) + z_hourly = normalize_logarithmic(df_hourly.values) + text_hourly = df_hourly.astype(int).astype(str).values # Use actual counts for hourly annotations + text_color_hourly = determine_text_color(z_hourly, threshold=0.5, color_scheme=color_scheme) + + # Create subplots + fig = make_subplots(rows=1, cols=3, shared_yaxes=True, column_widths=[0.1, 0.1, 0.7], horizontal_spacing=0.02) + + # Heatmap Traces + fig.add_trace(go.Heatmap( + z=z_confidence, customdata=df_birds_summary['Conf'].values, x=['Confidence'], y=species_list, + colorscale=color_scale, showscale=False, hovertemplate='Species: %{y}