# Advanced use cases

Have a more complex analysis in mind? We've put together a collection of useful code snippets to help you get started.

{% tabs %}
{% tab title="Heatmap" %}

<figure><img src="/files/66JxTJTVKwlSNbIv2Y69" alt=""><figcaption></figcaption></figure>

```python
#! /usr/bin/env python3
# /// script
# dependencies = [
#    "pandas",
#    "matplotlib",
#    "marpledata",
#    "tqdm",
# ]
# ///

"""
Parameters
"""
MDB_TOKEN = "<MDB_TOKEN>"
MDB_HOST = "db.marpledata.com"
STREAM = "Datastream" # Marple DB Datastream name

X_NAME = "signal name" 
X_UNIT = "unit"
Y_NAME = "signal name"
Y_UNIT = "unit"

# Optional figure export (set to None to disable saving)
SAVE_PATH = "heatmap-4k.png"
SAVE_WIDTH_PX = 3840
SAVE_HEIGHT_PX = 2160
SAVE_DPI = 300


from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd
from marple import DB
from marple.db import DataStream
from matplotlib.colors import LogNorm
from tqdm.auto import tqdm


def heatmap_via_python_sdk(stream: DataStream, x: str, y: str, x_unit: str = None, y_unit: str = None):
    datasets = stream.get_datasets()
    n_datasets = len(datasets)
    print(f"[1/4] Stream: {stream.name}")
    print(f"      Signals: {x}, {y}")
    print(f"      Datasets: {n_datasets}")

    print("[2/4] Downloading and resampling data (1s)...")
    dataset_frames = []
    for _, dataset_df in tqdm(
        datasets.get_data([x, y], dtype="numeric", resample_rule="1s"),
        total=n_datasets,
        desc="Datasets",
    ):
        dataset_frames.append(dataset_df)

    if not dataset_frames:
        print("No data returned for the selected stream/signals.")
        return

    cache_files = list(Path(db.client.cache_folder).glob("**/*.parquet"))
    cache_size_mb = sum(file.stat().st_size for file in cache_files) / 1024 / 1024
    print(f"[3/4] Cache size: {cache_size_mb:.2f} MB ({len(cache_files)} files)")

    df = pd.concat(dataset_frames, ignore_index=True).dropna(subset=[x, y])
    if df.empty:
        print("Data downloaded, but no valid rows remain after dropping missing values.")
        return

    print(f"[4/4] Plotting heatmap from {len(df):,} samples...")
    fig, ax = plt.subplots()
    cmap = plt.get_cmap("hot").copy()
    cmap.set_bad("black")
    h = ax.hist2d(df[x], df[y], bins=200, cmap=cmap, cmin=1, norm=LogNorm())
    fig.colorbar(h[3], ax=ax, label="Time [s]")
    ax.set_xlabel(f"{x} [{x_unit}]" if x_unit else x)
    ax.set_ylabel(f"{y} [{y_unit}]" if y_unit else y)
    ax.set_title(f"{x} vs {y} Heatmap (Stream: {stream.name}, {len(dataset_frames)} datasets)")
    fig.tight_layout()
    if SAVE_PATH:
        fig.set_size_inches(SAVE_WIDTH_PX / SAVE_DPI, SAVE_HEIGHT_PX / SAVE_DPI)
        fig.savefig(SAVE_PATH, dpi=SAVE_DPI)
        print(f"Saved {SAVE_PATH} ({SAVE_WIDTH_PX}x{SAVE_HEIGHT_PX}px @ {SAVE_DPI} dpi)")
    plt.show()


if __name__ == "__main__":
    db = DB(api_token=MDB_TOKEN, api_url=f"https://{MDB_HOST}/api/v1")
    stream = db.get_stream(STREAM)
    heatmap_via_python_sdk(stream, X_NAME, Y_NAME, X_UNIT, Y_UNIT)
```

{% endtab %}

{% tab title="3D Trajectory" %}

