Skip to content

Picking

This example demonstrates item-level marker hover and click selection.

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 features/picking (build and run), or rerun ./build/examples/c/features/picking
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.picking
Browser Live WebGPU route Open live example

This example is approved as a starting point for user code and coding agents. Keep the object lifetimes and data shapes intact while adapting the data and styling.

What To Look For

The marker grid uploads position, color, diameter_px, angle, and shape arrays, including several deliberately overlapping markers near the center. Move the cursor to query the frontmost marker item; hover scales the item, click toggles a persistent warning-color tint, and clicking the background clears selection. Current marker queries use marker-family bounds rather than exact SDF shape discard, which is enough to demonstrate interactive item picking.

Source

#!/usr/bin/env python3
"""Marker item picking with hover and click selection."""

from __future__ import annotations

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


GRID_COLS = 9
GRID_ROWS = 6
MARKER_COUNT = GRID_COLS * GRID_ROWS + 5
BASE_SIZE = 34.0
HOVER_SIZE = 50.0
QUERY_HOVER_ID = 1
QUERY_CLICK_ID = 2


def _marker_shape(index: int) -> int:
    shapes = (
        dvz.DVZ_MARKER_SHAPE_DISC,
        dvz.DVZ_MARKER_SHAPE_SQUARE,
        dvz.DVZ_MARKER_SHAPE_TRIANGLE,
        dvz.DVZ_MARKER_SHAPE_DIAMOND,
        dvz.DVZ_MARKER_SHAPE_CROSS,
        dvz.DVZ_MARKER_SHAPE_RING,
    )
    return shapes[index % len(shapes)]


def _marker_palette(index: int):
    palette = (ex.CYAN, ex.GREEN, ex.TEXT)
    color = palette[index % len(palette)]
    return color.r, color.g, color.b, 245


def _marker_data():
    positions = np.zeros((MARKER_COUNT, 3), dtype=np.float32)
    colors = np.zeros((MARKER_COUNT, 4), dtype=np.uint8)
    diameters = np.zeros(MARKER_COUNT, dtype=np.float32)
    angles = np.zeros(MARKER_COUNT, dtype=np.float32)
    shapes = np.zeros(MARKER_COUNT, dtype=np.uint32)

    for row in range(GRID_ROWS):
        for col in range(GRID_COLS):
            index = row * GRID_COLS + col
            positions[index] = (
                -0.88 + 1.76 * (col / (GRID_COLS - 1)),
                -0.72 + 1.44 * (row / (GRID_ROWS - 1)),
                0.0,
            )
            colors[index] = _marker_palette(index)
            diameters[index] = BASE_SIZE
            angles[index] = ((index % 12) / 12.0) * 2.0 * np.pi
            shapes[index] = _marker_shape(index)

    for i in range(5):
        index = GRID_COLS * GRID_ROWS + i
        positions[index] = (-0.05 + 0.025 * i, -0.02 + 0.020 * (i % 3), 0.0)
        colors[index] = (ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, 245)
        diameters[index] = BASE_SIZE
        angles[index] = 0.25 * np.pi * i
        shapes[index] = _marker_shape(index + 2)

    return positions, colors, diameters, angles.astype(np.float32), shapes


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

    style = dvz.dvz_marker_style()
    style.aspect = dvz.DVZ_SHAPE_ASPECT_OUTLINE
    style.edge_color = dvz.DvzColor(ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, 210)
    style.stroke_width_px = 2.0
    if dvz.dvz_marker_set_style(marker, style) != 0:
        raise RuntimeError("dvz_marker_set_style() failed")

    positions, colors, diameters, angles, shapes = _marker_data()
    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")
    ex.add_visual(panel, marker)
    return marker


def _handle_hover(state, query) -> None:
    if not state["cursor_valid"]:
        return
    if ex.query_item_hit(query, dvz.DVZ_SCENE_VISUAL_FAMILY_MARKER, MARKER_COUNT):
        latest = state["latest_hover_query"]
        same_item = (
            latest is not None
            and latest.visual_id == query.visual_id
            and latest.resolved_id == query.resolved_id
        )
        if not same_item:
            if dvz.dvz_hover_apply_query(state["hover"], query) != 0:
                raise RuntimeError("dvz_hover_apply_query() failed")
            print(f"hover marker id={query.resolved_id}")
        state["latest_hover_query"] = query
        state["has_hover_query"] = True
    else:
        if state["has_hover_query"]:
            dvz.dvz_hover_clear(state["hover"])
        state["latest_hover_query"] = None
        state["has_hover_query"] = False


