Skip to content

Visual Transform

This example compares raw point data with a visual-local affine transform.

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/visual_transform (build and run), or rerun ./build/examples/c/features/visual_transform
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.visual_transform
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

Both panels upload the same five point positions, colors, and diameter_px values, but the right visual also receives a transform matrix with translation, shear, and non-uniform scale. Pan or zoom each panel and compare the unchanged data pattern on the left with the transformed copy on the right. Visual transforms are useful for positioning repeated glyph groups or derived overlays without rewriting every data point.

Source

#!/usr/bin/env python3
"""Compare raw point data with a visual-local transform."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


POINT_COUNT = 5
Mat4 = (ctypes.c_float * 4) * 4


def _transform(x: float, y: float):
    mat = Mat4()
    values = (
        (1.24, 0.22, 0.0, 0.0),
        (-0.18, 0.82, 0.0, 0.0),
        (0.0, 0.0, 1.0, 0.0),
        (x, y, 0.0, 1.0),
    )
    for i, row in enumerate(values):
        for j, value in enumerate(row):
            mat[i][j] = value
    return mat


def _add_panel_points(scene, panel, color, transformed: bool) -> None:
    positions = np.array(
        [
            [-0.46, -0.24, 0.0],
            [-0.18, +0.24, 0.0],
            [+0.00, -0.08, 0.0],
            [+0.24, +0.30, 0.0],
            [+0.46, -0.18, 0.0],
        ],
        dtype=np.float32,
    )
    colors = ex.color_array(*(color for _ in range(POINT_COUNT)))
    diameters = np.array([28.0, 40.0, 24.0, 44.0, 30.0], dtype=np.float32)

    point = ex.add_points(scene, panel, positions, colors, diameters)
    if transformed:
        if dvz.dvz_visual_set_transform(point, _transform(0.16, 0.18)) != 0:
            raise RuntimeError("dvz_visual_set_transform() failed")
        if not dvz.dvz_visual_has_transform(point):
            raise RuntimeError("dvz_visual_has_transform() failed")


def main() -> None:
    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")

    base = ex.panel_rect(figure, 0.06, 0.10, 0.41, 0.80)
    transformed = ex.panel_rect(figure, 0.53, 0.10, 0.41, 0.80)
    muted = dvz.DvzColor(ex.TEXT.r, ex.TEXT.g, ex.TEXT.b, 210)
    _add_panel_points(scene, base, muted, False)
    _add_panel_points(scene, transformed, ex.CYAN, True)

    def configure(view) -> None:
        ex.bind_panzoom(view, scene, base, dvz.DVZ_DIM_MASK_XY)
        ex.bind_panzoom(view, scene, transformed, dvz.DVZ_DIM_MASK_XY)

    ex.run_with_view(scene, figure, "Visual Transform", 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
 */

/* This example compares raw point data with a visual-local affine transform.
 *
 * What to look for: both panels upload the same five point positions, colors, and diameter_px
 * values, but the right visual also receives a transform matrix with translation, shear, and
 * non-uniform scale. Pan or zoom each panel and compare the unchanged data pattern on the left
 * with the transformed copy on the right. Visual transforms are useful for positioning repeated
 * glyph groups or derived overlays without rewriting every data point.
 *
 * Scenario: features_visual_transform
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/visual_transform
 * Run:    ./build/examples/c/features/visual_transform --live
 * Smoke:  ./build/examples/c/features/visual_transform --png
 */



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

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

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



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

#define WIDTH  EXAMPLE_WINDOW_WIDTH
#define HEIGHT EXAMPLE_WINDOW_HEIGHT
#define POINT_COUNT 5u



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

DvzScenarioSpec dvz_example_visual_transform_scenario(void);



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

/**
 * Upload one retained point visual.
 *
 * @param visual point visual
 * @param color point color
 * @return true when data and style were accepted
 */
static bool _upload_points(DvzVisual* visual, DvzColor color)
{
    ANN(visual);

    const vec3 positions[POINT_COUNT] = {
        {-0.46f, -0.24f, 0.0f}, {-0.18f, +0.24f, 0.0f}, {+0.00f, -0.08f, 0.0f},
        {+0.24f, +0.30f, 0.0f}, {+0.46f, -0.18f, 0.0f},
    };
    DvzColor colors[POINT_COUNT] = {{0}};
    float diameters[POINT_COUNT] = {28.0f, 40.0f, 24.0f, 44.0f, 30.0f};
    for (uint32_t i = 0; i < POINT_COUNT; i++)
        colors[i] = color;

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .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;
    style.stroke_width_px = 0.0f;
    if (dvz_point_set_style(visual, &style) != 0)
        return false;
    return dvz_visual_set_depth_test(visual, false) == 0;
}



/**
 * Return a simple affine transform with translation and non-uniform scale.
 *
 * @param x X translation
 * @param y Y translation
 * @param out output matrix
 */
static void _example_transform(float x, float y, mat4 out)
{
    ANN(out);

    out[0][0] = 1.24f;
    out[0][1] = 0.22f;
    out[0][2] = 0.0f;
    out[0][3] = 0.0f;
    out[1][0] = -0.18f;
    out[1][1] = 0.82f;
    out[1][2] = 0.0f;
    out[1][3] = 0.0f;
    out[2][0] = 0.0f;
    out[2][1] = 0.0f;
    out[2][2] = 1.0f;
    out[2][3] = 0.0f;
    out[3][0] = x;
    out[3][1] = y;
    out[3][2] = 0.0f;
    out[3][3] = 1.0f;
}


/**
 * Add one point visual to a panel, optionally with a visual-local transform.
 *
 * @param scene scene owning the visual
 * @param panel panel receiving the visual
 * @param transformed whether to apply the example transform
 * @return true on success
 */
static bool _add_panel_points(DvzScene* scene, DvzPanel* panel, bool transformed)
{
    ANN(scene);
    ANN(panel);

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

    DvzColor color = transformed ? example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY)
                                 : example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_GRID);
    if (!transformed)
        color.a = 210u;
    if (!_upload_points(visual, color))
        return false;

    if (transformed)
    {
        mat4 transform = {{0}};
        mat4 current = {{0}};
        _example_transform(0.16f, 0.18f, transform);
        if (dvz_visual_set_transform(visual, transform) != 0)
            return false;
        if (!dvz_visual_has_transform(visual))
            return false;
        if (dvz_visual_get_transform(visual, current) != 0)
            return false;
    }

    return dvz_panel_add_visual(panel, visual, NULL) == 0;
}



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

/**
 * Initialize the visual-transform feature example.
 *
 * @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;

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

    DvzGrid* grid = dvz_figure_grid(ctx->figure, 1, 2);
    if (grid == NULL)
        return false;
    if (dvz_grid_set_margins(grid, &(DvzPanelReserve){.left_px = 36, .right_px = 36,
                                                       .top_px = 36, .bottom_px = 36}) != DVZ_OK)
        return false;
    if (dvz_grid_set_gutter(grid, 36.0f, 0.0f) != DVZ_OK)
        return false;

    DvzPanel* base = dvz_grid_panel(grid, 0, 0);
    DvzPanel* transformed = dvz_grid_panel(grid, 0, 1);
    if (base == NULL || transformed == NULL)
        return false;
    example_graphite_cyan_set_panel_background(base);
    example_graphite_cyan_set_panel_background(transformed);

    if (!_add_panel_points(ctx->scene, base, false))
        return false;
    if (!_add_panel_points(ctx->scene, transformed, true))
        return false;

    return dvz_scenario_panzoom(ctx, base, NULL, DVZ_DIM_MASK_XY) != NULL &&
           dvz_scenario_panzoom(ctx, transformed, NULL, DVZ_DIM_MASK_XY) != NULL;
}



/**
 * Return the visual-transform scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_visual_transform_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_visual_transform",
        .title = "Visual Transform",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .init = _scenario_init,
    };
}



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

/**
 * Run the visual-transform 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_visual_transform_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Data

Field Value
kind synthetic