<figure><img src="/files/hMHtvnFnRPlvuj7EAQiE" alt=""><figcaption></figcaption></figure>

```python
#! /usr/bin/env python3
# /// script
# dependencies = [
#    "pandas",
#    "matplotlib",
#    "marpledata",
# ]
# ///

"""
Parameters
"""
API_TOKEN = "<MDB_TOKEN>" # your Marple DB token here
STREAM = "Datastream" # your Marple DB datastream name here
DATASET = "file1.h5" # your file here

X_NAME = "Longitude" # your longitude signal here
Y_NAME = "Latitude" # your latitude signal here 
Z_NAME = "Altitude" # your altitude signal here 
SEG_NAME = "segment" # optional: your segments here (Stall 1, Stall 2, ...)

import matplotlib.pyplot as plt
import pandas as pd
from marple import DB # pip install marpledata

def dataset_label(ds):
    return getattr(ds, "path", None) or getattr(ds, "name", None) or f"id={getattr(ds, 'id', 'unknown')}"

db = DB(API_TOKEN)
stream = db.get_stream(STREAM)
datasets = stream.get_datasets().wait_for_import().where_imported()

numeric_df = None
segment_df = None

for ds, df in datasets.get_data([X_NAME, Y_NAME, Z_NAME], resample_rule="1s"):
    if dataset_label(ds) == DATASET:
        numeric_df = df.copy()
        break

for ds, df in datasets.get_data([SEG_NAME]):
    if dataset_label(ds) == DATASET:
        segment_df = df.copy()
        break

if numeric_df is None:
    raise ValueError("Dataset not found")

numeric_df = numeric_df[[X_NAME, Y_NAME, Z_NAME]].copy()
numeric_df[X_NAME] = pd.to_numeric(numeric_df[X_NAME], errors="coerce")
numeric_df[Y_NAME] = pd.to_numeric(numeric_df[Y_NAME], errors="coerce")
numeric_df[Z_NAME] = pd.to_numeric(numeric_df[Z_NAME], errors="coerce")
numeric_df = numeric_df.dropna(subset=[X_NAME, Y_NAME, Z_NAME])

if numeric_df.empty:
    raise ValueError("No valid numeric rows were returned")

if segment_df is not None and SEG_NAME in segment_df.columns:
    num = (
        numeric_df.sort_index()
        .reset_index()
        .rename(columns={numeric_df.index.name or "index": "time"})
    )
    seg = (
        segment_df[[SEG_NAME]]
        .copy()
        .sort_index()
        .reset_index()
        .rename(columns={segment_df.index.name or "index": "time"})
    )
    seg[SEG_NAME] = seg[SEG_NAME].astype(str)

    df = pd.merge_asof(
        num.sort_values("time"),
        seg.sort_values("time"),
        on="time",
        direction="backward"
    ).set_index("time")

    df[SEG_NAME] = df[SEG_NAME].fillna("Unknown")
else:
    df = numeric_df.copy()
    df[SEG_NAME] = "Unknown"

df = df[[X_NAME, Y_NAME, Z_NAME, SEG_NAME]].copy()
df = df.dropna(subset=[X_NAME, Y_NAME, Z_NAME])

if df.empty:
    raise ValueError("No valid rows remain after dropping missing values")

df = df.reset_index(drop=True)
df["segment_block"] = (df[SEG_NAME] != df[SEG_NAME].shift(1)).cumsum()

colors = [
    "blue", "red", "green", "orange", "purple", "cyan",
    "magenta", "gold", "brown", "black", "pink", "gray"
]

unique_segments = list(df[SEG_NAME].unique())
color_map = {seg: colors[i % len(colors)] for i, seg in enumerate(unique_segments)}

fig = plt.figure(figsize=(11, 8))
ax = fig.add_subplot(projection="3d")

used_labels = set()

for _, chunk in df.groupby("segment_block"):
    seg = chunk[SEG_NAME].iloc[0]
    color = color_map[seg]
    label = seg if seg not in used_labels else None

    ax.plot(
        chunk[X_NAME],
        chunk[Y_NAME],
        chunk[Z_NAME],
        linewidth=2.0,
        color=color,
        label=label
    )

    if label is not None:
        used_labels.add(seg)

ax.scatter(
    df[X_NAME].iloc[0], df[Y_NAME].iloc[0], df[Z_NAME].iloc[0],
    s=50, color="black", label="Start"
)
ax.scatter(
    df[X_NAME].iloc[-1], df[Y_NAME].iloc[-1], df[Z_NAME].iloc[-1],
    s=50, color="lime", label="End"
)

ax.set_xlabel(X_NAME)
ax.set_ylabel(Y_NAME)
ax.set_zlabel(Z_NAME)
ax.set_title("3D Trajectory by Segment")

ax.legend(loc="upper left", bbox_to_anchor=(1.02, 1.0))
plt.tight_layout()
plt.show()
```