def _handle_click(state, query) -> None:
    if ex.query_item_hit(query, dvz.DVZ_SCENE_VISUAL_FAMILY_MARKER, MARKER_COUNT):
        if dvz.dvz_selection_apply_query(state["selection"], query) != 0:
            raise RuntimeError("dvz_selection_apply_query() failed")
        print(f"toggle marker id={query.resolved_id}")
    else:
        dvz.dvz_selection_clear(state["selection"])


def main() -> None:
    scene, figure, panel = ex.scene_panel()
    _add_marker_grid(scene, panel)
    selection = ex.create_item_selection(scene)
    hover = ex.create_item_hover(scene, HOVER_SIZE / BASE_SIZE)

    state = {
        "selection": selection,
        "hover": hover,
        "cursor_valid": False,
        "has_hover_query": False,
        "latest_hover_query": None,
    }

    def configure_view(view) -> None:
        ex.bind_panzoom(view, scene, panel, dvz.DVZ_DIM_MASK_XY)

    def on_pointer(event) -> None:
        if event.type not in (
            dvz.DVZ_POINTER_EVENT_MOVE,
            dvz.DVZ_POINTER_EVENT_PRESS,
            dvz.DVZ_POINTER_EVENT_CLICK,
        ):
            return

        panel_pos = ex.figure_to_panel_px(panel, event.pos[0], event.pos[1])
        state["cursor_valid"] = panel_pos is not None
        if panel_pos is None:
            if state["has_hover_query"]:
                dvz.dvz_hover_clear(hover)
                state["has_hover_query"] = False
            if event.type in (dvz.DVZ_POINTER_EVENT_PRESS, dvz.DVZ_POINTER_EVENT_CLICK):
                dvz.dvz_selection_clear(selection)
            return

        if event.type == dvz.DVZ_POINTER_EVENT_MOVE:
            ex.queue_panel_query(
                panel,
                panel_pos[0],
                panel_pos[1],
                QUERY_HOVER_ID,
                dvz.DVZ_SCENE_TARGET_ITEM,
                hit_policy=dvz.DVZ_QUERY_HIT_FRONTMOST,
            )
        elif event.button == dvz.DVZ_POINTER_BUTTON_LEFT:
            ex.queue_panel_query(
                panel,
                panel_pos[0],
                panel_pos[1],
                QUERY_CLICK_ID,
                dvz.DVZ_SCENE_TARGET_ITEM,
                hit_policy=dvz.DVZ_QUERY_HIT_FRONTMOST,
            )

    def on_frame(_view, _frame_index: int, _elapsed: float) -> None:
        for query in ex.poll_queries(scene):
            if query.request_id == QUERY_CLICK_ID:
                _handle_click(state, query)
            elif query.request_id == QUERY_HOVER_ID:
                _handle_hover(state, query)

    ex.run_with_input_callbacks(
        scene,
        figure,
        "Picking",
        on_pointer=on_pointer,
        on_frame=on_frame,
        configure_view=configure_view,
    )


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
 */

/* This example demonstrates item-level marker hover and click selection.
 *
 * What to look for: the marker grid uploads position, color, diameter_px, angle, and shape arrays,
 * including several deliberately overlapping markers near the center. Move the cursor to query the
 * frontmost marker item; hover scales the item, click toggles a persistent warning-color tint, and
 * clicking the background clears selection. Current marker queries use marker-family bounds rather
 * than exact SDF shape discard, which is enough to demonstrate interactive item picking.
 *
 * Scenario: features_picking
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/picking
 * Run:    ./build/examples/c/features/picking --live
 * Smoke:  ./build/examples/c/features/picking --png
 */

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

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



