Skip to content

Linked Probe With Colorbar

This example links two image panels with a probe readout and colorbar.

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/linked_probe_colorbar (build and run), or rerun ./build/examples/c/showcases/linked_probe_colorbar
Python Available; direct-engine adaptation python3 -m examples.python.gallery.showcases.linked_probe_colorbar
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 source and derived images are generated from two FIELD_WIDTH x FIELD_HEIGHT float arrays and share one continuous scale. Moving the probe updates markers in both panels, queues a source-panel pixel query, and refreshes the readout with the raw and derived values; compare the same probe location across both images and the shared colorbar.

This workflow is useful for inspecting transformed measurements while preserving a visible link to the original sampled field.

Source

#!/usr/bin/env python3
"""Linked scalar image panels with a probe readout and shared colorbar."""

from __future__ import annotations

import ctypes
import math

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


FIELD_WIDTH = 256
FIELD_HEIGHT = 192
PROBE_X = 0.68
PROBE_Y = 0.56
PROBE_REQUEST_ID = 11
TAU = 2.0 * math.pi


def _clamp01(value):
    return np.clip(value, 0.0, 1.0)


def _sample_measurement(x, y):
    value = 0.13 + 0.07 * np.sin(TAU * (2.3 * x + 0.35 * y))
    value += 0.05 * np.cos(TAU * (0.55 * x - 3.6 * y))
    filament = np.sin(TAU * (x * 1.15 + 0.22 * np.sin(TAU * y)))
    value += 0.19 * np.exp(-18.0 * (filament - 0.18) * (filament - 0.18))

    centers = (
        (0.16, 0.22, 0.050),
        (0.31, 0.71, 0.042),
        (0.46, 0.38, 0.035),
        (0.58, 0.84, 0.040),
        (0.70, 0.56, 0.038),
        (0.78, 0.24, 0.046),
        (0.86, 0.69, 0.035),
        (0.24, 0.50, 0.030),
    )
    for i, (cx, cy, sigma) in enumerate(centers):
        dx = x - cx
        dy = y - cy
        d2 = (dx * dx + 1.4 * dy * dy) / (2.0 * sigma * sigma)
        value += (0.20 + 0.06 * (i % 3)) * np.exp(-d2)

    hot_dx = x - 0.69
    hot_dy = y - 0.57
    value += 0.40 * np.exp(-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0 * 0.030 * 0.030))
    return _clamp01(value)


def _sample_derived(x, y):
    value = 0.18 + 0.72 * _sample_measurement(x, y)
    value -= 0.16 * np.exp(-((x - 0.69) * (x - 0.69)) / (2.0 * 0.100 * 0.100))
    value += 0.11 * np.sin(TAU * (0.85 * x + 1.40 * y))
    value += 0.07 * np.cos(TAU * (2.20 * x - 0.45 * y))
    return _clamp01(value)


def _fields():
    x = np.linspace(0.0, 1.0, FIELD_WIDTH, dtype=np.float32)
    y = np.linspace(0.0, 1.0, FIELD_HEIGHT, dtype=np.float32)
    u, v = np.meshgrid(x, y)
    measurement = _sample_measurement(u, v).astype(np.float32)
    derived = _sample_derived(u, v).astype(np.float32)
    return measurement, derived


def _set_image_domain(panel) -> None:
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, 0.0, 1.0) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, 0.0, 1.0) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")


def _add_scale(scene):
    scale = ex.continuous_scale(scene, b"python_linked_probe_colorbar")
    fmt = dvz.dvz_format_desc()
    fmt.precision = 2
    fmt.trim_trailing_zeros = True
    if dvz.dvz_scale_set_format(scale, ctypes.byref(fmt)) != 0:
        raise RuntimeError("dvz_scale_set_format() failed")
    if dvz.dvz_scale_set_view_range(scale, 0.0, 1.0) != 0:
        raise RuntimeError("dvz_scale_set_view_range() failed")
    return scale