{% endtab %}

{% tab title="Flight Envelope" %}

<figure><img src="/files/YpopZbO6uTVboGmdUs1e" alt=""><figcaption></figcaption></figure>

```python
#! /usr/bin/env python3
# /// script
# dependencies = [
#    "pandas",
#    "matplotlib",
#    "marpledata",
#    "numpy",
#    "math",
# ]
# ///

"""
Parameters
"""
API_TOKEN = "<MDB_TOKEN>" # your Marple DB token here
STREAM = "Datastream" # your Marple DB datastream name here
DATASET = "file1.h5" # your file here

X_SIG = "TrueAirspeedTAS" # your longitude signal here
Y_SIG = "AltitudeMSL" # your latitude signal here 
SEG_SIG = "segment" # optional: your segments here (Stall 1, Stall 2, ...)

import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import math
from marple import DB

def dataset_label(ds):
    return getattr(ds, "path", None) or getattr(ds, "name", None) or f"id={getattr(ds, 'id', 'unknown')}"

db = DB(API_TOKEN)
stream = db.get_stream(STREAM)
datasets = stream.get_datasets().wait_for_import().where_imported()

numeric_df = None
segment_df = None

for ds, df in datasets.get_data([X_SIG, Y_SIG], resample_rule="1s"):
    if dataset_label(ds) == DATASET:
        numeric_df = df.copy()
        break

for ds, df in datasets.get_data([SEG_SIG]):
    if dataset_label(ds) == DATASET:
        segment_df = df.copy()
        break

if numeric_df is None:
    raise ValueError("Dataset not found")

numeric_df = numeric_df[[X_SIG, Y_SIG]].copy()
numeric_df[X_SIG] = pd.to_numeric(numeric_df[X_SIG], errors="coerce")
numeric_df[Y_SIG] = pd.to_numeric(numeric_df[Y_SIG], errors="coerce")
numeric_df = numeric_df.dropna(subset=[X_SIG, Y_SIG])

if numeric_df.empty:
    raise ValueError("No valid numeric rows were returned")

if segment_df is not None and SEG_SIG in segment_df.columns:
    num = (
        numeric_df.sort_index()
        .reset_index()
        .rename(columns={numeric_df.index.name or "index": "time"})
    )
    seg = (
        segment_df[[SEG_SIG]]
        .copy()
        .sort_index()
        .reset_index()
        .rename(columns={segment_df.index.name or "index": "time"})
    )
    seg[SEG_SIG] = seg[SEG_SIG].astype(str)

    df = pd.merge_asof(
        num.sort_values("time"),
        seg.sort_values("time"),
        on="time",
        direction="backward"
    ).set_index("time")

    df[SEG_SIG] = df[SEG_SIG].fillna("Unknown")
else:
    df = numeric_df.copy()
    df[SEG_SIG] = "Unknown"

df[X_SIG] = pd.to_numeric(df[X_SIG], errors="coerce")
df[Y_SIG] = pd.to_numeric(df[Y_SIG], errors="coerce")
df[SEG_SIG] = df[SEG_SIG].fillna("Unknown").astype(str)
df = df.dropna(subset=[X_SIG, Y_SIG])

if df.empty:
    raise ValueError("No valid rows remain after dropping missing values")

df["Altitude_km"] = df[Y_SIG] * 0.0003048

stall_df = df[df[SEG_SIG].str.contains("stall", case=False, na=False)].copy()
if stall_df.empty:
    raise ValueError("No stall segments found in this dataset")

# Limit values for scatter plot
ALT_MAX = 15.0
V_STALL_0 = 75.0
V_STALL_TOP = 185.0
V_EAS_0 = 160.0
V_EAS_TOP = 255.0
V_MACH_TOP = 248.0
V_MACH_LOW = 255.0
MACH_KNEE_ALT = 9.0
CEILING_ALT = 15.0

alt = np.linspace(0, ALT_MAX, 300)

v_stall = V_STALL_0 + (V_STALL_TOP - V_STALL_0) * (alt / ALT_MAX) ** 1.8

v_eas = V_EAS_0 + (V_EAS_TOP - V_EAS_0) * (alt / MACH_KNEE_ALT)
v_eas = np.where(alt <= MACH_KNEE_ALT, v_eas, np.nan)

v_mach = V_MACH_LOW - (V_MACH_LOW - V_MACH_TOP) * ((alt - MACH_KNEE_ALT) / (CEILING_ALT - MACH_KNEE_ALT))
v_mach = np.where(alt >= MACH_KNEE_ALT, v_mach, np.nan)

stall_segments = list(stall_df[SEG_SIG].dropna().unique())
n = len(stall_segments)

ncols = 2
nrows = math.ceil(n / ncols)

fig, axes = plt.subplots(nrows, ncols, figsize=(12, 5 * nrows), squeeze=False)
axes = axes.flatten()

for ax, seg_name in zip(axes, stall_segments):
    chunk = stall_df[stall_df[SEG_SIG] == seg_name].copy()

    ax.set_facecolor("#f3f3f3")

    mask_fill = alt <= MACH_KNEE_ALT
    ax.fill_betweenx(
        alt[mask_fill],
        v_stall[mask_fill],
        v_eas[mask_fill],
        color="#d8f2d8",
        alpha=0.8,
        zorder=1
    )

    mask_fill2 = alt >= MACH_KNEE_ALT
    ax.fill_betweenx(
        alt[mask_fill2],
        v_stall[mask_fill2],
        v_mach[mask_fill2],
        color="#d8f2d8",
        alpha=0.8,
        zorder=1
    )

    ax.plot([V_STALL_TOP, V_MACH_TOP], [CEILING_ALT, CEILING_ALT], color="black", linewidth=1.7, zorder=3)
    ax.plot(v_stall, alt, color="#6b9f8d", linewidth=2, zorder=3)
    ax.plot(v_eas, alt, color="#8b3f4d", linewidth=2, zorder=3)
    ax.plot(v_mach, alt, color="#7da2b8", linewidth=2, zorder=3)

    ax.scatter(
        chunk[X_SIG],
        chunk["Altitude_km"],
        s=20,
        alpha=0.8,
        color="red",
        edgecolors="none",
        zorder=4
    )

    ax.text(100, 10, "stall", color="#6b9f8d", fontsize=12)
    ax.text(205, 4.0, "constant EAS", color="#8b3f4d", fontsize=11)
    ax.text(250, 11, "constant\nMach", color="#7da2b8", fontsize=11, ha="left")
    ax.text((V_STALL_TOP + V_MACH_TOP) / 2, 15.3, "ceiling", fontsize=12, ha="center")

    ax.set_xlabel("True Airspeed TAS")
    ax.set_ylabel("Altitude (km)")
    ax.set_title(seg_name)
    ax.grid(True, alpha=0.3)

for ax in axes[n:]:
    ax.axis("off")

fig.suptitle("Flight Envelope by Stall Segment", fontsize=16)
plt.tight_layout()
plt.show()
```

