Pixel Selection¶
This example queries and selects individual cells in a pixel grid.
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/selection_pixel (build and run), or rerun ./build/examples/c/features/selection_pixel |
| Python | Available; direct-engine adaptation | python3 -m examples.python.gallery.features.selection_pixel |
| 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 40x24 grid uploads position, color, and pixel_size_px arrays generated from a smooth scalar field. Move the cursor over the live panel to query a pixel item; hover scales the square, click toggles a persistent warning-color tint, and clicking the background clears selection. This is useful for heatmaps and rasters where each rendered cell may correspond to a sample, sensor, or bin.
Source¶
#!/usr/bin/env python3
"""Pixel-grid item selection with hover and click feedback."""
from __future__ import annotations
import math
import numpy as np
import datoviz as dvz
from examples.python.gallery import common as ex
GRID_WIDTH = 40
GRID_HEIGHT = 24
PIXEL_COUNT = GRID_WIDTH * GRID_HEIGHT
PIXEL_SIZE = 18.0
QUERY_HOVER_ID = 11
QUERY_CLICK_ID = 12
TAU = 2.0 * math.pi
def _clamp01(value: float) -> float:
return min(max(value, 0.0), 1.0)
def _sample_value(u: float, v: float) -> float:
ridge = 0.5 + 0.5 * math.sin(TAU * (1.7 * u + 0.35 * v))
wave = 0.5 + 0.5 * math.cos(TAU * (0.6 * u - 1.9 * v))
dx = u - 0.34
dy = v - 0.64
blob = math.exp(-(dx * dx + 1.6 * dy * dy) / 0.012)
value = 0.08 + 0.36 * u + 0.18 * v + 0.16 * ridge + 0.10 * wave + 0.28 * blob
return _clamp01(value)
def _mix(a, b, t: float):
return (
int((1.0 - t) * a.r + t * b.r + 0.5),
int((1.0 - t) * a.g + t * b.g + 0.5),
int((1.0 - t) * a.b + t * b.b + 0.5),
245,
)
def _ramp(t: float):
t = _clamp01(t)
c0 = dvz.DvzColor(42, 56, 71, 255)
c1 = ex.CYAN
c2 = ex.GREEN
if t < 0.5:
return _mix(c0, c1, 2.0 * t)
return _mix(c1, c2, 2.0 * (t - 0.5))
def _pixel_data():
positions = np.zeros((PIXEL_COUNT, 3), dtype=np.float32)
colors = np.zeros((PIXEL_COUNT, 4), dtype=np.uint8)
sizes = np.full(PIXEL_COUNT, PIXEL_SIZE, dtype=np.float32)
step_x = 1.82 / (GRID_WIDTH - 1)
step_y = 1.36 / (GRID_HEIGHT - 1)
for y in range(GRID_HEIGHT):
for x in range(GRID_WIDTH):
index = y * GRID_WIDTH + x
u = x / (GRID_WIDTH - 1)
v = y / (GRID_HEIGHT - 1)
positions[index] = (-0.91 + step_x * x, -0.68 + step_y * y, 0.0)
colors[index] = _ramp(_sample_value(u, v))
return positions, colors, sizes
def _add_pixel_grid(scene, panel):
pixel = dvz.dvz_pixel(scene, 0)
if not pixel:
raise RuntimeError("dvz_pixel() failed")
dvz.dvz_visual_set_query_capabilities(pixel, dvz.DVZ_QUERY_CAPABILITY_ITEM)
positions, colors, sizes = _pixel_data()
if dvz.dvz_visual_set_data_many(
pixel,
{
"position": positions,
"color": colors,
"pixel_size_px": sizes,
},
) != 0:
raise RuntimeError("dvz_visual_set_data_many(pixel) failed")
if dvz.dvz_visual_set_depth_test(pixel, False) != 0:
raise RuntimeError("dvz_visual_set_depth_test(pixel) failed")
ex.add_visual(panel, pixel)
return pixel
def _handle_hover(state, query) -> None:
if not state["cursor_valid"]:
return
if ex.query_item_hit(query, dvz.DVZ_SCENE_VISUAL_FAMILY_PIXEL, PIXEL_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 pixel 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_PIXEL, PIXEL_COUNT):
if dvz.dvz_selection_apply_query(state["selection"], query) != 0:
raise RuntimeError("dvz_selection_apply_query() failed")
print(f"toggle pixel id={query.resolved_id}")
else:
dvz.dvz_selection_clear(state["selection"])
def main() -> None:
scene, figure, panel = ex.scene_panel()
_add_pixel_grid(scene, panel)
selection = ex.create_item_selection(scene)
hover = ex.create_item_hover(scene, 1.7)
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,
"Pixel Selection",
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 queries and selects individual cells in a retained pixel grid.
*
* What to look for: the 40x24 grid uploads position, color, and pixel_size_px arrays generated
* from a smooth scalar field. Move the cursor over the live panel to query a pixel item; hover
* scales the square, click toggles a persistent warning-color tint, and clicking the background
* clears selection. This is useful for heatmaps and rasters where each rendered cell may
* correspond to a sample, sensor, or bin.
*
* Scenario: features_selection_pixel
* Style: features, graphite_cyan, 1280x720 window target
*
* Build: just example-c features/selection_pixel
* Run: ./build/examples/c/features/selection_pixel --live
* Smoke: ./build/examples/c/features/selection_pixel --png
*/
/*************************************************************************************************/
/* Includes */
/*************************************************************************************************/
#include <math.h>
#include <stdbool.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include "_alloc.h"
#include "_assertions.h"
#include "datoviz/scene.h"
#include "example_style.h"
#include "runner/scenario_runner.h"
DvzScenarioSpec dvz_example_selection_pixel_scenario(void);
/*************************************************************************************************/
/* Constants */
/*************************************************************************************************/
#define WIDTH EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define GRID_WIDTH 40u
#define GRID_HEIGHT 24u
#define PIXEL_COUNT (GRID_WIDTH * GRID_HEIGHT)
#define PIXEL_SIZE 18.0f
#define QUERY_HOVER_ID 11u
#define QUERY_CLICK_ID 12u
static const float TAU = 6.28318530718f;
/*************************************************************************************************/
/* Structs */
/*************************************************************************************************/
typedef struct PixelSelectionState
{
DvzScene* scene;
DvzPanel* panel;
DvzSelection* selection;
DvzHover* hover;
vec3* positions;
DvzColor* colors;
float* sizes;
DvzQueryResult latest_hover_query;
bool has_hover_query;
bool cursor_valid;
} PixelSelectionState;
/*************************************************************************************************/
/* Helpers */
/*************************************************************************************************/
/**
* Clamp one value to the unit interval.
*
* @param x value
* @return clamped value
*/
static float _clamp01(float x)
{
return x < 0.0f ? 0.0f : x > 1.0f ? 1.0f : x;
}
/**
* Return one deterministic scalar sample for the pixel grid.
*
* @param u normalized grid x coordinate
* @param v normalized grid y coordinate
* @return normalized scalar value
*/
static float _sample_value(float u, float v)
{
const float ridge = 0.5f + 0.5f * sinf(TAU * (1.7f * u + 0.35f * v));
const float wave = 0.5f + 0.5f * cosf(TAU * (0.6f * u - 1.9f * v));
const float dx = u - 0.34f;
const float dy = v - 0.64f;
const float blob = expf(-(dx * dx + 1.6f * dy * dy) / 0.012f);
const float value = 0.08f + 0.36f * u + 0.18f * v + 0.16f * ridge + 0.10f * wave +
0.28f * blob;
return _clamp01(value);
}
/**
* Return a graphite-cyan ramp sample.
*
* @param t normalized scalar value
* @return RGBA8 color
*/
static DvzColor _ramp(float t)
{
t = _clamp01(t);
const DvzColor c0 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
const DvzColor c1 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
const DvzColor c2 = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
const DvzColor a = t < 0.5f ? c0 : c1;
const DvzColor b = t < 0.5f ? c1 : c2;
const float s = t < 0.5f ? 2.0f * t : 2.0f * (t - 0.5f);
return dvz_color_rgba(
(uint8_t)((1.0f - s) * (float)a.r + s * (float)b.r + 0.5f),
(uint8_t)((1.0f - s) * (float)a.g + s * (float)b.g + 0.5f),
(uint8_t)((1.0f - s) * (float)a.b + s * (float)b.b + 0.5f), 245u);
}
/**
* Fill the deterministic pixel grid.
*
* @param positions output pixel positions
* @param colors output pixel colors
* @param sizes output pixel sprite sizes in pixels
*/
static void _fill_pixels(
vec3 positions[PIXEL_COUNT], DvzColor colors[PIXEL_COUNT], float sizes[PIXEL_COUNT])
{
ANN(positions);
ANN(colors);
ANN(sizes);
const float step_x = 1.82f / (float)(GRID_WIDTH - 1u);
const float step_y = 1.36f / (float)(GRID_HEIGHT - 1u);
for (uint32_t y = 0; y < GRID_HEIGHT; y++)
{
for (uint32_t x = 0; x < GRID_WIDTH; x++)
{
const uint32_t i = y * GRID_WIDTH + x;
const float u = (float)x / (float)(GRID_WIDTH - 1u);
const float v = (float)y / (float)(GRID_HEIGHT - 1u);
const float value = _sample_value(u, v);
positions[i][0] = -0.91f + step_x * (float)x;
positions[i][1] = -0.68f + step_y * (float)y;
positions[i][2] = 0.0f;
colors[i] = _ramp(value);
sizes[i] = PIXEL_SIZE;
}
}
}
/**
* Free the retained state buffers.
*
* @param state pixel selection state
*/
static void _free_state(PixelSelectionState* state)
{
if (state == NULL)
return;
dvz_free(state->sizes);
dvz_free(state->colors);
dvz_free(state->positions);
dvz_free(state);
}
/**
* Toggle retained selection for one queried pixel.
*
* @param state pixel-selection example state
* @param query pixel item query result
*/
static void _toggle_pixel_selection(PixelSelectionState* 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_PIXEL ||
query->resolved_target != DVZ_SCENE_TARGET_ITEM || query->resolved_id >= PIXEL_COUNT)
return;
if (dvz_selection_apply_query(state->selection, query) != 0)
fprintf(stderr, "dvz_selection_apply_query() failed\n");
fprintf(stdout, "toggle pixel 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 pixel-selection example state
*/
static void _selection_pixel_pointer(const DvzScenarioPointerEvent* event, void* user_data)
{
PixelSelectionState* state = (PixelSelectionState*)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 pixel query results and update hover styling.
*
* @param ctx scenario context
* @param user_data pixel-selection example state
*/
static void _selection_pixel_post_frame(DvzScenarioContext* ctx, void* user_data)
{
(void)ctx;
PixelSelectionState* state = (PixelSelectionState*)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_PIXEL &&
query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < PIXEL_COUNT)
{
_toggle_pixel_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_PIXEL &&
query.resolved_target == DVZ_SCENE_TARGET_ITEM && query.resolved_id < PIXEL_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 pixel 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)
_selection_pixel_pointer(&event->content.pointer, user);
}
/*************************************************************************************************/
/* Scenario callbacks */
/*************************************************************************************************/
/**
* Initialize the retained pixel selection 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;
PixelSelectionState* state = (PixelSelectionState*)dvz_calloc(1, sizeof(*state));
if (state == NULL)
return false;
state->positions = (vec3*)dvz_calloc(PIXEL_COUNT, sizeof(*state->positions));
state->colors = (DvzColor*)dvz_calloc(PIXEL_COUNT, sizeof(*state->colors));
state->sizes = (float*)dvz_calloc(PIXEL_COUNT, sizeof(*state->sizes));
if (state->positions == NULL || state->colors == NULL || state->sizes == NULL)
goto error;
_fill_pixels(state->positions, state->colors, state->sizes);
ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
if (ctx->figure == NULL)
goto error;
DvzPanel* panel = dvz_panel_full(ctx->figure);
if (panel == NULL)
goto error;
example_graphite_cyan_set_panel_background(panel);
DvzVisual* visual = dvz_pixel(ctx->scene, 0);
if (visual == NULL)
goto error;
dvz_visual_set_query_capabilities(visual, DVZ_QUERY_CAPABILITY_ITEM);
DvzSelection* selection = dvz_selection(
ctx->scene,
&(DvzSelectionDesc){
DVZ_STRUCT_INIT_FIELDS(DvzSelectionDesc),
.mode = DVZ_SELECT_TOGGLE,
.target = DVZ_SCENE_TARGET_ITEM,
});
if (selection == NULL)
goto error;
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)
goto error;
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)
goto error;
DvzItemStateVisualStyle hover_style = dvz_item_state_visual_style();
hover_style.visual_flags = DVZ_ITEM_STATE_VISUAL_SCALE;
hover_style.scale = 1.7f;
if (dvz_hover_set_visual_style(hover, &hover_style) != 0)
goto error;
DvzVisualDataUpdate updates[] = {
{.attr_name = "position", .data = state->positions, .item_count = PIXEL_COUNT},
{.attr_name = "color", .data = state->colors, .item_count = PIXEL_COUNT},
{.attr_name = "pixel_size_px", .data = state->sizes, .item_count = PIXEL_COUNT},
};
if (dvz_visual_set_data_many(visual, updates, 3) != 0)
goto error;
if (dvz_visual_set_depth_test(visual, false) != 0)
goto error;
if (dvz_panel_add_visual(panel, visual, NULL) != 0)
goto error;
DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, NULL, DVZ_DIM_MASK_XY);
if (panzoom == NULL)
goto error;
state->scene = ctx->scene;
state->panel = panel;
state->selection = selection;
state->hover = hover;
if (out_user != NULL)
*out_user = state;
return true;
error:
_free_state(state);
return false;
}
/**
* Destroy the retained pixel selection feature scenario state.
*
* @param ctx scenario context
* @param user scenario state
*/
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
(void)ctx;
_free_state((PixelSelectionState*)user);
}
/**
* Return the retained pixel selection scenario specification.
*
* @return scenario specification
*/
DvzScenarioSpec dvz_example_selection_pixel_scenario(void)
{
return (DvzScenarioSpec){
.id = "features_selection_pixel",
.title = "Pixel Selection",
.width = WIDTH,
.height = HEIGHT,
.fps = 60.0,
.requirements = DVZ_SCENARIO_REQ_PIXEL_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 = _selection_pixel_post_frame,
.destroy = _scenario_destroy,
};
}
/*************************************************************************************************/
/* Functions */
/*************************************************************************************************/
#ifndef DVZ_EXAMPLE_NO_MAIN
/**
* Run the retained pixel selection feature example through the native scenario runner.
*
* @param argc command-line argument count
* @param argv command-line argument vector
* @return process exit code
*/
int main(int argc, char** argv)
{
DvzScenarioSpec spec = dvz_example_selection_pixel_scenario();
return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details
- ID:
features_selection_pixel - Category:
feature - Lane:
features - Status:
supported - Source:
examples/c/features/selection_pixel.c - Approved adaptation starter:
yes - Python source:
examples/python/gallery/features/selection_pixel.py - Python adaptation: Available; direct-engine adaptation
- Browser support: Live in browser
- WebGPU live route:
examples/webgpu/live.html?id=features_selection_pixel - Browser capability tags:
pixel,controller,panzoom,query-readback,frame-callbacks - Validation:
smoke+interaction+screenshot
Data
| Field | Value |
|---|---|
kind |
synthetic |