def _add_image(scene, panel, scale, values, queryable: bool):
    positions = np.array(
        [[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
        dtype=np.float32,
    )
    texcoords = np.array(
        [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]],
        dtype=np.float32,
    )
    image = dvz.dvz_image(scene, 0)
    if not image:
        raise RuntimeError("dvz_image() failed")
    if dvz.dvz_visual_set_data_many(image, {"position": positions, "texcoords": texcoords}) != 0:
        raise RuntimeError("dvz_visual_set_data_many(image) failed")
    if dvz.dvz_visual_set_scale(image, b"color", scale) != 0:
        raise RuntimeError("dvz_visual_set_scale() failed")
    field = dvz.dvz_sampled_field_from_array(scene, values)
    if dvz.dvz_visual_set_field(image, b"field", field) != 0:
        raise RuntimeError("dvz_visual_set_field() failed")
    if dvz.dvz_visual_set_depth_test(image, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(image) failed")
    if queryable:
        dvz.dvz_visual_set_query_capabilities(image, dvz.DVZ_QUERY_CAPABILITY_PIXEL)
    ex.add_visual(panel, image)
    return image


def _add_probe_marker(scene, panel):
    marker = dvz.dvz_marker(scene, 0)
    if not marker:
        raise RuntimeError("dvz_marker() failed")

    positions = np.array([[PROBE_X, PROBE_Y, 0.04]], dtype=np.float32)
    colors = ex.color_array(ex.YELLOW)
    colors[0, 3] = 245
    diameters = np.array([34.0], dtype=np.float32)
    angles = np.array([0.0], dtype=np.float32)
    shapes = np.array([dvz.DVZ_MARKER_SHAPE_TARGET], dtype=np.uint32)
    if dvz.dvz_visual_set_data_many(
        marker,
        {
            "position": positions,
            "color": colors,
            "diameter_px": diameters,
            "angle": angles,
            "shape": shapes,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(marker) failed")

    style = dvz.dvz_marker_style()
    style.aspect = dvz.DVZ_SHAPE_ASPECT_FILLED
    style.stroke_width_px = 0.0
    if dvz.dvz_marker_set_style(marker, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_marker_set_style() failed")
    if dvz.dvz_visual_set_depth_test(marker, False) != 0:
        raise RuntimeError("dvz_visual_set_depth_test(marker) failed")
    ex.add_visual(panel, marker)
    return marker


def _update_probe_marker(marker, x: float, y: float) -> None:
    position = np.array([[x, y, 0.04]], dtype=np.float32)
    if dvz.dvz_visual_set_data(marker, b"position", position) != 0:
        raise RuntimeError("dvz_visual_set_data(marker position) failed")


def _add_text(panel, text: bytes, x: float, y: float, size: float, color):
    visual = dvz.dvz_text(panel, 0)
    if not visual:
        raise RuntimeError("dvz_text() failed")
    style = dvz.dvz_text_style()
    style.size_px = size
    style.renderer = dvz.DVZ_TEXT_RENDERER_MSDF_ATLAS
    style.color[:] = (color.r, color.g, color.b, 255)
    if dvz.dvz_text_set_style(visual, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_text_set_style() failed")
    placement = dvz.dvz_text_placement()
    placement.mode = dvz.DVZ_TEXT_PLACEMENT_SCREEN
    placement.anchor = dvz.DVZ_SCENE_ANCHOR_PANEL_TOP_LEFT
    placement.position[:] = (x, y, 0.0)
    placement.text_anchor[:] = (0.0, 0.5)
    placement.has_text_anchor = True
    placement.depth_test = False
    if dvz.dvz_text_set_placement(visual, ctypes.byref(placement)) != 0:
        raise RuntimeError("dvz_text_set_placement() failed")
    if dvz.dvz_text_set_string(visual, text) != 0:
        raise RuntimeError("dvz_text_set_string() failed")
    return visual


def _readout_text(x: float, y: float, hit: bool = True) -> bytes:
    if not hit:
        return b"probe outside image"
    measurement = float(_sample_measurement(x, y))
    derived = float(_sample_derived(x, y))
    return f"x {x:.3f}  y {y:.3f}    measurement {measurement:.3f}    derived {derived:.3f}".encode()


def _update_readout(text, x: float, y: float, hit: bool = True) -> None:
    if dvz.dvz_text_set_string(text, _readout_text(x, y, hit)) != 0:
        raise RuntimeError("dvz_text_set_string(readout) failed")


def _add_axes(panel) -> 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 = 5
    ticks.min_pixel_spacing = 90.0
    ticks.minor_per_interval = 1
    for axis, vertical in ((x_axis, False), (y_axis, True)):
        style = dvz.dvz_axis_style()
        style.tick_size_px = 12.0
        style.label_size_px = 15.0
        style.label_gap_px = 42.0 if vertical else 30.0
        style.grid_color[:] = (96, 165, 250, 120)
        style.spine_color[:] = (217, 226, 236, 190)
        style.major_tick_color[:] = (217, 226, 236, 210)
        style.minor_tick_color[:] = (217, 226, 236, 160)
        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 dvz.dvz_axis_set_tick_policy(axis, ctypes.byref(ticks)) != 0:
            raise RuntimeError("dvz_axis_set_tick_policy() failed")


def _add_colorbar(panel, scale) -> None:
    desc = dvz.dvz_colorbar_desc()
    desc.orientation = dvz.DVZ_COLORBAR_ORIENTATION_VERTICAL
    desc.anchor = dvz.DVZ_SCENE_ANCHOR_PANEL_RIGHT
    desc.title = b"intensity"
    desc.reserve_px = 112.0
    desc.ramp_width_px = 28.0
    desc.plot_gap_px = 14.0
    desc.tick_length_px = 6.0
    desc.label_gap_px = 7.0
    colorbar = dvz.dvz_colorbar(panel, scale, ctypes.byref(desc))
    if not colorbar:
        raise RuntimeError("dvz_colorbar() failed")
    fmt = dvz.dvz_format_desc()
    fmt.precision = 2
    fmt.trim_trailing_zeros = True
    if dvz.dvz_colorbar_set_format(colorbar, ctypes.byref(fmt)) != 0:
        raise RuntimeError("dvz_colorbar_set_format() failed")


def _data_to_panel_px(panel, x: float, y: float):
    src = (ctypes.c_double * 2)(float(x), float(y))
    dst = (ctypes.c_double * 2)()
    ok = dvz.dvz_panel_data_to_position(panel, dvz.DVZ_PANEL_COORD_PANEL_PX, src, dst)
    if not ok:
        return None
    return float(dst[0]), float(dst[1])


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, 1, 2)
    if not grid:
        raise RuntimeError("dvz_figure_grid() failed")
    margins = dvz.DvzPanelReserve(42.0, 34.0, 46.0, 34.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, 0.0) != 0:
        raise RuntimeError("dvz_grid_set_gutter() failed")

    source = dvz.dvz_grid_panel(grid, 0, 0)
    derived_panel = dvz.dvz_grid_panel(grid, 0, 1)
    if not source or not derived_panel:
        raise RuntimeError("dvz_grid_panel() failed")
    for panel in (source, derived_panel):
        dvz.dvz_panel_set_background_color(panel, ex.BG)
        _set_image_domain(panel)

    measurement, derived = _fields()
    scale = _add_scale(scene)
    _add_image(scene, source, scale, measurement, True)
    _add_image(scene, derived_panel, scale, derived, False)
    _add_axes(source)
    _add_axes(derived_panel)
    _add_colorbar(derived_panel, scale)

    source_marker = _add_probe_marker(scene, source)
    derived_marker = _add_probe_marker(scene, derived_panel)
    _add_text(source, b"measurement", 96.0, 74.0, 18.0, ex.BLUE)
    _add_text(derived_panel, b"derived", 96.0, 74.0, 18.0, ex.BLUE)
    readout = _add_text(source, _readout_text(PROBE_X, PROBE_Y), 78.0, 56.0, 16.0, ex.TEXT)
    return scene, figure, source, derived_panel, source_marker, derived_marker, readout


def _configure_view(view, scene, source, derived_panel) -> None:
    source_controller, _ = ex.bind_panzoom(view, scene, source, dvz.DVZ_DIM_MASK_XY)
    derived_controller, _ = ex.bind_panzoom(view, scene, derived_panel, dvz.DVZ_DIM_MASK_XY)
    link = dvz.dvz_controller_link(
        scene,
        source_controller,
        derived_controller,
        dvz.DVZ_CONTROLLER_LINK_EXTENT_X | dvz.DVZ_CONTROLLER_LINK_EXTENT_Y,
        dvz.DVZ_CONTROLLER_LINK_TWO_WAY,
    )
    if not link:
        raise RuntimeError("dvz_controller_link() failed")


def _set_probe(source, derived_panel, source_marker, derived_marker, readout, x: float, y: float):
    x = float(np.clip(x, 0.0, 1.0))
    y = float(np.clip(y, 0.0, 1.0))
    _update_probe_marker(source_marker, x, y)
    _update_probe_marker(derived_marker, x, y)
    _update_readout(readout, x, y, True)
    return _data_to_panel_px(source, x, y)


def main() -> None:
    scene, figure, source, derived_panel, source_marker, derived_marker, readout = _build_scene()
    state = {"cursor": None, "last": None}

    def configure(view) -> None:
        _configure_view(view, scene, source, derived_panel)

    def on_pointer(event) -> None:
        if event.type not in (
            dvz.DVZ_POINTER_EVENT_MOVE,
            dvz.DVZ_POINTER_EVENT_CLICK,
            dvz.DVZ_POINTER_EVENT_PRESS,
        ):
            return
        data = None
        for panel in (source, derived_panel):
            panel_pos = ex.figure_to_panel_px(panel, event.pos[0], event.pos[1])
            if panel_pos is None:
                continue
            data = ex.panel_px_to_data(panel, panel_pos[0], panel_pos[1])
            if data is not None:
                break
        if data is None or not (0.0 <= data[0] <= 1.0 and 0.0 <= data[1] <= 1.0):
            return
        state["cursor"] = _set_probe(
            source, derived_panel, source_marker, derived_marker, readout, data[0], data[1]
        )

    def on_frame(_view, frame_index: int, _elapsed: float) -> None:
        if frame_index == 0 and state["cursor"] is None:
            state["cursor"] = _set_probe(
                source, derived_panel, source_marker, derived_marker, readout, PROBE_X, PROBE_Y
            )
        cursor = state["cursor"]
        if cursor is not None:
            ex.queue_panel_query(
                source, cursor[0], cursor[1], PROBE_REQUEST_ID, dvz.DVZ_SCENE_TARGET_PIXEL
            )
        for query in ex.poll_queries(scene):
            if query.request_id != PROBE_REQUEST_ID:
                continue
            data = ex.panel_px_to_data(source, query.panel_position[0], query.panel_position[1])
            if data is None:
                _update_readout(readout, 0.0, 0.0, False)
                state["last"] = None
                continue
            x, y = data
            hit = bool(query.hit) and 0.0 <= x <= 1.0 and 0.0 <= y <= 1.0
            value = (round(x, 3), round(y, 3), hit)
            if value != state["last"]:
                _update_readout(readout, x, y, hit)
                state["last"] = value

    ex.run_with_input_callbacks(
        scene,
        figure,
        "Linked Probe With Colorbar",
        on_pointer=on_pointer,
        on_frame=on_frame,
        configure_view=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
 */

/* linked_probe_colorbar - This example links two image panels with a probe readout and colorbar.
 *
 * What to look for: the source and derived images are generated from two FIELD_WIDTH x
 * FIELD_HEIGHT float arrays and share one continuous scale. Moving the probe updates markers in
 * both panels, queues a source-panel pixel query, and refreshes the readout with the raw and
 * derived values; compare the same probe location across both images and the shared colorbar.
 *
 * This workflow is useful for inspecting transformed measurements while preserving a visible link
 * to the original sampled field.
 *
 * Scenario: showcases_linked_probe_colorbar
 * Style: showcase workflow, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c showcases/linked_probe_colorbar
 * Run:    ./build/examples/c/showcases/linked_probe_colorbar --live
 * Smoke:  ./build/examples/c/showcases/linked_probe_colorbar --png
 */



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

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

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



DvzScenarioSpec dvz_showcase_linked_probe_colorbar_scenario(void);



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

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define FIELD_WIDTH         256u
#define FIELD_HEIGHT        192u
#define PROBE_X             0.68f
#define PROBE_Y             0.56f
#define PROBE_REQUEST_ID    11u

static const float TAU = 6.28318530718f;



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

typedef struct ProbeMarker
{
    DvzPanel* panel;
    DvzVisual* visual;
} ProbeMarker;


typedef struct LinkedProbeState
{
    DvzScene* scene;
    float* measurement;
    float* derived;
    DvzPanel* source_panel;
    DvzPanel* derived_panel;
    ProbeMarker source_marker;
    ProbeMarker derived_marker;
    DvzText* readout;
    bool cursor_valid;
    double cursor_x;
    double cursor_y;
    double last_raw;
    double last_derived;
    bool last_hit;
    bool has_last_result;
} LinkedProbeState;



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

/**
 * Clamp a float to the unit interval.
 *
 * @param value input value
 * @return clamped value
 */
static float _clamp01(float value)
{
    if (value < 0.0f)
        return 0.0f;
    if (value > 1.0f)
        return 1.0f;
    return value;
}



/**
 * Return a deterministic synthetic measurement sample.
 *
 * @param x normalized X coordinate
 * @param y normalized Y coordinate
 * @return normalized sample value
 */
static float _sample_measurement(float x, float y)
{
    float value = 0.13f + 0.07f * sinf(TAU * (2.3f * x + 0.35f * y));
    value += 0.05f * cosf(TAU * (0.55f * x - 3.6f * y));

    const float filament = sinf(TAU * (x * 1.15f + 0.22f * sinf(TAU * y)));
    value += 0.19f * expf(-18.0f * (filament - 0.18f) * (filament - 0.18f));

    const float centers[8][3] = {
        {0.16f, 0.22f, 0.050f}, {0.31f, 0.71f, 0.042f}, {0.46f, 0.38f, 0.035f},
        {0.58f, 0.84f, 0.040f}, {0.70f, 0.56f, 0.038f}, {0.78f, 0.24f, 0.046f},
        {0.86f, 0.69f, 0.035f}, {0.24f, 0.50f, 0.030f},
    };
    for (uint32_t i = 0; i < 8u; i++)
    {
        const float dx = x - centers[i][0];
        const float dy = y - centers[i][1];
        const float sigma = centers[i][2];
        const float d2 = (dx * dx + 1.4f * dy * dy) / (2.0f * sigma * sigma);
        value += (0.20f + 0.06f * (float)(i % 3u)) * expf(-d2);
    }

    const float hot_dx = x - 0.69f;
    const float hot_dy = y - 0.57f;
    value += 0.40f * expf(-(hot_dx * hot_dx + hot_dy * hot_dy) / (2.0f * 0.030f * 0.030f));
    return _clamp01(value);
}



/**
 * Return a second deterministic field derived from the measurement.
 *
 * @param x normalized X coordinate
 * @param y normalized Y coordinate
 * @return normalized sample value
 */
static float _sample_derived(float x, float y)
{
    float value = 0.18f + 0.72f * _sample_measurement(x, y);
    value -= 0.16f * expf(-((x - 0.69f) * (x - 0.69f)) / (2.0f * 0.100f * 0.100f));
    value += 0.11f * sinf(TAU * (0.85f * x + 1.40f * y));
    value += 0.07f * cosf(TAU * (2.20f * x - 0.45f * y));
    return _clamp01(value);
}



/**
 * Fill both scalar fields used by the workflow.
 *
 * @param measurement output measurement field
 * @param derived output derived field
 */
static void _fill_fields(
    float measurement[FIELD_WIDTH * FIELD_HEIGHT], float derived[FIELD_WIDTH * FIELD_HEIGHT])
{
    ANN(measurement);
    ANN(derived);

    for (uint32_t y = 0; y < FIELD_HEIGHT; y++)
    {
        for (uint32_t x = 0; x < FIELD_WIDTH; x++)
        {
            const float u = FIELD_WIDTH > 1u ? (float)x / (float)(FIELD_WIDTH - 1u) : 0.0f;
            const float v = FIELD_HEIGHT > 1u ? (float)y / (float)(FIELD_HEIGHT - 1u) : 0.0f;
            measurement[y * FIELD_WIDTH + x] = _sample_measurement(u, v);
            derived[y * FIELD_WIDTH + x] = _sample_derived(u, v);
        }
    }
}



/**
 * Set the normalized image data domain on one panel.
 *
 * @param panel target panel
 * @return true when both dimensions were set
 */
static bool _set_image_domain(DvzPanel* panel)
{
    ANN(panel);

    int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, 0.0, 1.0);
    if (rc != 0)
        return false;
    rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, 0.0, 1.0);
    return rc == 0;
}



/**
 * Create the continuous scale shared by both images and the colorbar.
 *
 * @param scene scene owning scale resources
 * @return created scale, or NULL on failure
 */
static DvzScale* _add_scale(DvzScene* scene)
{
    ANN(scene);

    DvzScale* scale = dvz_scale(
        scene, &(DvzScaleDesc){DVZ_STRUCT_INIT_FIELDS(DvzScaleDesc),
                   .kind = DVZ_SCALE_CONTINUOUS,
                   .label = "intensity",
               });
    if (scale == NULL)
        return NULL;
    dvz_scale_set_format(
        scale, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc),
                   .precision = 2,
                   .trim_trailing_zeros = true});
    dvz_scale_set_domain(scale, 0.0, 1.0);
    dvz_scale_set_view_range(scale, 0.0, 1.0);

    DvzColormap* colormap = example_graphite_cyan_colormap(scene);
    if (colormap == NULL)
        return NULL;
    dvz_scale_set_colormap(scale, colormap);
    return scale;
}



/**
 * Add one scalar image panel.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param scale shared scalar color scale
 * @param values scalar field data
 * @param queryable whether the image should accept pixel-query requests
 * @return true when the image was added
 */
static bool _add_image(
    DvzScene* scene, DvzPanel* panel, DvzScale* scale, float values[FIELD_WIDTH * FIELD_HEIGHT],
    bool queryable)
{
    ANN(scene);
    ANN(panel);
    ANN(scale);
    ANN(values);

    vec3 data_positions[4] = {
        {0.0f, 0.0f, 0.0f},
        {0.0f, 1.0f, 0.0f},
        {1.0f, 0.0f, 0.0f},
        {1.0f, 1.0f, 0.0f},
    };
    vec2 texcoords[4] = {
        {0.0f, 0.0f},
        {0.0f, 1.0f},
        {1.0f, 0.0f},
        {1.0f, 1.0f},
    };

    DvzVisual* image = dvz_image(scene, 0);
    if (image == NULL)
        return false;
    if (dvz_visual_set_data(image, "position", data_positions, 4) != 0)
        return false;
    if (dvz_visual_set_data(image, "texcoords", texcoords, 4) != 0)
        return false;
    if (dvz_visual_set_scale(image, "color", scale) != 0)
        return false;

    DvzSampledField* field = dvz_sampled_field(
        scene, &(DvzSampledFieldDesc){DVZ_STRUCT_INIT_FIELDS(DvzSampledFieldDesc),
                   .dim = DVZ_FIELD_DIM_2D,
                   .format = DVZ_FIELD_FORMAT_R32_FLOAT,
                   .semantic = DVZ_FIELD_SEMANTIC_SCALAR,
                   .width = FIELD_WIDTH,
                   .height = FIELD_HEIGHT,
                   .depth = 1,
               });
    if (field == NULL)
        return false;
    if (dvz_sampled_field_set_data(
            field, &(DvzFieldDataView){DVZ_STRUCT_INIT_FIELDS(DvzFieldDataView),
                       .data = values,
                       .bytes_per_row = FIELD_WIDTH * sizeof(float),
                       .rows_per_image = FIELD_HEIGHT,
                   }) != DVZ_OK)
    {
        return false;
    }
    if (dvz_visual_set_field(image, "field", field) != DVZ_OK)
        return false;
    if (dvz_visual_set_depth_test(image, false) != 0)
        return false;
    if (queryable)
        dvz_visual_set_query_capabilities(image, DVZ_QUERY_CAPABILITY_PIXEL);
    return dvz_panel_add_visual(panel, image, NULL) == 0;
}



/**
 * Move one marker to a normalized data position.
 *
 * @param marker marker to update
 * @param x normalized data X
 * @param y normalized data Y
 * @return true when the marker was updated
 */
static bool _update_probe_marker(ProbeMarker* marker, float x, float y)
{
    if (marker == NULL || marker->panel == NULL || marker->visual == NULL)
        return false;

    vec3 marker_data[1] = {{x, y, 0.04f}};
    return dvz_visual_set_data(marker->visual, "position", marker_data, 1) == 0;
}


/**
 * Add one marker visual pair to a panel.
 *
 * @param scene scene owning visuals
 * @param panel panel receiving marker visuals
 * @param out output marker
 * @return true when the marker was added
 */
static bool _add_probe_marker(DvzScene* scene, DvzPanel* panel, ProbeMarker* out)
{
    ANN(scene);
    ANN(panel);
    ANN(out);

    ProbeMarker marker = {.panel = panel};
    marker.visual = dvz_marker(scene, 0);
    if (marker.visual == NULL)
        return false;
    vec3 marker_data[1] = {{PROBE_X, PROBE_Y, 0.04f}};
    DvzColor marker_color[1] = {example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_WARNING)};
    marker_color[0].a = 245u;
    float marker_diameter[1] = {34.0f};
    float marker_angle[1] = {0.0f};
    uint32_t marker_shape[1] = {DVZ_MARKER_SHAPE_TARGET};
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = marker_data, .item_count = 1},
        {.attr_name = "color", .data = marker_color, .item_count = 1},
        {.attr_name = "diameter_px", .data = marker_diameter, .item_count = 1},
        {.attr_name = "angle", .data = marker_angle, .item_count = 1},
        {.attr_name = "shape", .data = marker_shape, .item_count = 1},
    };
    if (dvz_visual_set_data_many(marker.visual, updates, 5) != 0)
        return false;
    DvzMarkerStyle marker_style = dvz_marker_style();
    marker_style.aspect = DVZ_SHAPE_ASPECT_FILLED;
    marker_style.stroke_width_px = 0.0f;
    if (dvz_marker_set_style(marker.visual, &marker_style) != 0)
        return false;
    if (dvz_visual_set_depth_test(marker.visual, false) != 0)
        return false;
    if (dvz_panel_add_visual(panel, marker.visual, NULL) != 0)
        return false;

    *out = marker;
    return true;
}