DvzScenarioSpec dvz_example_picking_scenario(void);


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

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define GRID_COLS    9
#define GRID_ROWS    6
#define MARKER_COUNT (GRID_COLS * GRID_ROWS + 5)
#define BASE_SIZE    34.0f
#define HOVER_SIZE   50.0f
#define QUERY_HOVER_ID 1u
#define QUERY_CLICK_ID 2u
#define PI_F         3.14159265358979323846f



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

typedef struct PickingState PickingState;

struct PickingState
{
    DvzScene* scene;
    DvzPanel* panel;
    DvzSelection* selection;
    DvzHover* hover;
    DvzQueryResult latest_hover_query;
    bool has_hover_query;
    bool cursor_valid;
};



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

/**
 * Return a deterministic mixed marker shape.
 *
 * @param index marker index
 * @return marker shape enum value
 */
static uint32_t _marker_shape(uint32_t index)
{
    switch (index % 6u)
    {
    case 1:
        return DVZ_MARKER_SHAPE_SQUARE;
    case 2:
        return DVZ_MARKER_SHAPE_TRIANGLE;
    case 3:
        return DVZ_MARKER_SHAPE_DIAMOND;
    case 4:
        return DVZ_MARKER_SHAPE_CROSS;
    case 5:
        return DVZ_MARKER_SHAPE_RING;
    default:
        return DVZ_MARKER_SHAPE_DISC;
    }
}



/**
 * Return one deterministic marker color from the shared graphite-cyan palette.
 *
 * @param index marker index
 * @return marker color
 */
static DvzColor _marker_palette_color(uint32_t index)
{
    const ExampleStyleColorRole roles[] = {
        EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY,
        EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY,
        EXAMPLE_STYLE_COLOR_TEXT,
    };
    DvzColor color = example_graphite_cyan_color(roles[index % DVZ_ARRAY_COUNT(roles)]);
    color.a = 245u;
    return color;
}



/**
 * Toggle retained selection for one queried marker.
 *
 * @param state marker-pick example state
 * @param query marker item query result
 */
static void _toggle_marker_selection(PickingState* state, const DvzQueryResult* query)
{
    if (state == NULL || query == NULL)
        return;
    if (
        query->status != DVZ_QUERY_STATUS_HIT || !query->hit ||
        query->visual_family != DVZ_SCENE_VISUAL_FAMILY_MARKER ||
        query->resolved_target != DVZ_SCENE_TARGET_ITEM || query->resolved_id >= MARKER_COUNT)
        return;

    if (dvz_selection_apply_query(state->selection, query) != 0)
        fprintf(stderr, "dvz_selection_apply_query() failed\n");
    fprintf(stdout, "toggle marker id=%" PRIu64 "\n", query->resolved_id);
}



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

/**
 * Queue hover and click queries from panel-local pointer events.
 *
 * @param event portable pointer event
 * @param user_data marker-pick example state
 */
static void _picking_pointer(const DvzScenarioPointerEvent* event, void* user_data)
{
    PickingState* state = (PickingState*)user_data;
    if (state == NULL || event == NULL)
        return;
    if (
        event->type != DVZ_SCENARIO_POINTER_MOVE && event->type != DVZ_SCENARIO_POINTER_CLICK)
        return;

    double x = 0.0;
    double y = 0.0;
    double panel_pos[2] = {0};
    state->cursor_valid = dvz_panel_transform_point(
        state->panel, DVZ_PANEL_COORD_FIGURE_PX, DVZ_PANEL_COORD_PANEL_PX,
        (const double[2]){event->x, event->y}, panel_pos);
    x = panel_pos[0];
    y = panel_pos[1];
    if (!state->cursor_valid)
    {
        if (state->has_hover_query)
        {
            dvz_hover_clear(state->hover);
            state->has_hover_query = false;
        }
        if (event->type == DVZ_SCENARIO_POINTER_CLICK && event->button == DVZ_POINTER_BUTTON_LEFT)
            dvz_selection_clear(state->selection);
        return;
    }

    DvzQueryRequest request = dvz_query_request();
    request.target = DVZ_SCENE_TARGET_ITEM;
    request.hit_policy = DVZ_QUERY_HIT_FRONTMOST;
    if (event->type == DVZ_SCENARIO_POINTER_MOVE)
    {
        request.request_id = QUERY_HOVER_ID;
        if (dvz_panel_query_px(state->panel, x, y, &request) != 0)
            fprintf(stderr, "dvz_panel_query_px() failed\n");
    }
    else if (event->type == DVZ_SCENARIO_POINTER_CLICK && event->button == DVZ_POINTER_BUTTON_LEFT)
    {
        request.request_id = QUERY_CLICK_ID;
        if (dvz_panel_query_px(state->panel, x, y, &request) != 0)
            fprintf(stderr, "dvz_panel_query_px(click) failed\n");
    }
}