{% endtab %}

{% tab title="Primary Flight Display" %}

<figure><img src="/files/mE7oi0QpyNwGKW6O9QVn" alt=""><figcaption></figcaption></figure>

```python
#! /usr/bin/env python3
# /// script
# dependencies = [
#    "pandas",
#    "matplotlib",
#    "marpledata",
#    "numpy",
# ]
# ///

"""
Parameters
"""
API_TOKEN = "<MDB_TOKEN>" # your Marple DB token here
STREAM = "Datastream" # your Marple DB datastream name here
DATASET = "file1.h5" # your file here

PITCH_SIG = "PitchAngle"
ROLL_SIG = "RollAngle"
SPD_SIG = "TrueAirspeedTAS"
ALT_SIG = "AltitudeMSL"
SEG_SIG = "segment"
 
OUT_GIF = "pfd_stall.gif"
N_FRAMES = 450

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from matplotlib.patches import Polygon, Rectangle
from matplotlib.animation import FuncAnimation, PillowWriter
from matplotlib.transforms import Affine2D
from marple import DB

def dataset_label(ds):
    return getattr(ds, "path", None) or getattr(ds, "name", None) or f"id={getattr(ds, 'id', 'unknown')}"

db = DB(API_TOKEN)
stream = db.get_stream(STREAM)
datasets = stream.get_datasets().wait_for_import().where_imported()

numeric_df = None
segment_df = None

for ds, df in datasets.get_data([PITCH_SIG, ROLL_SIG, SPD_SIG, ALT_SIG], resample_rule="1s"):
    if dataset_label(ds) == DATASET:
        numeric_df = df.copy()
        break

for ds, df in datasets.get_data([SEG_SIG]):
    if dataset_label(ds) == DATASET:
        segment_df = df.copy()
        break

if numeric_df is None:
    raise ValueError("Dataset not found")

numeric_df = numeric_df[[PITCH_SIG, ROLL_SIG, SPD_SIG, ALT_SIG]].copy()
for col in [PITCH_SIG, ROLL_SIG, SPD_SIG, ALT_SIG]:
    numeric_df[col] = pd.to_numeric(numeric_df[col], errors="coerce")

numeric_df = numeric_df.dropna(subset=[PITCH_SIG, ROLL_SIG, SPD_SIG, ALT_SIG])

if numeric_df.empty:
    raise ValueError("No valid numeric rows were returned")

if segment_df is not None and SEG_SIG in segment_df.columns:
    num = (
        numeric_df.sort_index()
        .reset_index()
        .rename(columns={numeric_df.index.name or "index": "time"})
    )
    seg = (
        segment_df[[SEG_SIG]]
        .copy()
        .sort_index()
        .reset_index()
        .rename(columns={segment_df.index.name or "index": "time"})
    )
    seg[SEG_SIG] = seg[SEG_SIG].astype(str)

    df = pd.merge_asof(
        num.sort_values("time"),
        seg.sort_values("time"),
        on="time",
        direction="backward"
    ).set_index("time")

    df[SEG_SIG] = df[SEG_SIG].fillna("Unknown")
else:
    df = numeric_df.copy()
    df[SEG_SIG] = "Unknown"

stall_df = df[df[SEG_SIG].astype(str).str.contains("stall", case=False, na=False)].copy()

if stall_df.empty:
    raise ValueError("No stall segments found in this dataset")

pitch = stall_df[PITCH_SIG].to_numpy()
roll = stall_df[ROLL_SIG].to_numpy()
speed = stall_df[SPD_SIG].to_numpy()
altitude = stall_df[ALT_SIG].to_numpy()
segment = stall_df[SEG_SIG].astype(str).to_numpy()

if len(stall_df) > N_FRAMES:
    idx = np.linspace(0, len(stall_df) - 1, N_FRAMES).astype(int)
    pitch = pitch[idx]
    roll = roll[idx]
    speed = speed[idx]
    altitude = altitude[idx]
    segment = segment[idx]

fig, ax = plt.subplots(figsize=(9, 7))
fig.patch.set_facecolor("black")
ax.set_facecolor("black")
ax.set_xlim(-1.55, 1.55)
ax.set_ylim(-1.15, 1.15)
ax.set_aspect("equal")
ax.axis("off")

att_x0, att_y0 = -0.78, -0.60
att_w, att_h = 1.56, 1.20
att_clip = Rectangle((att_x0, att_y0), att_w, att_h, transform=ax.transData)

sky = Polygon([[-4, 0], [4, 0], [4, 4], [-4, 4]], closed=True, color="#4da6ff")
ground = Polygon([[-4, -4], [4, -4], [4, 0], [-4, 0]], closed=True, color="#9c5a1a")
horizon_line, = ax.plot([-4, 4], [0, 0], color="white", linewidth=2)

ax.add_patch(sky)
ax.add_patch(ground)

for artist in [sky, ground, horizon_line]:
    artist.set_clip_path(att_clip)

pitch_lines = []
pitch_labels = []

for deg in range(-30, 31, 5):
    if deg == 0:
        continue

    half = 0.30 if deg % 10 == 0 else 0.18
    line, = ax.plot([-half, half], [deg / 30.0, deg / 30.0], color="white", linewidth=1.2)
    line.set_clip_path(att_clip)
    pitch_lines.append((deg, line))

    if deg % 10 == 0:
        t1 = ax.text(
            -half - 0.06, deg / 30.0, f"{abs(deg)}",
            color="white", fontsize=9, ha="right", va="center",
            fontweight="bold", zorder=12
        )
        t2 = ax.text(
            half + 0.06, deg / 30.0, f"{abs(deg)}",
            color="white", fontsize=9, ha="left", va="center",
            fontweight="bold", zorder=12
        )
        pitch_labels.append((deg, t1, t2))

ax.add_patch(Rectangle((att_x0, att_y0), att_w, att_h, fill=False, edgecolor="white", linewidth=2, zorder=20))
ax.add_patch(Rectangle((-2, 0.60), 4, 1.0, facecolor="black", edgecolor="none", zorder=30))
ax.add_patch(Rectangle((-2, -1.20), 4, 0.60, facecolor="black", edgecolor="none", zorder=30))
ax.add_patch(Rectangle((att_x0, att_y0), att_w, att_h, fill=False, edgecolor="white", linewidth=2, zorder=31))

ax.plot([-0.30, -0.07], [0, 0], color="yellow", linewidth=3, zorder=32)
ax.plot([0.07, 0.30], [0, 0], color="yellow", linewidth=3, zorder=32)
ax.plot([0, 0], [-0.06, 0.06], color="yellow", linewidth=3, zorder=32)

bank_pointer = Polygon([[0, 0.72], [-0.03, 0.66], [0.03, 0.66]], closed=True, color="white", zorder=32)
ax.add_patch(bank_pointer)

speed_x0 = -1.11
alt_x0 = 0.83
tape_w = 0.28
tape_h = 1.20

speed_box = Rectangle((speed_x0, -0.60), tape_w, tape_h, facecolor="#101010", edgecolor="white", linewidth=1.5, zorder=10)
alt_box = Rectangle((alt_x0, -0.60), tape_w, tape_h, facecolor="#101010", edgecolor="white", linewidth=1.5, zorder=10)
ax.add_patch(speed_box)
ax.add_patch(alt_box)

speed_window = Rectangle((speed_x0 - 0.02, -0.10), tape_w + 0.04, 0.20, facecolor="white", edgecolor="white", linewidth=1.5, zorder=40)
alt_window = Rectangle((alt_x0 - 0.02, -0.10), tape_w + 0.04, 0.20, facecolor="white", edgecolor="white", linewidth=1.5, zorder=40)
ax.add_patch(speed_window)
ax.add_patch(alt_window)

speed_value = ax.text(speed_x0 + tape_w / 2, 0.0, "", color="black", fontsize=14, ha="center", va="center", fontweight="bold", zorder=41)
alt_value = ax.text(alt_x0 + tape_w / 2, 0.0, "", color="black", fontsize=14, ha="center", va="center", fontweight="bold", zorder=41)

speed_ticks = []
alt_ticks = []

segment_box = Rectangle((-0.70, -1.05), 1.40, 0.20, facecolor="#101010", edgecolor="white", linewidth=1.2, zorder=50)
ax.add_patch(segment_box)

segment_title = ax.text(0, -0.93, "Current Segment", color="white", fontsize=10, ha="center", va="center", fontweight="bold", zorder=51)
segment_text = ax.text(0, -1.00, "", color="white", fontsize=11, ha="center", va="center", zorder=51)

def clear_tick_artists(tick_list):
    for artist in tick_list:
        artist.remove()
    tick_list.clear()

def draw_left_tape(center_value, tick_store, step):
    clear_tick_artists(tick_store)
    for d in range(-4, 5):
        val = center_value + d * step
        y = d * 0.14
        line = ax.plot([speed_x0 + 0.03, speed_x0 + 0.09], [y, y], color="white", linewidth=1, zorder=12)[0]
        txt = ax.text(speed_x0 + 0.11, y, f"{val:.0f}", color="white", fontsize=9, ha="left", va="center", zorder=12)
        tick_store.extend([line, txt])

def draw_right_tape(center_value, tick_store, step):
    clear_tick_artists(tick_store)
    for d in range(-4, 5):
        val = center_value + d * step
        y = d * 0.14
        line = ax.plot([alt_x0 + 0.03, alt_x0 + 0.09], [y, y], color="white", linewidth=1, zorder=12)[0]
        txt = ax.text(alt_x0 + 0.11, y, f"{val:.0f}", color="white", fontsize=9, ha="left", va="center", zorder=12)
        tick_store.extend([line, txt])

def update(i):
    p = float(pitch[i])
    r = float(roll[i])
    spd = float(speed[i])
    alt = float(altitude[i])
    seg = str(segment[i])

    pitch_offset = p / 25.0
    trans = Affine2D().translate(0, -pitch_offset).rotate_deg_around(0, 0, -r) + ax.transData

    sky.set_transform(trans)
    ground.set_transform(trans)
    horizon_line.set_transform(trans)

    for deg, line in pitch_lines:
        half = 0.30 if deg % 10 == 0 else 0.18
        y = deg / 30.0
        line.set_data([-half, half], [y, y])
        line.set_transform(trans)

    for deg, t1, t2 in pitch_labels:
        half = 0.30
        y = deg / 30.0
        t1.set_position((-half - 0.06, y))
        t2.set_position((half + 0.06, y))
        t1.set_transform(trans)
        t2.set_transform(trans)

    speed_value.set_text(f"{spd:.0f}")
    alt_value.set_text(f"{alt:.0f}")
    segment_text.set_text(seg)

    draw_left_tape(spd, speed_ticks, 10)
    draw_right_tape(alt, alt_ticks, 100)

    return [sky, ground, horizon_line, speed_value, alt_value, segment_title, segment_text] + speed_ticks + alt_ticks

anim = FuncAnimation(fig, update, frames=len(pitch), interval=200, blit=False)
anim.save(OUT_GIF, writer=PillowWriter(fps=5))
plt.close(fig)

print(f"Saved {OUT_GIF}")
```

{% endtab %}
{% endtabs %}

## Discover Python SDK data pulling in 1 minute

{% embed url="<https://www.youtube.com/watch?index=3&list=PLtVuqpI9QpJAFtUEvuS23Z47XGHrRc_P9&v=XU64Ho8n7pQ>" %}

Want to discover more Marple features in 1 minute? Check out other [1 Minute Marple videos](/docs/other-resources/1-minute-marple-videos.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.marpledata.com/docs/sdk/overview/python-sdk/advanced-use-cases.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