/**
 * Set or refresh the readout text from one normalized data coordinate.
 *
 * @param state workflow state
 * @param x normalized data X
 * @param y normalized data Y
 * @param hit whether the latest query hit the source image
 */
static void _update_readout(LinkedProbeState* state, float x, float y, bool hit)
{
    if (state == NULL || state->readout == NULL)
        return;

    char text[160] = {0};
    if (hit)
    {
        const float measurement = _sample_measurement(x, y);
        const float derived = _sample_derived(x, y);
        dvz_snprintf(
            text, sizeof(text), "x %.3f  y %.3f    measurement %.3f    derived %.3f", x, y,
            measurement, derived);
    }
    else
    {
        dvz_snprintf(text, sizeof(text), "probe outside image");
    }
    dvz_text_set_string(state->readout, text);
}



/**
 * Add the retained screen-space readout text.
 *
 * @param panel panel receiving the text object
 * @return created text object, or NULL on failure
 */
static DvzText* _add_readout(DvzPanel* panel)
{
    ANN(panel);

    DvzText* text = dvz_text(panel, 0);
    if (text == NULL)
        return NULL;

    DvzColor color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
    DvzTextStyle style = dvz_text_style();
    style.size_px = 16.0f;
    style.renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
    style.color[0] = color.r;
    style.color[1] = color.g;
    style.color[2] = color.b;
    style.color[3] = 255u;
    if (dvz_text_set_style(text, &style) != 0)
        return NULL;

    DvzTextPlacement placement = dvz_text_placement();
    placement.mode = DVZ_TEXT_PLACEMENT_SCREEN;
    placement.anchor = DVZ_SCENE_ANCHOR_PANEL_TOP_LEFT;
    placement.position[0] = 78.0f;
    placement.position[1] = 56.0f;
    placement.position[2] = 0.0f;
    placement.text_anchor[0] = 0.0f;
    placement.text_anchor[1] = 0.5f;
    placement.has_text_anchor = true;
    placement.depth_test = false;
    dvz_text_set_placement(text, &placement);

    dvz_text_set_string(text, "x 0.680  y 0.560    measurement 0.000    derived 0.000");
    return text;
}