/**
 * Consume marker query results and update hover styling.
 *
 * @param ctx scenario context
 * @param user_data marker-pick example state
 */
static void _picking_post_frame(DvzScenarioContext* ctx, void* user_data)
{
    (void)ctx;
    PickingState* state = (PickingState*)user_data;
    if (state == NULL)
        return;

    DvzQueryResult query = {0};
    while (dvz_scene_poll_query(state->scene, &query))
    {
        if (query.request_id == QUERY_CLICK_ID)
        {
            if (
                query.status == DVZ_QUERY_STATUS_HIT && query.hit &&
                query.visual_family == DVZ_SCENE_VISUAL_FAMILY_MARKER &&
                query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < MARKER_COUNT)
            {
                _toggle_marker_selection(state, &query);
            }
            else
            {
                dvz_selection_clear(state->selection);
            }
            continue;
        }
        if (query.request_id != QUERY_HOVER_ID)
            continue;
        if (!state->cursor_valid)
            continue;

        if (
            query.status == DVZ_QUERY_STATUS_HIT && query.hit &&
            query.visual_family == DVZ_SCENE_VISUAL_FAMILY_MARKER &&
            query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < MARKER_COUNT)
        {
            bool same_item =
                state->latest_hover_query.visual_id == query.visual_id &&
                state->latest_hover_query.resolved_id == query.resolved_id;
            bool hover_changed = !state->has_hover_query || !same_item;
            if (hover_changed && dvz_hover_apply_query(state->hover, &query) != 0)
                fprintf(stderr, "dvz_hover_apply_query() failed\n");
            if (!same_item)
                fprintf(stdout, "hover marker id=%" PRIu64 "\n", query.resolved_id);
            state->has_hover_query = true;
            state->latest_hover_query = query;
        }
        else
        {
            if (state->has_hover_query)
                dvz_hover_clear(state->hover);
            state->has_hover_query = false;
        }
    }
}



/**
 * 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)
        _picking_pointer(&event->content.pointer, user);
}



/*************************************************************************************************/
/*  Scenario callbacks                                                                           */
/*************************************************************************************************/

/**
 * Initialize the retained marker picking feature scenario.
 *
 * @param ctx scenario context
 * @param out_user scenario state output
 * @return true on success
 */
