Skip to content

Linked Panels With Axes

This example builds linked time-series panels with shared axes behavior.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c showcases/panel_linked_axes (build and run), or rerun ./build/examples/c/showcases/panel_linked_axes
Python Available; direct-engine adaptation python3 -m examples.python.gallery.showcases.panel_linked_axes
Browser Live WebGPU route Open live example

Use this example as capability or integration evidence, not as a minimal copy-paste template. Start from the nearest supported, copy-safe example and add this feature after verifying the linked API reference.

What To Look For

The left column combines a synthetic signal trace, an event-raster panel, and residual points, while the right panel summarizes the same time range. The important arrays are the path samples, event segments, residual points, shaded bands, and cursor lines; compare the panels to see that they use different y domains while their x navigation remains linked.

This workflow is useful for scientific dashboards where traces, events, and summary statistics must stay aligned during panzoom interaction.

Source

#!/usr/bin/env python3
"""Linked time-series panels with shared X navigation and retained axes."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


PATH_COUNT = 360
EVENT_COUNT = 96
EVENT_ROWS = 8
POINT_COUNT = 168
PHASE_COUNT = 360
BAND_COUNT = 2
CURSOR_COUNT = BAND_COUNT
TAU = 2.0 * np.pi


def _signal(t):
    return (
        0.85 * np.sin(TAU * (1.08 * t + 0.06))
        + 0.34 * np.sin(TAU * (3.20 * t + 0.18))
        + 0.12 * np.cos(TAU * (6.00 * t + 0.10))
    )


def _lagged_signal(t):
    return 0.76 * np.sin(TAU * (1.08 * t - 0.09)) + 0.30 * np.sin(
        TAU * (2.50 * t + 0.32)
    )


def _signal_data():
    t = np.linspace(0.0, 1.0, PATH_COUNT, dtype=np.float32)
    positions = np.column_stack((12.0 * t, _signal(t), np.zeros(PATH_COUNT))).astype(np.float32)
    colors = np.empty((PATH_COUNT, 4), dtype=np.uint8)
    colors[:, 0] = 72
    colors[:, 1] = np.clip(188.0 + 44.0 * t, 0, 255).astype(np.uint8)
    colors[:, 2] = 242
    colors[:, 3] = 255
    widths = np.full(PATH_COUNT, 3.2, dtype=np.float32)
    return positions, colors, widths


def _event_data():
    starts = np.zeros((EVENT_COUNT, 3), dtype=np.float32)
    ends = np.zeros((EVENT_COUNT, 3), dtype=np.float32)
    colors = np.zeros((EVENT_COUNT, 4), dtype=np.uint8)
    widths = np.zeros(EVENT_COUNT, dtype=np.float32)
    groups = EVENT_COUNT // EVENT_ROWS

    for i in range(EVENT_COUNT):
        row = i % EVENT_ROWS
        group = i // EVENT_ROWS
        base = group / float(groups - 1)
        phase = row / float(EVENT_ROWS - 1)
        x = 12.0 * base + 0.18 * np.sin(TAU * (0.23 * i + 0.17 * phase))
        starts[i] = (np.clip(x, 0.0, 12.0), row - 0.34, 0.0)
        ends[i] = (starts[i, 0], row + 0.34, 0.0)
        colors[i] = (100, 170 + 9 * row, 220, 230)
        widths[i] = 2.2 + 0.8 * (row % 3)
    return starts, ends, colors, widths


def _residual_data():
    t = np.linspace(0.0, 1.0, POINT_COUNT, dtype=np.float32)
    y = 0.55 * (_signal(t) - _lagged_signal(t)) + 0.12 * np.sin(TAU * (9.0 * t + 0.20))
    positions = np.column_stack((12.0 * t, y, np.zeros(POINT_COUNT))).astype(np.float32)

    mag = np.minimum(np.abs(y), 1.0)
    colors = np.empty((POINT_COUNT, 4), dtype=np.uint8)
    colors[:, 0] = (110.0 + 65.0 * mag).astype(np.uint8)
    colors[:, 1] = (170.0 + 52.0 * (1.0 - mag)).astype(np.uint8)
    colors[:, 2] = 216
    colors[:, 3] = 238
    diameters = (4.0 + 5.0 * mag).astype(np.float32)
    return positions, colors, diameters


def _phase_data():
    t = np.linspace(0.0, 1.0, PHASE_COUNT, dtype=np.float32)
    positions = np.column_stack((_signal(t), _lagged_signal(t), np.zeros(PHASE_COUNT))).astype(
        np.float32
    )
    colors = np.empty((PHASE_COUNT, 4), dtype=np.uint8)
    colors[:, 0] = (64.0 + 64.0 * t).astype(np.uint8)
    colors[:, 1] = 214
    colors[:, 2] = 205
    colors[:, 3] = 245
    widths = np.full(PHASE_COUNT, 2.4, dtype=np.float32)
    return positions, colors, widths


def _band_data(ymin: float, ymax: float):
    x0 = [3.10, 8.05]
    x1 = [3.76, 8.72]
    band_colors = [(72, 170, 205, 45), (128, 220, 185, 38)]
    positions = np.zeros((6 * BAND_COUNT, 3), dtype=np.float32)
    colors = np.zeros((6 * BAND_COUNT, 4), dtype=np.uint8)
    for b in range(BAND_COUNT):
        k = 6 * b
        positions[k : k + 6] = [
            [x0[b], ymin, -0.02],
            [x1[b], ymin, -0.02],
            [x1[b], ymax, -0.02],
            [x0[b], ymin, -0.02],
            [x1[b], ymax, -0.02],
            [x0[b], ymax, -0.02],
        ]
        colors[k : k + 6] = band_colors[b]
    return positions, colors


def _cursor_data(ymin: float, ymax: float):
    xs = [3.43, 8.38]
    line_colors = [(100, 220, 245, 185), (150, 240, 205, 175)]
    starts = np.zeros((CURSOR_COUNT, 3), dtype=np.float32)
    ends = np.zeros((CURSOR_COUNT, 3), dtype=np.float32)
    colors = np.zeros((CURSOR_COUNT, 4), dtype=np.uint8)
    widths = np.full(CURSOR_COUNT, 2.2, dtype=np.float32)
    for i, x in enumerate(xs):
        starts[i] = (x, ymin, 0.0)
        ends[i] = (x, ymax, 0.0)
        colors[i] = line_colors[i]
    return starts, ends, colors, widths


def _set_domains(panel, x0: float, x1: float, y0: float, y1: float) -> None:
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, x0, x1) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, y0, y1) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")


def _configure_panel(panel) -> None:
    dvz.dvz_panel_set_background_color(panel, ex.BG)
    border = dvz.dvz_panel_border_desc()
    border.visible = True
    border.color = dvz.DvzColor(ex.BLUE.r, ex.BLUE.g, ex.BLUE.b, 220)
    border.width_px = 2.25
    border.inset_px = 1.125
    if dvz.dvz_panel_set_border(panel, ctypes.byref(border)) != 0:
        raise RuntimeError("dvz_panel_set_border() failed")


def _add_axes(panel, x_label: bytes | None, y_label: bytes) -> None:
    x_axis = dvz.dvz_panel_axis(panel, dvz.DVZ_DIM_X)
    y_axis = dvz.dvz_panel_axis(panel, dvz.DVZ_DIM_Y)
    if not x_axis or not y_axis:
        raise RuntimeError("dvz_panel_axis() failed")

    ticks = dvz.dvz_axis_tick_policy()
    ticks.target_count = 6
    ticks.min_pixel_spacing = 96.0
    ticks.minor_per_interval = 3
    for axis in (x_axis, y_axis):
        if dvz.dvz_axis_set_tick_policy(axis, ctypes.byref(ticks)) != 0:
            raise RuntimeError("dvz_axis_set_tick_policy() failed")

    for axis, vertical in ((x_axis, False), (y_axis, True)):
        style = dvz.dvz_axis_style()
        style.tick_size_px = 11.0
        style.label_size_px = 14.0
        style.tick_gap_px = 7.0
        style.label_gap_px = 14.0 if vertical else 0.0
        style.grid_color[:] = (96, 165, 250, 145)
        style.spine_color[:] = (217, 226, 236, 210)
        style.major_tick_color[:] = (217, 226, 236, 230)
        style.minor_tick_color[:] = (217, 226, 236, 210)
        if dvz.dvz_axis_set_style(axis, ctypes.byref(style)) != 0:
            raise RuntimeError("dvz_axis_set_style() failed")
        if dvz.dvz_axis_set_grid(axis, True) != 0:
            raise RuntimeError("dvz_axis_set_grid() failed")

    if x_label is not None and dvz.dvz_axis_set_label(x_axis, x_label) != 0:
        raise RuntimeError("dvz_axis_set_label(X) failed")
    if dvz.dvz_axis_set_label(y_axis, y_label) != 0:
        raise RuntimeError("dvz_axis_set_label(Y) failed")


def _add_bands(scene, panel, ymin: float, ymax: float) -> None:
    positions, colors = _band_data(ymin, ymax)
    visual = dvz.dvz_primitive(scene, dvz.DVZ_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0)
    if not visual:
        raise RuntimeError("dvz_primitive() failed")
    if dvz.dvz_visual_set_data_many(visual, {"position": positions, "color": colors}) != 0:
        raise RuntimeError("dvz_visual_set_data_many(bands) failed")
    if dvz.dvz_visual_set_alpha_mode(visual, dvz.DVZ_ALPHA_BLENDED) != 0:
        raise RuntimeError("dvz_visual_set_alpha_mode(bands) failed")
    if dvz.dvz_visual_set_depth_test(visual, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(bands) failed")
    ex.add_visual(panel, visual)


def _add_cursor_lines(scene, panel, ymin: float, ymax: float) -> None:
    starts, ends, colors, widths = _cursor_data(ymin, ymax)
    visual = dvz.dvz_segment(scene, 0)
    if not visual:
        raise RuntimeError("dvz_segment() failed")
    if dvz.dvz_visual_set_data_many(
        visual,
        {
            "position_start": starts,
            "position_end": ends,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(cursor) failed")
    if dvz.dvz_segment_set_caps(visual, dvz.DVZ_SEGMENT_CAP_SQUARE, dvz.DVZ_SEGMENT_CAP_SQUARE) != 0:
        raise RuntimeError("dvz_segment_set_caps(cursor) failed")
    if dvz.dvz_visual_set_depth_test(visual, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(cursor) failed")
    ex.add_visual(panel, visual)


def _add_path(scene, panel, positions, colors, widths) -> None:
    visual = dvz.dvz_path(scene, 0)
    if not visual:
        raise RuntimeError("dvz_path() failed")
    if dvz.dvz_visual_set_data_many(
        visual,
        {
            "position": positions,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(path) failed")
    if dvz.dvz_path_set_caps(visual, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND) != 0:
        raise RuntimeError("dvz_path_set_caps() failed")
    if dvz.dvz_path_set_join(visual, dvz.DVZ_PATH_JOIN_ROUND, 4.0) != 0:
        raise RuntimeError("dvz_path_set_join() failed")
    if dvz.dvz_visual_set_depth_test(visual, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(path) failed")
    ex.add_visual(panel, visual)


def _add_event_panel(scene, panel) -> None:
    starts, ends, colors, widths = _event_data()
    visual = dvz.dvz_segment(scene, 0)
    if not visual:
        raise RuntimeError("dvz_segment() failed")
    if dvz.dvz_visual_set_data_many(
        visual,
        {
            "position_start": starts,
            "position_end": ends,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(events) failed")
    if dvz.dvz_segment_set_caps(visual, dvz.DVZ_SEGMENT_CAP_SQUARE, dvz.DVZ_SEGMENT_CAP_SQUARE) != 0:
        raise RuntimeError("dvz_segment_set_caps(events) failed")
    if dvz.dvz_visual_set_depth_test(visual, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(events) failed")
    ex.add_visual(panel, visual)


def _add_residual_panel(scene, panel) -> None:
    positions, colors, diameters = _residual_data()
    visual = dvz.dvz_point(scene, 0)
    if not visual:
        raise RuntimeError("dvz_point() failed")
    if dvz.dvz_visual_set_data_many(
        visual,
        {
            "position": positions,
            "color": colors,
            "diameter_px": diameters,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(residuals) failed")
    style = dvz.dvz_point_style_desc()
    style.aspect = dvz.DVZ_SHAPE_ASPECT_FILLED
    if dvz.dvz_point_set_style(visual, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_point_set_style() failed")
    if dvz.dvz_visual_set_depth_test(visual, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(residuals) failed")
    ex.add_visual(panel, visual)


def _build_scene():
    scene = dvz.dvz_scene()
    if not scene:
        raise RuntimeError("dvz_scene() failed")
    figure = dvz.dvz_figure(scene, ex.WIDTH, ex.HEIGHT, 0)
    if not figure:
        raise RuntimeError("dvz_figure() failed")
    grid = dvz.dvz_figure_grid(figure, 3, 2)
    if not grid:
        raise RuntimeError("dvz_figure_grid() failed")

    margins = dvz.DvzPanelReserve(36.0, 30.0, 24.0, 30.0)
    if dvz.dvz_grid_set_margins(grid, ctypes.byref(margins)) != 0:
        raise RuntimeError("dvz_grid_set_margins() failed")
    if dvz.dvz_grid_set_gutter(grid, 28.0, 26.0) != 0:
        raise RuntimeError("dvz_grid_set_gutter() failed")

    signal = dvz.dvz_grid_panel(grid, 0, 0)
    events = dvz.dvz_grid_panel(grid, 1, 0)
    residuals = dvz.dvz_grid_panel(grid, 2, 0)
    summary = dvz.dvz_grid_panel_span(grid, 0, 1, 3, 1)
    if not signal or not events or not residuals or not summary:
        raise RuntimeError("dvz_grid_panel() failed")

    for panel in (signal, events, residuals, summary):
        _configure_panel(panel)

    _set_domains(signal, 0.0, 12.0, -1.6, 1.6)
    _set_domains(events, 0.0, 12.0, -0.8, 7.8)
    _set_domains(residuals, 0.0, 12.0, -1.0, 1.0)
    _set_domains(summary, -1.45, 1.45, -1.45, 1.45)

    for panel, ymin, ymax in ((signal, -1.6, 1.6), (events, -0.8, 7.8), (residuals, -1.0, 1.0)):
        _add_bands(scene, panel, ymin, ymax)
    _add_path(scene, signal, *_signal_data())
    _add_event_panel(scene, events)
    _add_residual_panel(scene, residuals)
    _add_path(scene, summary, *_phase_data())
    for panel, ymin, ymax in ((signal, -1.6, 1.6), (events, -0.8, 7.8), (residuals, -1.0, 1.0)):
        _add_cursor_lines(scene, panel, ymin, ymax)

    _add_axes(signal, None, b"signal")
    _add_axes(events, None, b"events")
    _add_axes(residuals, b"time (s)", b"residual")
    _add_axes(summary, b"signal", b"lagged")
    return scene, figure, (signal, events, residuals), summary


def _configure_view(view, scene, left_panels, summary) -> None:
    first_x = None
    for panel in left_panels:
        x_controller, x_panzoom = ex.bind_panzoom(view, scene, panel, dvz.DVZ_DIM_MASK_X)
        _, y_panzoom = ex.bind_panzoom(view, scene, panel, dvz.DVZ_DIM_MASK_Y)
        if not dvz.dvz_panzoom_zoom_limits(
            y_panzoom, (ctypes.c_float * 2)(1.0, 1.0), (ctypes.c_float * 2)(1.0, 1.0)
        ):
            raise RuntimeError("dvz_panzoom_zoom_limits() failed")
        if first_x is None:
            first_x = x_controller
            if dvz.dvz_panzoom_zoom(x_panzoom, (ctypes.c_float * 2)(1.36, 1.0)) != 0:
                raise RuntimeError("dvz_panzoom_zoom(left X) failed")
        else:
            link = dvz.dvz_controller_link(
                scene,
                first_x,
                x_controller,
                dvz.DVZ_CONTROLLER_LINK_EXTENT_X,
                dvz.DVZ_CONTROLLER_LINK_TWO_WAY,
            )
            if not link:
                raise RuntimeError("dvz_controller_link() failed")
    ex.bind_panzoom(view, scene, summary, dvz.DVZ_DIM_MASK_XY)


def main() -> None:
    scene, figure, left_panels, summary = _build_scene()

    def configure(view) -> None:
        _configure_view(view, scene, left_panels, summary)

    ex.run_with_view(scene, figure, "Linked Panels With Axes", configure)


if __name__ == "__main__":
    main()
/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* panel_linked_axes - This example builds linked time-series panels with shared axes behavior.
 *
 * What to look for: the left column combines a synthetic signal trace, an event-raster panel, and
 * residual points, while the right panel summarizes the same time range. The important arrays are
 * the path samples, event segments, residual points, shaded bands, and cursor lines; compare the
 * panels to see that they use different y domains while their x navigation remains linked.
 *
 * This workflow is useful for scientific dashboards where traces, events, and summary statistics
 * must stay aligned during panzoom interaction.
 *
 * Scenario: showcases_panel_linked_axes
 * Style: showcase workflow, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c showcases/panel_linked_axes
 * Run:    ./build/examples/c/showcases/panel_linked_axes --live
 */



/*************************************************************************************************/
/*  Includes                                                                                     */
/*************************************************************************************************/

#include <math.h>
#include <stdbool.h>
#include <stdint.h>

#include "_alloc.h"
#include "_assertions.h"
#include "datoviz/scene.h"
#include "example_common.h"
#include "example_style.h"
#include "runner/scenario_runner.h"



/*************************************************************************************************/
/*  Forward declarations                                                                         */
/*************************************************************************************************/

DvzScenarioSpec dvz_showcase_linked_panel_axes_scenario(void);



/*************************************************************************************************/
/*  Constants                                                                                    */
/*************************************************************************************************/

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define PATH_COUNT        360u
#define EVENT_COUNT       96u
#define EVENT_ROWS        8u
#define POINT_COUNT       168u
#define PHASE_COUNT       360u
#define BAND_COUNT        2u
#define BAND_VERTEX_COUNT (6u * BAND_COUNT)
#define CURSOR_COUNT      BAND_COUNT

static const float TAU = 6.28318530718f;



/*************************************************************************************************/
/*  Structs                                                                                      */
/*************************************************************************************************/

typedef struct LinkedAxesState
{
    DvzPanzoom* left_x[3];
    uint32_t left_x_count;
} LinkedAxesState;

#define LINKED_AXES_CHECK(condition, message)                                                    \
    do                                                                                            \
    {                                                                                             \
        if (!(condition))                                                                         \
        {                                                                                         \
            dvz_fprintf(stderr, "%s\n", message);                                                \
            return false;                                                                         \
        }                                                                                         \
    } while (0)



/*************************************************************************************************/
/*  Helpers                                                                                      */
/*************************************************************************************************/

/**
 * Return the deterministic signal used across panels.
 *
 * @param t normalized time in [0, 1]
 * @return signal value
 */
static float _signal(float t)
{
    return 0.85f * sinf(TAU * (1.08f * t + 0.06f)) +
           0.34f * sinf(TAU * (3.20f * t + 0.18f)) +
           0.12f * cosf(TAU * (6.00f * t + 0.10f));
}



/**
 * Return the deterministic secondary signal used for summaries.
 *
 * @param t normalized time in [0, 1]
 * @return secondary signal value
 */
static float _lagged_signal(float t)
{
    return 0.76f * sinf(TAU * (1.08f * t - 0.09f)) +
           0.30f * sinf(TAU * (2.50f * t + 0.32f));
}



/**
 * Fill deterministic signal path data in data coordinates.
 *
 * @param positions output data-space path positions
 * @param colors output path colors
 * @param widths output path stroke widths
 * @param count sample count
 */
static void _fill_signal(vec3* positions, DvzColor* colors, float* widths, uint32_t count)
{
    ANN(positions);
    ANN(colors);
    ANN(widths);

    const float inv_count = count > 1 ? 1.0f / (float)(count - 1u) : 1.0f;
    for (uint32_t i = 0; i < count; i++)
    {
        const float t = (float)i * inv_count;
        positions[i][0] = 12.0f * t;
        positions[i][1] = _signal(t);
        positions[i][2] = 0.0f;

        colors[i] = dvz_color_rgba(72, (uint8_t)(188.0f + 44.0f * t), 242, 255);
        widths[i] = 3.2f;
    }
}



/**
 * Fill event raster segments in data coordinates.
 *
 * @param starts output segment starts
 * @param ends output segment ends
 * @param colors output segment colors
 * @param widths output segment widths
 * @param count event count
 */
static void _fill_events(
    vec3* starts, vec3* ends, DvzColor* colors, float* widths, uint32_t count)
{
    ANN(starts);
    ANN(ends);
    ANN(colors);
    ANN(widths);

    for (uint32_t i = 0; i < count; i++)
    {
        const uint32_t row = i % EVENT_ROWS;
        const uint32_t group = i / EVENT_ROWS;
        const float base = (float)group / (float)((count / EVENT_ROWS) - 1u);
        const float phase = (float)row / (float)(EVENT_ROWS - 1u);
        const float x = 12.0f * base + 0.18f * sinf(TAU * (0.23f * (float)i + 0.17f * phase));
        const float y0 = (float)row - 0.34f;
        const float y1 = (float)row + 0.34f;

        starts[i][0] = fminf(fmaxf(x, 0.0f), 12.0f);
        starts[i][1] = y0;
        starts[i][2] = 0.0f;
        ends[i][0] = starts[i][0];
        ends[i][1] = y1;
        ends[i][2] = 0.0f;

        colors[i] = dvz_color_rgba(100, (uint8_t)(170u + 9u * row), 220, 230);
        widths[i] = 2.2f + 0.8f * (float)(row % 3u);
    }
}



/**
 * Fill deterministic residual point data in data coordinates.
 *
 * @param positions output data-space point positions
 * @param colors output point colors
 * @param diameters output point diameters
 * @param count point count
 */
static void _fill_residuals(vec3* positions, DvzColor* colors, float* diameters, uint32_t count)
{
    ANN(positions);
    ANN(colors);
    ANN(diameters);

    const float inv_count = count > 1 ? 1.0f / (float)(count - 1u) : 1.0f;
    for (uint32_t i = 0; i < count; i++)
    {
        const float t = (float)i * inv_count;
        const float x = 12.0f * t;
        const float y = 0.55f * (_signal(t) - _lagged_signal(t)) +
                        0.12f * sinf(TAU * (9.0f * t + 0.20f));

        positions[i][0] = x;
        positions[i][1] = y;
        positions[i][2] = 0.0f;

        const float mag = fminf(fabsf(y), 1.0f);
        colors[i] = dvz_color_rgba(
            (uint8_t)(110.0f + 65.0f * mag), (uint8_t)(170.0f + 52.0f * (1.0f - mag)),
            216, 238);
        diameters[i] = 4.0f + 5.0f * mag;
    }
}



/**
 * Fill phase portrait data in the right-side summary panel.
 *
 * @param positions output data-space path positions
 * @param colors output path colors
 * @param widths output path stroke widths
 * @param count sample count
 */
static void _fill_phase(vec3* positions, DvzColor* colors, float* widths, uint32_t count)
{
    ANN(positions);
    ANN(colors);
    ANN(widths);

    const float inv_count = count > 1 ? 1.0f / (float)(count - 1u) : 1.0f;
    for (uint32_t i = 0; i < count; i++)
    {
        const float t = (float)i * inv_count;
        positions[i][0] = _signal(t);
        positions[i][1] = _lagged_signal(t);
        positions[i][2] = 0.0f;

        colors[i] = dvz_color_rgba((uint8_t)(64.0f + 64.0f * t), 214, 205, 245);
        widths[i] = 2.4f;
    }
}



/**
 * Fill synchronized X bands in one panel's data coordinates.
 *
 * @param ymin panel data-domain minimum
 * @param ymax panel data-domain maximum
 * @param positions output band triangle positions
 * @param colors output band colors
 */
static void _fill_bands(double ymin, double ymax, vec3* positions, DvzColor* colors)
{
    ANN(positions);
    ANN(colors);

    const float x0[BAND_COUNT] = {3.10f, 8.05f};
    const float x1[BAND_COUNT] = {3.76f, 8.72f};
    const DvzColor band_colors[BAND_COUNT] = {
        {72, 170, 205, 45},
        {128, 220, 185, 38},
    };

    for (uint32_t b = 0; b < BAND_COUNT; b++)
    {
        const uint32_t k = 6u * b;
        positions[k + 0][0] = x0[b];
        positions[k + 0][1] = (float)ymin;
        positions[k + 1][0] = x1[b];
        positions[k + 1][1] = (float)ymin;
        positions[k + 2][0] = x1[b];
        positions[k + 2][1] = (float)ymax;
        positions[k + 3][0] = x0[b];
        positions[k + 3][1] = (float)ymin;
        positions[k + 4][0] = x1[b];
        positions[k + 4][1] = (float)ymax;
        positions[k + 5][0] = x0[b];
        positions[k + 5][1] = (float)ymax;
        for (uint32_t j = 0; j < 6u; j++)
        {
            positions[k + j][2] = -0.02f;
            colors[k + j] = band_colors[b];
        }
    }
}



/**
 * Fill fixed-pixel-width cursor lines in one panel's data coordinates.
 *
 * @param ymin panel data-domain minimum
 * @param ymax panel data-domain maximum
 * @param starts output cursor segment starts
 * @param ends output cursor segment ends
 * @param colors output cursor colors
 * @param widths output cursor line widths
 */
static void _fill_cursor_lines(
    double ymin, double ymax, vec3* starts, vec3* ends, DvzColor* colors, float* widths)
{
    ANN(starts);
    ANN(ends);
    ANN(colors);
    ANN(widths);

    const float x[CURSOR_COUNT] = {3.43f, 8.38f};
    const DvzColor line_colors[CURSOR_COUNT] = {
        {100, 220, 245, 185},
        {150, 240, 205, 175},
    };

    for (uint32_t i = 0; i < CURSOR_COUNT; i++)
    {
        starts[i][0] = x[i];
        starts[i][1] = (float)ymin;
        starts[i][2] = 0.0f;
        ends[i][0] = x[i];
        ends[i][1] = (float)ymax;
        ends[i][2] = 0.0f;
        colors[i] = line_colors[i];
        widths[i] = 2.2f;
    }
}



/**
 * Configure retained X/Y axes on one panel.
 *
 * @param panel target panel
 * @param x_label X axis label
 * @param y_label Y axis label
 * @return true when all axis calls succeed
 */
static bool _add_axes(DvzPanel* panel, const char* x_label, const char* y_label)
{
    ANN(panel);
    ANN(y_label);

    DvzAxis* x_axis = dvz_panel_axis(panel, DVZ_DIM_X);
    DvzAxis* y_axis = dvz_panel_axis(panel, DVZ_DIM_Y);
    if (x_axis == NULL || y_axis == NULL)
        return false;

    DvzAxisTickPolicy ticks = dvz_axis_tick_policy();
    ticks.target_count = 6;
    ticks.min_pixel_spacing = 96.0f;
    ticks.minor_per_interval = 3;
    if (dvz_axis_set_tick_policy(x_axis, &ticks) != DVZ_OK)
        return false;
    if (dvz_axis_set_tick_policy(y_axis, &ticks) != DVZ_OK)
        return false;
    ExampleAxisStyleOptions style = example_graphite_cyan_axis_options();
    style.tick_size_px = 11.0f;
    style.label_size_px = 14.0f;
    style.tick_gap_px = 7.0f;
    style.x_label_gap_px = 0.0f;
    style.y_label_gap_px = 14.0f;
    style.minor_tick_alpha = 210;
    style.grid_alpha = 145;
    if (!example_graphite_cyan_apply_axis_style(x_axis, false, &style) ||
        !example_graphite_cyan_apply_axis_style(y_axis, true, &style))
        return false;
    if (dvz_axis_set_grid(x_axis, true) != DVZ_OK || dvz_axis_set_grid(y_axis, true) != DVZ_OK)
        return false;
    if (x_label != NULL && dvz_axis_set_label(x_axis, x_label) != DVZ_OK)
        return false;
    return dvz_axis_set_label(y_axis, y_label) == DVZ_OK;
}



/**
 * Add synchronized reference bands clipped to one panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param ymin panel data-domain minimum
 * @param ymax panel data-domain maximum
 * @return true when the bands were added
 */
static bool _add_bands(DvzScene* scene, DvzPanel* panel, double ymin, double ymax)
{
    ANN(scene);
    ANN(panel);

    vec3 data_positions[BAND_VERTEX_COUNT] = {{0}};
    DvzColor colors[BAND_VERTEX_COUNT] = {{0}};
    _fill_bands(ymin, ymax, data_positions, colors);

    DvzVisual* visual = dvz_primitive(scene, DVZ_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0);
    if (visual == NULL)
        return false;
    if (dvz_visual_set_data(visual, "position", data_positions, BAND_VERTEX_COUNT) != 0)
        return false;
    if (dvz_visual_set_data(visual, "color", colors, BAND_VERTEX_COUNT) != 0)
        return false;
    if (dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_BLENDED) != 0)
        return false;
    if (dvz_visual_set_depth_test(visual, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



/**
 * Add synchronized fixed-width cursor lines clipped to one panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param ymin panel data-domain minimum
 * @param ymax panel data-domain maximum
 * @return true when the cursor lines were added
 */
static bool _add_cursor_lines(DvzScene* scene, DvzPanel* panel, double ymin, double ymax)
{
    ANN(scene);
    ANN(panel);

    vec3 starts[CURSOR_COUNT] = {{0}};
    vec3 ends[CURSOR_COUNT] = {{0}};
    DvzColor colors[CURSOR_COUNT] = {{0}};
    float widths[CURSOR_COUNT] = {0};
    _fill_cursor_lines(ymin, ymax, starts, ends, colors, widths);

    DvzVisual* visual = dvz_segment(scene, 0);
    if (visual == NULL)
        return false;
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position_start", .data = starts, .item_count = CURSOR_COUNT},
        {.attr_name = "position_end", .data = ends, .item_count = CURSOR_COUNT},
        {.attr_name = "color", .data = colors, .item_count = CURSOR_COUNT},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = CURSOR_COUNT},
    };
    if (dvz_visual_set_data_many(visual, updates, 4) != 0)
        return false;
    if (dvz_segment_set_caps(visual, DVZ_SEGMENT_CAP_SQUARE, DVZ_SEGMENT_CAP_SQUARE) != 0)
        return false;
    if (dvz_visual_set_depth_test(visual, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



/**
 * Add a stroked path visual to one panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param positions data-space path positions
 * @param colors path colors
 * @param widths path stroke widths
 * @param count sample count
 * @return true when the visual was added
 */
static bool _add_path(
    DvzScene* scene, DvzPanel* panel, vec3* positions, DvzColor* colors, float* widths,
    uint32_t count)
{
    ANN(scene);
    ANN(panel);
    ANN(positions);
    ANN(colors);
    ANN(widths);

    DvzVisual* visual = dvz_path(scene, 0);
    if (visual == NULL)
        return false;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = count},
        {.attr_name = "color", .data = colors, .item_count = count},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = count},
    };
    if (dvz_visual_set_data_many(visual, updates, 3) != 0)
        return false;
    if (dvz_path_set_caps(visual, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
        return false;
    if (dvz_path_set_join(visual, DVZ_PATH_JOIN_ROUND, 4.0f) != 0)
        return false;
    if (dvz_visual_set_depth_test(visual, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



/**
 * Add the top signal path panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the visual was added
 */
static bool _add_signal_panel(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 positions[PATH_COUNT] = {{0}};
    DvzColor colors[PATH_COUNT] = {{0}};
    float widths[PATH_COUNT] = {0};
    _fill_signal(positions, colors, widths, PATH_COUNT);
    return _add_path(scene, panel, positions, colors, widths, PATH_COUNT);
}



/**
 * Add the middle event raster panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the visual was added
 */
static bool _add_event_panel(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 starts[EVENT_COUNT] = {{0}};
    vec3 ends[EVENT_COUNT] = {{0}};
    DvzColor colors[EVENT_COUNT] = {{0}};
    float widths[EVENT_COUNT] = {0};
    _fill_events(starts, ends, colors, widths, EVENT_COUNT);

    DvzVisual* visual = dvz_segment(scene, 0);
    if (visual == NULL)
        return false;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position_start", .data = starts, .item_count = EVENT_COUNT},
        {.attr_name = "position_end", .data = ends, .item_count = EVENT_COUNT},
        {.attr_name = "color", .data = colors, .item_count = EVENT_COUNT},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = EVENT_COUNT},
    };
    if (dvz_visual_set_data_many(visual, updates, 4) != 0)
        return false;
    if (dvz_segment_set_caps(visual, DVZ_SEGMENT_CAP_SQUARE, DVZ_SEGMENT_CAP_SQUARE) != 0)
        return false;
    if (dvz_visual_set_depth_test(visual, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



/**
 * Add the bottom residual point panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the visual was added
 */
static bool _add_residual_panel(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 data_positions[POINT_COUNT] = {{0}};
    DvzColor colors[POINT_COUNT] = {{0}};
    float diameters[POINT_COUNT] = {0};
    _fill_residuals(data_positions, colors, diameters, POINT_COUNT);

    DvzVisual* visual = dvz_point(scene, 0);
    if (visual == NULL)
        return false;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = data_positions, .item_count = POINT_COUNT},
        {.attr_name = "color", .data = colors, .item_count = POINT_COUNT},
        {.attr_name = "diameter_px", .data = diameters, .item_count = POINT_COUNT},
    };
    if (dvz_visual_set_data_many(visual, updates, 3) != 0)
        return false;

    DvzPointStyleDesc style = dvz_point_style_desc();
    style.aspect = DVZ_SHAPE_ASPECT_FILLED;
    if (dvz_point_set_style(visual, &style) != 0)
        return false;
    if (dvz_visual_set_depth_test(visual, false) != 0)
        return false;
    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



/**
 * Add the right-side phase summary panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @return true when the visual was added
 */
static bool _add_summary_panel(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 positions[PHASE_COUNT] = {{0}};
    DvzColor colors[PHASE_COUNT] = {{0}};
    float widths[PHASE_COUNT] = {0};
    _fill_phase(positions, colors, widths, PHASE_COUNT);
    return _add_path(scene, panel, positions, colors, widths, PHASE_COUNT);
}



/**
 * Bind linked X panzooms and independent Y/summary panzooms.
 *
 * @param ctx scenario context
 * @param left left-column panels
 * @param left_count number of left-column panels
 * @param summary right summary panel
 * @return true when controllers and input routing are ready
 */
static bool _bind_linked_panzooms(
    DvzScenarioContext* ctx,
    DvzPanel** left,
    uint32_t left_count,
    DvzPanel* summary,
    LinkedAxesState* state)
{
    ANN(ctx);
    ANN(left);
    ANN(summary);

    DvzController* summary_xy = dvz_panzoom(ctx->scene, NULL);
    if (summary_xy == NULL)
        return false;

    DvzController* first_x = NULL;
    for (uint32_t i = 0; i < left_count; i++)
    {
        DvzController* x = dvz_panzoom(ctx->scene, NULL);
        DvzController* y = dvz_panzoom(ctx->scene, NULL);
        if (left[i] == NULL || x == NULL || y == NULL)
            return false;
        DvzPanzoom* x_panzoom = dvz_controller_panzoom(x);
        DvzPanzoom* y_panzoom = dvz_controller_panzoom(y);
        if (
            x_panzoom == NULL || y_panzoom == NULL ||
            !dvz_panzoom_zoom_limits(y_panzoom, (vec2){1.0f, 1.0f}, (vec2){1.0f, 1.0f}))
        {
            return false;
        }
        if (state != NULL && state->left_x_count < DVZ_ARRAY_COUNT(state->left_x))
            state->left_x[state->left_x_count++] = x_panzoom;
        if (dvz_scenario_bind_controller(ctx, left[i], x, DVZ_DIM_MASK_X) != 0)
            return false;
        if (dvz_scenario_bind_controller(ctx, left[i], y, DVZ_DIM_MASK_Y) != 0)
            return false;
        if (first_x == NULL)
        {
            first_x = x;
        }
        else if (
            dvz_controller_link(
                ctx->scene, first_x, x, DVZ_CONTROLLER_LINK_EXTENT_X,
                DVZ_CONTROLLER_LINK_TWO_WAY) == NULL)
        {
            return false;
        }
    }
    return dvz_scenario_bind_controller(ctx, summary, summary_xy, DVZ_DIM_MASK_XY) == 0;
}



/**
 * Set one panel domain with checked return values.
 *
 * @param panel target panel
 * @param x0 X minimum
 * @param x1 X maximum
 * @param y0 Y minimum
 * @param y1 Y maximum
 * @return true when both domains were set
 */
static bool _set_domains(DvzPanel* panel, double x0, double x1, double y0, double y1)
{
    ANN(panel);

    int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, x0, x1);
    if (rc != 0)
        return false;
    rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, y0, y1);
    return rc == 0;
}



/**
 * Add a subtle fixed border around one panel.
 *
 * @param panel target panel
 * @return true when the border was configured
 */
static bool _set_panel_border(DvzPanel* panel)
{
    ANN(panel);

    DvzColor color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
    color.a = 220u;
    DvzPanelBorderDesc border = dvz_panel_border_desc();
    border.color = color;
    border.width_px = 2.25f;
    border.inset_px = 1.125f;
    return dvz_panel_set_border(panel, &border) == DVZ_OK;
}



/**
 * Configure one panel's background and border.
 *
 * @param panel target panel
 * @return true when layout was configured
 */
static bool _configure_panel(DvzPanel* panel)
{
    ANN(panel);

    example_graphite_cyan_set_panel_background(panel);
    return _set_panel_border(panel);
}



/**
 * Initialize the linked-panel axes scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return whether setup succeeded
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    ANN(ctx);
    ANN(ctx->scene);
    if (out_user != NULL)
        *out_user = NULL;

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    LINKED_AXES_CHECK(ctx->figure != NULL, "dvz_figure() failed");
    DvzGrid* grid = dvz_figure_grid(ctx->figure, 3, 2);
    LINKED_AXES_CHECK(grid != NULL, "dvz_figure_grid() failed");
    bool ok = dvz_grid_set_margins(
                  grid, &(DvzPanelReserve){.left_px = 36.0f, .right_px = 30.0f,
                                           .top_px = 24.0f, .bottom_px = 30.0f}) == DVZ_OK;
    LINKED_AXES_CHECK(ok, "dvz_grid_set_margins() failed");
    ok = dvz_grid_set_gutter(grid, 28.0f, 26.0f) == DVZ_OK;
    LINKED_AXES_CHECK(ok, "dvz_grid_set_gutter() failed");

    DvzPanel* signal = dvz_grid_panel(grid, 0, 0);
    DvzPanel* events = dvz_grid_panel(grid, 1, 0);
    DvzPanel* residuals = dvz_grid_panel(grid, 2, 0);
    DvzPanel* summary = dvz_grid_panel_span(grid, 0, 1, 3, 1);
    LINKED_AXES_CHECK(
        signal != NULL && events != NULL && residuals != NULL && summary != NULL,
        "dvz_grid_panel() failed");

    ok = _configure_panel(signal);
    LINKED_AXES_CHECK(ok, "_configure_panel(signal) failed");
    ok = _configure_panel(events);
    LINKED_AXES_CHECK(ok, "_configure_panel(events) failed");
    ok = _configure_panel(residuals);
    LINKED_AXES_CHECK(ok, "_configure_panel(residuals) failed");
    example_graphite_cyan_set_panel_background(summary);
    ok = _set_panel_border(summary);
    LINKED_AXES_CHECK(ok, "_set_panel_border(summary) failed");

    ok = _set_domains(signal, 0.0, 12.0, -1.6, 1.6);
    LINKED_AXES_CHECK(ok, "_set_domains(signal) failed");
    ok = _set_domains(events, 0.0, 12.0, -0.8, 7.8);
    LINKED_AXES_CHECK(ok, "_set_domains(events) failed");
    ok = _set_domains(residuals, 0.0, 12.0, -1.0, 1.0);
    LINKED_AXES_CHECK(ok, "_set_domains(residuals) failed");
    ok = _set_domains(summary, -1.45, 1.45, -1.45, 1.45);
    LINKED_AXES_CHECK(ok, "_set_domains(summary) failed");

    ok = _add_bands(ctx->scene, signal, -1.6, 1.6);
    LINKED_AXES_CHECK(ok, "_add_bands(signal) failed");
    ok = _add_bands(ctx->scene, events, -0.8, 7.8);
    LINKED_AXES_CHECK(ok, "_add_bands(events) failed");
    ok = _add_bands(ctx->scene, residuals, -1.0, 1.0);
    LINKED_AXES_CHECK(ok, "_add_bands(residuals) failed");
    ok = _add_signal_panel(ctx->scene, signal);
    LINKED_AXES_CHECK(ok, "_add_signal_panel() failed");
    ok = _add_event_panel(ctx->scene, events);
    LINKED_AXES_CHECK(ok, "_add_event_panel() failed");
    ok = _add_residual_panel(ctx->scene, residuals);
    LINKED_AXES_CHECK(ok, "_add_residual_panel() failed");
    ok = _add_summary_panel(ctx->scene, summary);
    LINKED_AXES_CHECK(ok, "_add_summary_panel() failed");
    ok = _add_cursor_lines(ctx->scene, signal, -1.6, 1.6);
    LINKED_AXES_CHECK(ok, "_add_cursor_lines(signal) failed");
    ok = _add_cursor_lines(ctx->scene, events, -0.8, 7.8);
    LINKED_AXES_CHECK(ok, "_add_cursor_lines(events) failed");
    ok = _add_cursor_lines(ctx->scene, residuals, -1.0, 1.0);
    LINKED_AXES_CHECK(ok, "_add_cursor_lines(residuals) failed");

    ok = _add_axes(signal, NULL, "signal");
    LINKED_AXES_CHECK(ok, "_add_axes(signal) failed");
    ok = _add_axes(events, NULL, "events");
    LINKED_AXES_CHECK(ok, "_add_axes(events) failed");
    ok = _add_axes(residuals, "time (s)", "residual");
    LINKED_AXES_CHECK(ok, "_add_axes(residuals) failed");
    ok = _add_axes(summary, "signal", "lagged");
    LINKED_AXES_CHECK(ok, "_add_axes(summary) failed");

    DvzPanel* left[] = {signal, events, residuals};
    LinkedAxesState* state = (LinkedAxesState*)dvz_calloc(1, sizeof(*state));
    LINKED_AXES_CHECK(state != NULL, "linked axes state allocation failed");
    ok = _bind_linked_panzooms(ctx, left, 3, summary, state);
    if (!ok)
    {
        dvz_free(state);
        return false;
    }
    if (out_user != NULL)
        *out_user = state;

    return true;
}



/**
 * Apply deterministic shared-X panning for generated gallery media.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_frame(DvzScenarioContext* ctx, void* user)
{
    LinkedAxesState* state = (LinkedAxesState*)user;
    if (ctx == NULL || !ctx->preview_mode || state == NULL)
        return;

    const uint64_t count = ctx->preview_frame_count > 0 ? ctx->preview_frame_count : 1;
    const float phase = (float)(ctx->preview_frame_index % count) / (float)count;
    const float theta = TAU * phase;
    vec2 pan = {0.26f * sinf(theta), 0.0f};
    vec2 zoom = {1.36f, 1.0f};
    for (uint32_t i = 0; i < state->left_x_count; i++)
    {
        if (state->left_x[i] == NULL)
            continue;
        dvz_panzoom_zoom(state->left_x[i], zoom);
        dvz_panzoom_pan(state->left_x[i], pan);
    }
}



/**
 * Destroy the linked axes scenario state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    dvz_free(user);
}



/**
 * Return the linked-panel axes scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_showcase_linked_panel_axes_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "showcases_panel_linked_axes",
        .title = "Linked Panels With Axes",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_POINT_VISUAL | DVZ_SCENARIO_REQ_TEXT_VISUAL |
                        DVZ_SCENARIO_REQ_CONTROLLER | DVZ_SCENARIO_REQ_PANZOOM |
                        DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .frame = _scenario_frame,
        .destroy = _scenario_destroy,
    };
}



/*************************************************************************************************/
/*  Functions                                                                                    */
/*************************************************************************************************/

/**
 * Run the linked-panel axes and linked-X panzoom feature proof.
 *
 * @param argc command-line argument count
 * @param argv command-line argument vector
 * @return process exit code
 */
#ifndef DVZ_EXAMPLE_NO_MAIN
int main(int argc, char** argv)
{
    DvzScenarioSpec spec = dvz_showcase_linked_panel_axes_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif

#undef LINKED_AXES_CHECK
Example details

Tags

workflow, linked-panels, axes, panzoom, synthetic

Data

Field Value
kind synthetic