/**
 * Add a small panel label.
 *
 * @param panel target panel
 * @param label panel label
 * @param role text color role
 * @return true when the label was added
 */
static bool _add_panel_label(DvzPanel* panel, const char* label, ExampleStyleColorRole role)
{
    ANN(panel);
    ANN(label);

    DvzText* text = dvz_text(panel, 0);
    if (text == NULL)
        return false;
    DvzColor color = example_graphite_cyan_color(role);
    DvzTextStyle style = dvz_text_style();
    style.size_px = 18.0f;
    style.renderer = DVZ_TEXT_RENDERER_MSDF_ATLAS;
    style.color[0] = color.r;
    style.color[1] = color.g;
    style.color[2] = color.b;
    style.color[3] = 255u;
    if (dvz_text_set_style(text, &style) != 0)
        return false;

    DvzTextPlacement placement = dvz_text_placement();
    placement.mode = DVZ_TEXT_PLACEMENT_SCREEN;
    placement.anchor = DVZ_SCENE_ANCHOR_PANEL_TOP_LEFT;
    placement.position[0] = 96.0f;
    placement.position[1] = 74.0f;
    placement.position[2] = 0.0f;
    placement.text_anchor[0] = 0.0f;
    placement.text_anchor[1] = 0.5f;
    placement.has_text_anchor = true;
    placement.depth_test = false;
    dvz_text_set_placement(text, &placement);
    dvz_text_set_string(text, label);
    return true;
}



