diff --git a/marimo_eda/Individual_LWF_Deposition.py b/marimo_eda/Individual_LWF_Deposition.py
new file mode 100644
index 0000000..5f5f0b0
--- /dev/null
+++ b/marimo_eda/Individual_LWF_Deposition.py
@@ -0,0 +1,243 @@
+import marimo
+
+__generated_with = "0.24.0"
+app = marimo.App(width="medium")
+
+
+@app.cell
+def _():
+ import pandas as pd
+ import marimo as mo
+ from pathlib import Path
+
+ return Path, mo, pd
+
+
+@app.cell
+def _(Path, pd):
+ foliage_path = Path("lwf_foliage_dw100_i_2026-07-30.csv")
+
+ foliage = pd.read_csv(
+ foliage_path,
+ sep=";",
+ )
+ return (foliage,)
+
+
+@app.cell
+def _(foliage):
+ foliage.head()
+ return
+
+
+@app.cell
+def _(foliage, pd):
+ foliage["survey_date"] = pd.to_datetime(
+ foliage["survey_date"],
+ errors="coerce",
+ )
+ return
+
+
+@app.cell
+def _(foliage, mo):
+ plot_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ foliage["plot_id"]
+ .dropna()
+ .unique()
+ .tolist()
+ ),
+ value="All",
+ label="LWF site",
+ )
+
+ species_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ foliage["species"]
+ .dropna()
+ .unique()
+ .tolist()
+ ),
+ value="All",
+ label="Species",
+ )
+
+ leaf_type_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ foliage["leaf_type"]
+ .dropna()
+ .unique()
+ .tolist()
+ ),
+ value="All",
+ label="Leaf type",
+ )
+
+ age_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ foliage["leaf_age_class"]
+ .dropna()
+ .unique()
+ .tolist()
+ ),
+ value="All",
+ label="Leaf age class",
+ )
+
+ mo.vstack([
+ mo.hstack([
+ plot_selector,
+ species_selector,
+ ]),
+ mo.hstack([
+ leaf_type_selector,
+ age_selector,
+ ]),
+ ])
+ return age_selector, leaf_type_selector, plot_selector, species_selector
+
+
+@app.cell
+def _(
+ age_selector,
+ foliage,
+ leaf_type_selector,
+ plot_selector,
+ species_selector,
+):
+ filtered_foliage = foliage.copy()
+
+ if plot_selector.value != "All":
+ filtered_foliage = filtered_foliage[
+ filtered_foliage["plot_id"] == plot_selector.value
+ ]
+
+ if species_selector.value != "All":
+ filtered_foliage = filtered_foliage[
+ filtered_foliage["species"] == species_selector.value
+ ]
+
+ if leaf_type_selector.value != "All":
+ filtered_foliage = filtered_foliage[
+ filtered_foliage["leaf_type"] == leaf_type_selector.value
+ ]
+
+ if age_selector.value != "All":
+ filtered_foliage = filtered_foliage[
+ filtered_foliage["leaf_age_class"] == age_selector.value
+ ]
+
+ filtered_foliage
+ return (filtered_foliage,)
+
+
+@app.cell
+def _(filtered_foliage):
+ filtered_foliage["gew100"].describe()
+ return
+
+
+@app.cell
+def _(filtered_foliage):
+ filtered_foliage["gew100"].isna().sum()
+ return
+
+
+@app.cell
+def _(filtered_foliage):
+ import plotly.graph_objects as go
+
+ plot_data = (
+ filtered_foliage[
+ ["survey_date", "gew100", "sample_id"]
+ ]
+ .dropna(subset=["survey_date", "gew100"])
+ .sort_values("survey_date")
+ )
+
+ fig = go.Figure()
+
+ fig.add_trace(
+ go.Scatter(
+ x=plot_data["survey_date"],
+ y=plot_data["gew100"],
+ mode="markers",
+ name="Individual measurement",
+ marker=dict(size=7),
+ customdata=plot_data["sample_id"],
+ hovertemplate=(
+ "Date: %{x|%d %B %Y}"
+ "
gew100: %{y:.2f} g"
+ "
Sample: %{customdata}"
+ ""
+ ),
+ )
+ )
+
+ fig.update_layout(
+ title="Foliage dry weight over time",
+ xaxis_title="Survey date",
+ yaxis_title="gew100 (g)",
+ hovermode="closest",
+ )
+
+ fig
+ return (go,)
+
+
+@app.cell
+def _(foliage):
+ samples_per_group = (
+ foliage
+ .groupby(
+ [
+ "survey_date",
+ "species",
+ "leaf_type",
+ "leaf_age_class",
+ ],
+ dropna=False,
+ )
+ .agg(
+ n_samples=("sample_id", "nunique"),
+ mean_gew100=("gew100", "mean"),
+ median_gew100=("gew100", "median"),
+ std_gew100=("gew100", "std"),
+ )
+ .reset_index()
+ .sort_values("survey_date")
+ )
+
+ samples_per_group
+ return
+
+
+@app.cell
+def _(go, samples_per_date):
+ fig_1 = go.Figure()
+
+ fig_1.add_trace(
+ go.Bar(
+ x=samples_per_date["survey_date"],
+ y=samples_per_date["n_samples"],
+ hovertemplate=(
+ "Date: %{x|%d %B %Y}"
+ "
Samples: %{y}"
+ ""
+ ),
+ )
+ )
+
+ fig_1.update_layout(
+ title="Number of foliage samples per survey date",
+ xaxis_title="Survey date",
+ yaxis_title="Number of individual samples",
+ )
+
+ fig_1
+ return
+
+
+if __name__ == "__main__":
+ app.run()
diff --git a/marimo_eda/LAI_Licor.py b/marimo_eda/LAI_Licor.py
new file mode 100644
index 0000000..ae34d1c
--- /dev/null
+++ b/marimo_eda/LAI_Licor.py
@@ -0,0 +1,437 @@
+import marimo
+
+__generated_with = "0.24.0"
+app = marimo.App(width="medium")
+
+
+@app.cell
+def _():
+ import marimo as mo
+
+ return (mo,)
+
+
+@app.cell
+def _(mo):
+ sites = [1,2,3]
+
+ mo.ui.dropdown(sites)
+ return
+
+
+@app.cell
+def _():
+ import pandas as pd
+
+ df = pd.read_excel("legend_deposition_2026-07-28.xlsx")
+ df2 = pd.read_csv("monthly_dep_lwf_2026-07-28.csv")
+ return (pd,)
+
+
+@app.cell
+def _():
+ from pathlib import Path
+
+ data_dir = Path("data")
+
+ files = sorted(data_dir.iterdir())
+
+ files
+ return Path, files
+
+
+@app.cell
+def _(files):
+ for file in files:
+ print(f"{file.name:60} {file.suffix}")
+ return
+
+
+@app.cell
+def _(Path, pd):
+ lai_path = Path("LAI_Licor_3_rings_all_years.xlsx")
+
+ raw_lai = pd.read_excel(
+ lai_path,
+ header=None,
+ )
+
+ raw_lai.head()
+ return (raw_lai,)
+
+
+@app.cell
+def _(pd, raw_lai):
+ lai = raw_lai.iloc[3:].copy()
+
+ lai.columns = [
+ "plot",
+ "subplot",
+ "date",
+ "plot_type",
+ "lai_miller",
+ "se_miller",
+ "lai_norman_campbell",
+ "se_norman_campbell",
+ "angle_miller",
+ "se_angle_miller",
+ "angle_norman_campbell",
+ "se_angle_norman_campbell",
+ "number_of_points",
+ "season",
+ ]
+
+ lai = lai.reset_index(drop=True)
+
+ # Strip whitespace from text fields
+ for column in ["plot", "subplot", "plot_type", "season"]:
+ lai[column] = lai[column].astype(str).str.strip()
+
+ # Convert "." to missing values
+ lai = lai.replace(".", pd.NA)
+
+ # Parse dates
+ lai["date"] = pd.to_datetime(lai["date"], errors="coerce")
+
+ # Numeric variables
+ numeric_columns = [
+ "lai_miller",
+ "se_miller",
+ "lai_norman_campbell",
+ "se_norman_campbell",
+ "angle_miller",
+ "se_angle_miller",
+ "angle_norman_campbell",
+ "se_angle_norman_campbell",
+ "number_of_points",
+ ]
+
+ for column in numeric_columns:
+ lai[column] = pd.to_numeric(lai[column], errors="coerce")
+
+ lai.head()
+ return (lai,)
+
+
+@app.cell
+def _(lai, mo):
+ plot_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ lai["plot"].dropna().unique().tolist()
+ ),
+ value="All",
+ label="Plot",
+ )
+
+ subplot_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ lai["subplot"].dropna().unique().tolist()
+ ),
+ value="All",
+ label="Subplot",
+ )
+
+ plot_type_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ lai["plot_type"].dropna().unique().tolist()
+ ),
+ value="All",
+ label="Plot type",
+ )
+
+ season_selector = mo.ui.dropdown(
+ options=["All"] + sorted(
+ lai["season"].dropna().unique().tolist()
+ ),
+ value="All",
+ label="Season",
+ )
+
+ mo.hstack(
+ [
+ plot_selector,
+ subplot_selector,
+ plot_type_selector,
+ season_selector,
+ ]
+ )
+ return plot_selector, plot_type_selector, season_selector, subplot_selector
+
+
+@app.cell
+def _(
+ lai,
+ plot_selector,
+ plot_type_selector,
+ season_selector,
+ subplot_selector,
+):
+ filtered_lai = lai.copy()
+
+ if plot_selector.value != "All":
+ filtered_lai = filtered_lai[
+ filtered_lai["plot"] == plot_selector.value
+ ]
+
+ if subplot_selector.value != "All":
+ filtered_lai = filtered_lai[
+ filtered_lai["subplot"] == subplot_selector.value
+ ]
+
+ if plot_type_selector.value != "All":
+ filtered_lai = filtered_lai[
+ filtered_lai["plot_type"] == plot_type_selector.value
+ ]
+
+ if season_selector.value != "All":
+ filtered_lai = filtered_lai[
+ filtered_lai["season"] == season_selector.value
+ ]
+
+ filtered_lai
+ return (filtered_lai,)
+
+
+@app.cell
+def _(mo):
+ measurement_options = {
+ "LAI — Miller": "lai_miller",
+ "LAI — Norman & Campbell": "lai_norman_campbell",
+ "Mean angle — Miller": "angle_miller",
+ "Mean angle — Norman & Campbell": "angle_norman_campbell",
+ }
+
+ measurement_selector = mo.ui.dropdown(
+ options=list(measurement_options.keys()),
+ value="LAI — Miller",
+ label="Measurement",
+ )
+
+ measurement_selector
+ return measurement_options, measurement_selector
+
+
+@app.cell
+def _(measurement_options, measurement_selector):
+ selected_measurement = measurement_options[
+ measurement_selector.value
+ ]
+
+ selected_measurement
+ return (selected_measurement,)
+
+
+@app.cell
+def _():
+ import plotly.graph_objects as go
+ import numpy as np
+
+ return (go,)
+
+
+@app.cell
+def _(filtered_lai, go, measurement_selector, selected_measurement):
+ plot_data = filtered_lai[
+ ["date", selected_measurement]
+ ].copy()
+
+ plot_data = (
+ plot_data
+ .sort_values("date")
+ .reset_index(drop=True)
+ )
+
+ # ============================================================
+ # SETTINGS
+ # ============================================================
+
+ # Gaps longer than this are shown as dashed lines
+ max_gap_days = 365 * 2
+
+
+ # ============================================================
+ # FIGURE
+ # ============================================================
+
+ fig = go.Figure()
+
+
+ # ============================================================
+ # 1. MEASURED OBSERVATIONS
+ # ============================================================
+
+ measured = (
+ plot_data
+ .dropna(subset=[selected_measurement])
+ .sort_values("date")
+ .reset_index(drop=True)
+ )
+
+ fig.add_trace(
+ go.Scatter(
+ x=measured["date"],
+ y=measured[selected_measurement],
+ mode="markers",
+ name="Measured",
+ marker=dict(size=8),
+
+ # Full date shown when hovering
+ hovertemplate=(
+ "Date: %{x|%d %B %Y}"
+ "
"
+ f"{measurement_selector.value}: "
+ "%{y:.2f}"
+ ""
+ ),
+
+ showlegend=True,
+ )
+ )
+
+
+ # ============================================================
+ # 2. CONNECT MEASUREMENTS
+ # ============================================================
+
+ for i in range(len(measured) - 1):
+
+ current = measured.iloc[i]
+ next_row = measured.iloc[i + 1]
+
+ gap_days = (
+ next_row["date"] - current["date"]
+ ).days
+
+ # --------------------------------------------------------
+ # Normal gap → solid line
+ # --------------------------------------------------------
+
+ if gap_days <= max_gap_days:
+
+ fig.add_trace(
+ go.Scatter(
+ x=[
+ current["date"],
+ next_row["date"],
+ ],
+ y=[
+ current[selected_measurement],
+ next_row[selected_measurement],
+ ],
+ mode="lines",
+ line=dict(width=2),
+ showlegend=False,
+ hoverinfo="skip",
+ )
+ )
+
+ # --------------------------------------------------------
+ # Long gap → dashed line
+ # --------------------------------------------------------
+
+ else:
+
+ fig.add_trace(
+ go.Scatter(
+ x=[
+ current["date"],
+ next_row["date"],
+ ],
+ y=[
+ current[selected_measurement],
+ next_row[selected_measurement],
+ ],
+ mode="lines",
+ line=dict(
+ width=2,
+ dash="dash",
+ ),
+ name="Long data gap",
+ showlegend=(i == 0),
+ hoverinfo="skip",
+ )
+ )
+
+
+ # ============================================================
+ # 3. HIGHLIGHT NaN DATES
+ # ============================================================
+
+ missing_dates = (
+ plot_data.loc[
+ plot_data[selected_measurement].isna(),
+ "date"
+ ]
+ .dropna()
+ .drop_duplicates()
+ .sort_values()
+ )
+
+ for date in missing_dates:
+
+ fig.add_vline(
+ x=date,
+ line_width=2,
+ line_dash="dot",
+ line_color="red",
+ showlegend=False,
+ )
+
+
+ # ============================================================
+ # 4. LEGEND ENTRY FOR MISSING OBSERVATIONS
+ # ============================================================
+
+ if len(missing_dates) > 0:
+
+ fig.add_trace(
+ go.Scatter(
+ x=[None],
+ y=[None],
+ mode="lines",
+ line=dict(
+ width=2,
+ dash="dot",
+ color="red",
+ ),
+ name="Missing observation",
+ showlegend=True,
+ hoverinfo="skip",
+ )
+ )
+
+
+ # ============================================================
+ # 5. LAYOUT
+ # ============================================================
+
+ fig.update_layout(
+ title=f"{measurement_selector.value} over time",
+ xaxis_title="Date",
+ yaxis_title=measurement_selector.value,
+
+ # Important: inspect individual observations
+ hovermode="closest",
+ )
+
+ fig
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+@app.cell
+def _():
+ return
+
+
+if __name__ == "__main__":
+ app.run()