static bool _scenario_init(DvzScenarioContext* ctx, void** out_user)
{
    if (ctx == NULL)
        return false;
    if (out_user != NULL)
        *out_user = NULL;

    PickingState* state = (PickingState*)dvz_calloc(1, sizeof(*state));
    if (state == NULL)
        return false;
    if (out_user != NULL)
        *out_user = state;

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    if (ctx->figure == NULL)
        return false;

    DvzPanel* panel = dvz_panel_full(ctx->figure);
    if (panel == NULL)
        return false;
    example_graphite_cyan_set_panel_background(panel);

    DvzVisual* visual = dvz_marker(ctx->scene, 0);
    if (visual == NULL)
        return false;
    dvz_visual_set_query_capabilities(visual, DVZ_QUERY_CAPABILITY_ITEM);

    DvzMarkerStyle style = dvz_marker_style();
    style.aspect = DVZ_SHAPE_ASPECT_OUTLINE;
    DvzColor grid = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
    style.edge_color = dvz_color_rgba(grid.r, grid.g, grid.b, 210);
    style.stroke_width_px = 2.0f;
    if (dvz_marker_set_style(visual, &style) != 0)
        return false;

    DvzSelection* selection = dvz_selection(
        ctx->scene,
        &(DvzSelectionDesc){
            DVZ_STRUCT_INIT_FIELDS(DvzSelectionDesc),
            .mode = DVZ_SELECT_TOGGLE,
            .target = DVZ_SCENE_TARGET_ITEM,
        });
    if (selection == NULL)
        return false;
    DvzSelectionVisualStyle selection_style = dvz_selection_visual_style();
    selection_style.selected_visual_flags = DVZ_ITEM_STATE_VISUAL_TINT;
    selection_style.selected_tint = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_WARNING);
    selection_style.selected_tint_mix = 1.0f;
    selection_style.unselected_visual_flags = DVZ_ITEM_STATE_VISUAL_NONE;
    if (dvz_selection_set_visual_style(selection, &selection_style) != 0)
        return false;

    DvzHover* hover = dvz_hover(
        ctx->scene,
        &(DvzHoverDesc){
            DVZ_STRUCT_INIT_FIELDS(DvzHoverDesc),
            .target = DVZ_SCENE_TARGET_ITEM,
            .hit_policy = DVZ_QUERY_HIT_FRONTMOST,
        });
    if (hover == NULL)
        return false;
    DvzItemStateVisualStyle hover_style = dvz_item_state_visual_style();
    hover_style.visual_flags = DVZ_ITEM_STATE_VISUAL_SCALE;
    hover_style.scale = HOVER_SIZE / BASE_SIZE;
    if (dvz_hover_set_visual_style(hover, &hover_style) != 0)
        return false;

    state->scene = ctx->scene;
    state->panel = panel;
    state->selection = selection;
    state->hover = hover;

    vec3 positions[MARKER_COUNT] = {0};
    DvzColor colors[MARKER_COUNT] = {0};
    float diameters[MARKER_COUNT] = {0};
    float angles[MARKER_COUNT] = {0};
    uint32_t shapes[MARKER_COUNT] = {0};

    for (uint32_t row = 0; row < GRID_ROWS; row++)
    {
        for (uint32_t col = 0; col < GRID_COLS; col++)
        {
            uint32_t index = row * GRID_COLS + col;
            positions[index][0] = -0.88f + 1.76f * ((float)col / (float)(GRID_COLS - 1));
            positions[index][1] = -0.72f + 1.44f * ((float)row / (float)(GRID_ROWS - 1));
            positions[index][2] = 0.0f;
            colors[index] = _marker_palette_color(index);
            diameters[index] = BASE_SIZE;
            angles[index] = ((float)(index % 12u) / 12.0f) * 2.0f * PI_F;
            shapes[index] = _marker_shape(index);
        }
    }

    for (uint32_t i = 0; i < 5; i++)
    {
        uint32_t index = GRID_COLS * GRID_ROWS + i;
        positions[index][0] = -0.05f + 0.025f * (float)i;
        positions[index][1] = -0.02f + 0.020f * (float)(i % 3u);
        positions[index][2] = 0.0f;
        colors[index] = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
        diameters[index] = BASE_SIZE;
        angles[index] = 0.25f * PI_F * (float)i;
        shapes[index] = _marker_shape(index + 2u);
    }

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = MARKER_COUNT},
        {.attr_name = "color", .data = colors, .item_count = MARKER_COUNT},
        {.attr_name = "diameter_px", .data = diameters, .item_count = MARKER_COUNT},
        {.attr_name = "angle", .data = angles, .item_count = MARKER_COUNT},
        {.attr_name = "shape", .data = shapes, .item_count = MARKER_COUNT},
    };
    if (dvz_visual_set_data_many(visual, updates, 5) != 0)
        return false;
    if (dvz_panel_add_visual(panel, visual, NULL) != 0)
        return false;

    DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, NULL, DVZ_DIM_MASK_XY);
    return panzoom != NULL;
}



/**
 * Destroy the retained marker picking feature 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 retained marker picking scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_picking_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_picking",
        .title = "Picking",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_MARKER_VISUAL | DVZ_SCENARIO_REQ_QUERY_READBACK |
                        DVZ_SCENARIO_REQ_CONTROLLER | DVZ_SCENARIO_REQ_PANZOOM |
                        DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .event = _scenario_event,
        .post_frame = _picking_post_frame,
        .destroy = _scenario_destroy,
    };
}



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

/**
 * Run the retained marker picking and selection feature example 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_example_picking_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Data

Field Value
kind synthetic