/**
 * Add linear axes to one normalized image panel.
 *
 * @param panel target panel
 * @return true when both axes were configured
 */
static bool _add_axes(DvzPanel* panel)
{
    ANN(panel);

    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;

    ExampleAxisStyleOptions options = example_graphite_cyan_axis_options();
    options.label_size_px = 15.0f;
    options.tick_size_px = 12.0f;
    options.x_label_gap_px = 30.0f;
    options.y_label_gap_px = 42.0f;
    if (!example_graphite_cyan_apply_axis_style(x_axis, false, &options))
        return false;
    if (!example_graphite_cyan_apply_axis_style(y_axis, true, &options))
        return false;
    if (dvz_axis_set_grid(x_axis, true) != DVZ_OK || dvz_axis_set_grid(y_axis, true) != DVZ_OK)
        return false;

    DvzAxisTickPolicy ticks = dvz_axis_tick_policy();
    ticks.target_count = 5;
    ticks.min_pixel_spacing = 90.0f;
    ticks.minor_per_interval = 1;
    if (dvz_axis_set_tick_policy(x_axis, &ticks) != DVZ_OK)
        return false;
    return dvz_axis_set_tick_policy(y_axis, &ticks) == DVZ_OK;
}



/**
 * Add the shared vertical colorbar to the derived panel.
 *
 * @param panel panel receiving the colorbar
 * @param scale shared scale
 * @return true when the colorbar was added
 */
static bool _add_colorbar(DvzPanel* panel, DvzScale* scale)
{
    ANN(panel);
    ANN(scale);

    DvzColorbar* colorbar = dvz_colorbar(
        panel, scale,
        &(DvzColorbarDesc){DVZ_STRUCT_INIT_FIELDS(DvzColorbarDesc),
            .orientation = DVZ_COLORBAR_ORIENTATION_VERTICAL,
            .anchor = DVZ_SCENE_ANCHOR_PANEL_RIGHT,
            .title = "intensity",
            .reserve_px = 112.0f,
            .ramp_width_px = 28.0f,
            .plot_gap_px = 14.0f,
            .tick_length_px = 6.0f,
            .label_gap_px = 7.0f,
        });
    if (colorbar == NULL)
        return false;
    dvz_colorbar_set_format(
        colorbar, &(DvzFormatDesc){DVZ_STRUCT_INIT_FIELDS(DvzFormatDesc),
                      .precision = 2,
                      .trim_trailing_zeros = true});
    return true;
}



/**
 * Bind linked panzoom controllers to both image panels.
 *
 * @param ctx scenario context
 * @param source source panel
 * @param derived derived panel
 * @return true when controller binding is ready
 */
static bool _bind_linked_panzoom(
    DvzScenarioContext* ctx, DvzPanel* source, DvzPanel* derived)
{
    ANN(ctx);
    ANN(source);
    ANN(derived);

    DvzController* source_panzoom = dvz_panzoom(ctx->scene, NULL);
    DvzController* derived_panzoom = dvz_panzoom(ctx->scene, NULL);
    if (source_panzoom == NULL || derived_panzoom == NULL)
        return false;
    if (dvz_scenario_bind_controller(ctx, source, source_panzoom, DVZ_DIM_MASK_XY) != 0)
        return false;
    if (dvz_scenario_bind_controller(ctx, derived, derived_panzoom, DVZ_DIM_MASK_XY) != 0)
        return false;
    return dvz_controller_link(
               ctx->scene, source_panzoom, derived_panzoom,
               DVZ_CONTROLLER_LINK_EXTENT_X | DVZ_CONTROLLER_LINK_EXTENT_Y,
               DVZ_CONTROLLER_LINK_TWO_WAY) != NULL;
}



/**
 * Queue one source-panel pixel query at the current probe coordinate.
 *
 * @param state workflow state
 */
static void _queue_probe(LinkedProbeState* state)
{
    if (state == NULL || !state->cursor_valid || state->source_panel == NULL)
        return;

    DvzQueryRequest request = dvz_query_request();
    request.request_id = PROBE_REQUEST_ID;
    request.target = DVZ_SCENE_TARGET_PIXEL;
    if (dvz_panel_query_px(state->source_panel, state->cursor_x, state->cursor_y, &request) != 0)
    {
        dvz_fprintf(stderr, "dvz_panel_query_px() failed\n");
    }
}



/**
 * Return whether a query result differs from the last displayed value.
 *
 * @param state workflow state
 * @param query query result
 * @param x normalized data X
 * @param y normalized data Y
 * @return true when the readout should be refreshed
 */
static bool _query_changed(
    LinkedProbeState* state, const DvzQueryResult* query, float x, float y)
{
    if (state == NULL || query == NULL)
        return false;
    if (!state->has_last_result || state->last_hit != query->hit)
        return true;
    if (!query->hit)
        return false;

    const double raw = (double)_sample_measurement(x, y);
    const double derived = (double)_sample_derived(x, y);
    const double raw_delta = fabs(raw - state->last_raw);
    const double derived_delta = fabs(derived - state->last_derived);
    return raw_delta >= 1e-3 || derived_delta >= 1e-3;
}



/**
 * Store the last displayed query result.
 *
 * @param state workflow state
 * @param query query result
 * @param x normalized data X
 * @param y normalized data Y
 */
static void _store_query_result(
    LinkedProbeState* state, const DvzQueryResult* query, float x, float y)
{
    if (state == NULL || query == NULL)
        return;
    state->has_last_result = true;
    state->last_hit = query->hit;
    if (query->hit)
    {
        state->last_raw = (double)_sample_measurement(x, y);
        state->last_derived = (double)_sample_derived(x, y);
    }
}



/**
 * Move both probe markers and queue a source-panel query.
 *
 * @param state workflow state
 * @param x normalized data X
 * @param y normalized data Y
 */
static void _set_probe(LinkedProbeState* state, float x, float y)
{
    if (state == NULL)
        return;

    (void)_update_probe_marker(&state->source_marker, x, y);
    (void)_update_probe_marker(&state->derived_marker, x, y);

    double panel_px[2] = {0};
    if (dvz_panel_data_to_position(
            state->source_panel, DVZ_PANEL_COORD_PANEL_PX, (const double[2]){x, y}, panel_px))
    {
        state->cursor_valid = true;
        state->cursor_x = panel_px[0];
        state->cursor_y = panel_px[1];
        _queue_probe(state);
    }
}



/*************************************************************************************************/
/*  Callbacks                                                                                    */
/*************************************************************************************************/

/**
 * Keep both linked probe markers at the latest cursor coordinate.
 *
 * @param event portable pointer event
 * @param user_data workflow state
 */
static void _linked_probe_pointer(
    const DvzScenarioPointerEvent* event, void* user_data)
{
    LinkedProbeState* state = (LinkedProbeState*)user_data;
    if (state == NULL || event == NULL)
        return;
    if (event->type != DVZ_SCENARIO_POINTER_MOVE && event->type != DVZ_SCENARIO_POINTER_CLICK)
        return;

    double data[2] = {0};
    if (dvz_panel_position_to_data(
            state->source_panel, DVZ_PANEL_COORD_FIGURE_PX,
            (const double[2]){event->x, event->y}, data) ||
        dvz_panel_position_to_data(
            state->derived_panel, DVZ_PANEL_COORD_FIGURE_PX,
            (const double[2]){event->x, event->y}, data))
    {
        if (data[0] >= 0.0 && data[0] <= 1.0 && data[1] >= 0.0 && data[1] <= 1.0)
            _set_probe(state, (float)data[0], (float)data[1]);
    }
}



/**
 * Poll query results and update the retained readout.
 *
 * @param ctx scenario context
 * @param user_data workflow state
 */
static void _linked_probe_post_frame(DvzScenarioContext* ctx, void* user_data)
{
    (void)ctx;
    LinkedProbeState* state = (LinkedProbeState*)user_data;
    if (state == NULL)
        return;

    DvzQueryResult query = {0};
    while (dvz_scene_poll_query(state->scene, &query))
    {
        double data[2] = {0};
        const bool in_panel = dvz_panel_position_to_data(
            state->source_panel, DVZ_PANEL_COORD_PANEL_PX, query.panel_position, data);
        if (
            in_panel && data[0] >= 0.0 && data[0] <= 1.0 && data[1] >= 0.0 &&
            data[1] <= 1.0 && _query_changed(state, &query, (float)data[0], (float)data[1]))
        {
            _update_readout(state, (float)data[0], (float)data[1], query.hit);
            _store_query_result(state, &query, (float)data[0], (float)data[1]);
        }
    }

    _queue_probe(state);
}


/**
 * Handle portable scenario events.
 *
 * @param ctx scenario context
 * @param event portable event
 * @param user scenario state
 */
static void _scenario_event(DvzScenarioContext* ctx, const DvzScenarioEvent* event, void* user)
{
    (void)ctx;
    if (event == NULL)
        return;
    if (event->kind == DVZ_SCENARIO_EVENT_POINTER)
        _linked_probe_pointer(&event->content.pointer, user);
}



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

/**
 * Initialize the linked probe and colorbar workflow scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return whether initialization succeeded
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL)
        return false;
    if (out_user != NULL)
        *out_user = NULL;

    bool ok = false;
    LinkedProbeState* state = (LinkedProbeState*)dvz_calloc(1, sizeof(*state));
    if (state == NULL)
        return false;
    state->scene = ctx->scene;
    if (out_user != NULL)
        *out_user = state;
    state->measurement =
        (float*)dvz_calloc((DvzSize)FIELD_WIDTH * FIELD_HEIGHT, sizeof(float));
    state->derived = (float*)dvz_calloc((DvzSize)FIELD_WIDTH * FIELD_HEIGHT, sizeof(float));
    EXAMPLE_CHECK(
        state->measurement != NULL && state->derived != NULL, "linked field allocation failed");
    _fill_fields(state->measurement, state->derived);
    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    EXAMPLE_CHECK(ctx->figure != NULL, "dvz_figure() failed");

    DvzGrid* grid = dvz_figure_grid(ctx->figure, 1, 2);
    EXAMPLE_CHECK(grid != NULL, "dvz_figure_grid() failed");
    ok = dvz_grid_set_margins(
             grid, &(DvzPanelReserve){.left_px = 42.0f, .right_px = 34.0f, .top_px = 46.0f,
                                      .bottom_px = 34.0f}) == DVZ_OK;
    EXAMPLE_CHECK(ok, "dvz_grid_set_margins() failed");
    ok = dvz_grid_set_gutter(grid, 28.0f, 0.0f) == DVZ_OK;
    EXAMPLE_CHECK(ok, "dvz_grid_set_gutter() failed");

    DvzPanel* source = dvz_grid_panel(grid, 0, 0);
    DvzPanel* derived_panel = dvz_grid_panel(grid, 0, 1);
    EXAMPLE_CHECK(source != NULL && derived_panel != NULL, "dvz_grid_panel() failed");

    example_graphite_cyan_set_panel_background(source);
    example_graphite_cyan_set_panel_background(derived_panel);

    ok = _set_image_domain(source);
    EXAMPLE_CHECK(ok, "_set_image_domain(source) failed");
    ok = _set_image_domain(derived_panel);
    EXAMPLE_CHECK(ok, "_set_image_domain(derived) failed");

    DvzScale* scale = _add_scale(ctx->scene);
    EXAMPLE_CHECK(scale != NULL, "adding shared color scale failed");

    ok = _add_image(ctx->scene, source, scale, state->measurement, true);
    EXAMPLE_CHECK(ok, "adding source image failed");
    ok = _add_image(ctx->scene, derived_panel, scale, state->derived, false);
    EXAMPLE_CHECK(ok, "adding derived image failed");

    ok = _add_axes(source);
    EXAMPLE_CHECK(ok, "_add_axes(source) failed");
    ok = _add_axes(derived_panel);
    EXAMPLE_CHECK(ok, "_add_axes(derived) failed");

    ok = _add_colorbar(derived_panel, scale);
    EXAMPLE_CHECK(ok, "_add_colorbar() failed");

    ProbeMarker source_marker = {0};
    ProbeMarker derived_marker = {0};
    ok = _add_probe_marker(ctx->scene, source, &source_marker);
    EXAMPLE_CHECK(ok, "_add_probe_marker(source) failed");
    ok = _add_probe_marker(ctx->scene, derived_panel, &derived_marker);
    EXAMPLE_CHECK(ok, "_add_probe_marker(derived) failed");

    ok = _add_panel_label(source, "measurement", EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
    EXAMPLE_CHECK(ok, "_add_panel_label(source) failed");
    ok = _add_panel_label(derived_panel, "derived", EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
    EXAMPLE_CHECK(ok, "_add_panel_label(derived) failed");

    DvzText* readout = _add_readout(source);
    EXAMPLE_CHECK(readout != NULL, "_add_readout() failed");

    ok = _bind_linked_panzoom(ctx, source, derived_panel);
    EXAMPLE_CHECK(ok, "_bind_linked_panzoom() failed");

    state->source_panel = source;
    state->derived_panel = derived_panel;
    state->source_marker = source_marker;
    state->derived_marker = derived_marker;
    state->readout = readout;
    _update_readout(state, PROBE_X, PROBE_Y, true);
    _set_probe(state, PROBE_X, PROBE_Y);

    ok = true;
cleanup:
    return ok;
}



/**
 * Destroy the linked probe and colorbar workflow scenario state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    LinkedProbeState* state = (LinkedProbeState*)user;
    if (state == NULL)
        return;
    dvz_free(state->derived);
    dvz_free(state->measurement);
    dvz_free(state);
}



DvzScenarioSpec dvz_showcase_linked_probe_colorbar_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "showcases_linked_probe_colorbar",
        .title = "Linked Probe With Colorbar",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_IMAGE_VISUAL | DVZ_SCENARIO_REQ_MARKER_VISUAL |
                        DVZ_SCENARIO_REQ_TEXT_VISUAL | DVZ_SCENARIO_REQ_QUERY_READBACK |
                        DVZ_SCENARIO_REQ_FRAME_CALLBACKS | DVZ_SCENARIO_REQ_CONTROLLER |
                        DVZ_SCENARIO_REQ_PANZOOM,
        .init = _scenario_init,
        .event = _scenario_event,
        .post_frame = _linked_probe_post_frame,
        .destroy = _scenario_destroy,
    };
}



/**
 * Run the linked probe and colorbar workflow through the native scenario runner.
 *
 * @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_probe_colorbar_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Tags

workflow, image, sampled-field, pixel-query, linked-panzoom, colorbar, readout

Data

Field Value
kind synthetic