Skip to content

User Scale

This example shows how one user-scale value affects screen-space visual sizes and axes.

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/user_scale (build and run), or rerun ./build/examples/c/features/user_scale
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.user_scale
Browser Native only the user-scale control is a native ImGui slider on a Datoviz app view

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 panel combines a data-space path, outlined markers, axes, and panzoom. The path uploads position, color, and stroke_width_px arrays, while the markers upload position, color, diameter_px, angle, and symbol arrays. Live mode animates the user scale sinusoidally by default; disable animation or move the GUI scale slider to inspect a fixed value and compare marker diameters, stroke widths, text, and axis styling while data coordinates stay fixed. User scale is useful for HiDPI displays, screenshots, and accessibility-sized scientific figures. The GUI is attached only for --live/--live-record and starts docked on the left.

Source

#!/usr/bin/env python3
"""User scale applied to screen-space visual sizes and axes."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


PATH_COUNT = 192
MARKER_COUNT = 9
TAU = 2.0 * np.pi


def _path_data():
    t = np.linspace(0.0, 1.0, PATH_COUNT, dtype=np.float32)
    positions = np.column_stack(
        (
            -3.0 + 6.0 * t,
            0.55 * np.sin(2.0 * TAU * t) + 0.18 * np.sin(7.0 * TAU * t),
            np.zeros(PATH_COUNT, dtype=np.float32),
        )
    ).astype(np.float32)
    colors = np.empty((PATH_COUNT, 4), dtype=np.uint8)
    half = PATH_COUNT // 2
    colors[:half] = (ex.CYAN.r, ex.CYAN.g, ex.CYAN.b, ex.CYAN.a)
    colors[half:] = (ex.GREEN.r, ex.GREEN.g, ex.GREEN.b, ex.GREEN.a)
    widths = np.full(PATH_COUNT, 5.0, dtype=np.float32)
    return positions, colors, widths


def _marker_data():
    t = np.linspace(0.0, 1.0, MARKER_COUNT, dtype=np.float32)
    positions = np.column_stack(
        (-2.75 + 5.5 * t, np.full(MARKER_COUNT, -0.95), np.zeros(MARKER_COUNT))
    ).astype(np.float32)

    palette = ex.color_array(ex.CYAN, ex.GREEN, ex.YELLOW)
    colors = np.vstack([palette[i % 3] for i in range(MARKER_COUNT)]).astype(np.uint8)
    diameters = np.array([34.0 + 8.0 * (i % 3) for i in range(MARKER_COUNT)], dtype=np.float32)
    angles = np.array([0.16 * i for i in range(MARKER_COUNT)], dtype=np.float32)
    symbols = np.array(
        [
            dvz.DVZ_SYMBOL_DISC if i % 3 == 0 else
            dvz.DVZ_SYMBOL_TRIANGLE if i % 3 == 1 else
            dvz.DVZ_SYMBOL_DIAMOND
            for i in range(MARKER_COUNT)
        ],
        dtype=np.uint32,
    )
    return positions, colors, diameters, angles, symbols


def _add_path(scene, panel) -> None:
    path = dvz.dvz_path(scene, 0)
    if not path:
        raise RuntimeError("dvz_path() failed")

    positions, colors, widths = _path_data()
    if dvz.dvz_visual_set_data_many(
        path,
        {
            "position": positions,
            "color": colors,
            "stroke_width_px": widths,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(path) failed")
    if dvz.dvz_path_set_caps(path, dvz.DVZ_SEGMENT_CAP_ROUND, dvz.DVZ_SEGMENT_CAP_ROUND) != 0:
        raise RuntimeError("dvz_path_set_caps() failed")
    if dvz.dvz_path_set_join(path, dvz.DVZ_PATH_JOIN_ROUND, 4.0) != 0:
        raise RuntimeError("dvz_path_set_join() failed")

    attach = dvz.dvz_visual_attach_desc()
    attach.coord_space = dvz.DVZ_VISUAL_COORD_DATA
    if dvz.dvz_panel_add_visual(panel, path, ctypes.byref(attach)) != 0:
        raise RuntimeError("dvz_panel_add_visual(path) failed")


def _add_markers(scene, panel) -> None:
    markers = dvz.dvz_marker(scene, 0)
    if not markers:
        raise RuntimeError("dvz_marker() failed")

    style = dvz.dvz_marker_style()
    style.aspect = dvz.DVZ_SHAPE_ASPECT_OUTLINE
    style.edge_color = ex.TEXT
    style.stroke_width_px = 2.75
    if dvz.dvz_marker_set_style(markers, ctypes.byref(style)) != 0:
        raise RuntimeError("dvz_marker_set_style() failed")

    positions, colors, diameters, angles, symbols = _marker_data()
    if dvz.dvz_visual_set_data_many(
        markers,
        {
            "position": positions,
            "color": colors,
            "diameter_px": diameters,
            "angle": angles,
            "symbol": symbols,
        },
    ) != 0:
        raise RuntimeError("dvz_visual_set_data_many(markers) failed")
    if dvz.dvz_visual_set_alpha_mode(markers, dvz.DVZ_ALPHA_BLENDED) != 0:
        raise RuntimeError("dvz_visual_set_alpha_mode() failed")
    ex.add_visual(panel, markers)


def _add_axes(panel) -> None:
    axes = dvz.dvz_panel_axes_2d_desc()
    axes.x_label = b"x"
    axes.y_label = b"amplitude"
    if dvz.dvz_panel_set_axes_2d(panel, ctypes.byref(axes)) != 0:
        raise RuntimeError("dvz_panel_set_axes_2d() failed")

    for dim in (dvz.DVZ_DIM_X, dvz.DVZ_DIM_Y):
        axis = dvz.dvz_panel_axis(panel, dim)
        if not axis:
            raise RuntimeError("dvz_panel_axis() failed")
        if dvz.dvz_axis_set_grid(axis, True) != 0:
            raise RuntimeError("dvz_axis_set_grid() failed")
        style = dvz.dvz_axis_style()
        style.tick_size_px = 13.0
        style.label_size_px = 19.0
        style.tick_gap_px = 9.0
        style.grid_color[:] = (116, 132, 148, 105)
        if dvz.dvz_axis_set_style(axis, ctypes.byref(style)) != 0:
            raise RuntimeError("dvz_axis_set_style() failed")


def _build_scene():
    scene, figure, panel = ex.scene_panel()
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, -3.25, 3.25) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, -1.45, 1.20) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")

    _add_path(scene, panel)
    _add_markers(scene, panel)
    _add_axes(panel)
    return scene, figure, panel


def _configure_view(view, scene, panel, *, user_scale: float = 1.4, gui: bool = True) -> None:
    ex.bind_panzoom(view, scene, panel, dvz.DVZ_DIM_MASK_XY)
    if dvz.dvz_view_set_user_scale(view, float(user_scale)) != 0:
        raise RuntimeError("dvz_view_set_user_scale() failed")

    if not gui:
        return
    overlay = dvz.dvz_view_gui(view, None)
    if not overlay:
        return

    scale = ctypes.c_float(float(user_scale))

    def gui_callback(gui_ptr, view_ptr, _user_data) -> None:
        if dvz.dvz_gui_begin(gui_ptr, b"User scale", None, 0):
            if dvz.dvz_gui_slider_float(gui_ptr, b"Scale", ctypes.byref(scale), 0.5, 2.5):
                dvz.dvz_view_set_user_scale(view_ptr, scale.value)
        dvz.dvz_gui_end(gui_ptr)

    if dvz.dvz_view_set_gui_callback(view, gui_callback, None) != 0:
        raise RuntimeError("dvz_view_set_gui_callback() failed")


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

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

    ex.run_with_view(scene, figure, "User Scale", 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 shows how one user-scale value affects screen-space visual sizes and axes.
 *
 * What to look for: the panel combines a data-space path, outlined markers, axes, and panzoom.
 * The path uploads position, color, and stroke_width_px arrays, while the markers upload position,
 * color, diameter_px, angle, and symbol arrays. Live mode animates the user scale sinusoidally by
 * default; disable animation or move the GUI scale slider to inspect a fixed value and compare
 * marker diameters, stroke widths, text, and axis styling while data coordinates stay fixed. User
 * scale is useful for HiDPI displays, screenshots, and accessibility-sized scientific figures. The
 * GUI is attached only for `--live`/`--live-record` and starts docked on the left.
 *
 * Scenario: features_user_scale
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/user_scale
 * Run:    ./build/examples/c/features/user_scale --live
 * Smoke:  ./build/examples/c/features/user_scale --png --user-scale 1.4
 */



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

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

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



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

DvzScenarioSpec dvz_example_user_scale_scenario(void);

static bool _gui_controls(DvzGui* gui, void* user_data);



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

#define WIDTH        EXAMPLE_WINDOW_WIDTH
#define HEIGHT       EXAMPLE_WINDOW_HEIGHT
#define PATH_COUNT   192u
#define MARKER_COUNT 9u
#define SCALE_PERIOD 2.0

static const float TAU = 6.28318530718f;



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

typedef struct UserScaleState
{
    ExampleTuner tuner;
    DvzView* view;
    float user_scale;
    bool animate;
} UserScaleState;



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

/**
 * Fill a deterministic curve in data coordinates.
 *
 * @param positions output curve positions
 * @param colors output curve colors
 * @param widths output logical stroke widths
 */
static void
_fill_path(vec3 positions[PATH_COUNT], DvzColor colors[PATH_COUNT], float widths[PATH_COUNT])
{
    ANN(positions);
    ANN(colors);
    ANN(widths);

    for (uint32_t i = 0; i < PATH_COUNT; i++)
    {
        const float t = (float)i / (float)(PATH_COUNT - 1u);
        positions[i][0] = -3.0f + 6.0f * t;
        positions[i][1] = 0.55f * sinf(2.0f * TAU * t) + 0.18f * sinf(7.0f * TAU * t);
        positions[i][2] = 0.0f;
        colors[i] = example_graphite_cyan_color(
            i < PATH_COUNT / 2u ? EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY
                                : EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY);
        widths[i] = 5.0f;
    }
}



/**
 * Add a retained stroked path.
 *
 * @param scene scene owning the visual
 * @param panel target panel
 * @return true on success
 */
static bool _add_path(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    vec3 data_positions[PATH_COUNT] = {{0}};
    DvzColor colors[PATH_COUNT] = {{0}};
    float widths[PATH_COUNT] = {0};
    _fill_path(data_positions, colors, widths);

    DvzVisual* path = dvz_path(scene, 0);
    if (path == NULL)
        return false;
    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = data_positions, .item_count = PATH_COUNT},
        {.attr_name = "color", .data = colors, .item_count = PATH_COUNT},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = PATH_COUNT},
    };
    if (dvz_visual_set_data_many(path, updates, 3) != 0)
        return false;
    if (dvz_path_set_caps(path, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
        return false;
    if (dvz_path_set_join(path, DVZ_PATH_JOIN_ROUND, 4.0f) != 0)
        return false;
    DvzVisualAttachDesc attach = dvz_visual_attach_desc();
    attach.coord_space = DVZ_VISUAL_COORD_DATA;
    return dvz_panel_add_visual(panel, path, &attach) == 0;
}



/**
 * Add outlined data-space markers.
 *
 * @param scene scene owning the visual
 * @param panel target panel
 * @return true on success
 */
static bool _add_markers(DvzScene* scene, DvzPanel* panel)
{
    ANN(scene);
    ANN(panel);

    DvzVisual* markers = dvz_marker(scene, 0);
    if (markers == NULL)
        return false;

    DvzMarkerStyle style = dvz_marker_style();
    style.aspect = DVZ_SHAPE_ASPECT_OUTLINE;
    style.edge_color = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_TEXT);
    style.stroke_width_px = 2.75f;
    if (dvz_marker_set_style(markers, &style) != 0)
        return false;

    vec3 positions[MARKER_COUNT] = {{0}};
    DvzColor colors[MARKER_COUNT] = {{0}};
    float diameters[MARKER_COUNT] = {0};
    float angles[MARKER_COUNT] = {0};
    uint32_t symbols[MARKER_COUNT] = {0};
    for (uint32_t i = 0; i < MARKER_COUNT; i++)
    {
        const float t = (float)i / (float)(MARKER_COUNT - 1u);
        positions[i][0] = -2.75f + 5.5f * t;
        positions[i][1] = -0.95f;
        positions[i][2] = 0.0f;
        colors[i] = example_graphite_cyan_color(
            i % 3u == 0u   ? EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY
            : i % 3u == 1u ? EXAMPLE_STYLE_COLOR_ACCENT_SECONDARY
                           : EXAMPLE_STYLE_COLOR_WARNING);
        diameters[i] = 34.0f + 8.0f * (float)(i % 3u);
        angles[i] = 0.16f * (float)i;
        symbols[i] = (uint32_t)(i % 3u == 0u   ? DVZ_SYMBOL_DISC
                                : i % 3u == 1u ? DVZ_SYMBOL_TRIANGLE
                                               : DVZ_SYMBOL_DIAMOND);
    }

    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 = "symbol", .data = symbols, .item_count = MARKER_COUNT},
    };
    if (dvz_visual_set_data_many(markers, updates, 5) != 0)
        return false;
    if (dvz_visual_set_alpha_mode(markers, DVZ_ALPHA_BLENDED) != 0)
        return false;
    return dvz_panel_add_visual(panel, markers, NULL) == 0;
}



/**
 * Configure panel axes.
 *
 * @param panel target panel
 * @return true on success
 */
static bool _add_axes(DvzPanel* panel)
{
    ANN(panel);

    DvzPanelAxes2DDesc axes = dvz_panel_axes_2d_desc();
    axes.x_label = "x";
    axes.y_label = "amplitude";

    if (dvz_panel_set_axes_2d(panel, &axes) != DVZ_OK)
        return false;
    return example_graphite_cyan_style_axes_2d(panel, NULL);
}



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

/**
 * Initialize the user-scale 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 || out_user == NULL)
        return false;

    UserScaleState* state = (UserScaleState*)dvz_calloc(1, sizeof(UserScaleState));
    if (state == NULL)
        return false;
    state->tuner = example_tuner("User scale");
    state->user_scale = ctx->user_scale > 0.0f ? ctx->user_scale : 1.0f;
    state->animate = true;

    ctx->figure = dvz_figure(ctx->scene, ctx->width, ctx->height, 0);
    if (ctx->figure == NULL)
        goto error;
    example_tuner_figure(&state->tuner, ctx->figure);

    DvzPanel* panel = dvz_panel_full(ctx->figure);
    if (panel == NULL)
        goto error;
    example_graphite_cyan_set_panel_background(panel);

    if (dvz_panel_set_domain(panel, DVZ_DIM_X, -3.25, 3.25) != 0)
        goto error;
    if (dvz_panel_set_domain(panel, DVZ_DIM_Y, -1.45, 1.20) != 0)
        goto error;
    if (!_add_path(ctx->scene, panel))
        goto error;
    if (!_add_markers(ctx->scene, panel))
        goto error;
    if (!_add_axes(panel))
        goto error;

    DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, NULL, DVZ_DIM_MASK_XY);
    if (panzoom == NULL)
        goto error;
#ifndef DVZ_EXAMPLE_NO_APP
    if (!example_tuner_add_component(
            &state->tuner, "Display scale", state, NULL, _gui_controls, NULL, NULL, NULL))
    {
        goto error;
    }
#endif

    *out_user = state;
    return true;

error:
    dvz_free(state);
    return false;
}



/**
 * Draw the user-scale tuner component.
 *
 * @param gui GUI context
 * @param user_data example state
 * @return whether an animation or scale control changed
 */
static bool _gui_controls(DvzGui* gui, void* user_data)
{
    UserScaleState* state = (UserScaleState*)user_data;
    if (gui == NULL || state == NULL || state->view == NULL)
        return false;

    const bool animate_changed = dvz_gui_checkbox(gui, "Animate", &state->animate);
    const bool scale_changed =
        dvz_gui_slider_float(gui, "Scale", &state->user_scale, 0.5f, 2.5f);
    if (scale_changed)
    {
        state->animate = false;
        (void)dvz_view_set_user_scale(state->view, state->user_scale);
    }
    return animate_changed || scale_changed;
}



/**
 * Attach the live GUI to the native view.
 *
 * @param ctx scenario context
 * @param app app
 * @param view native view
 * @param user scenario state
 * @return true on success
 */
static bool _scenario_view(DvzScenarioContext* ctx, DvzApp* app, DvzView* view, void* user)
{
    (void)ctx;
    (void)app;
    UserScaleState* state = (UserScaleState*)user;
    if (state == NULL || view == NULL)
        return false;
    state->view = view;
    state->user_scale = dvz_view_user_scale(view);
    return true;
}


/**
 * Attach the left-docked tuner in explicit live mode.
 *
 * @param ctx scenario context
 * @param app app
 * @param view native view
 * @param user scenario state
 * @return true on success
 */
static bool _scenario_live_view(DvzScenarioContext* ctx, DvzApp* app, DvzView* view, void* user)
{
    if (!_scenario_view(ctx, app, view, user))
        return false;
    if (ctx == NULL || ctx->presentation != DVZ_RUNNER_PRESENT_GLFW)
        return true;
    UserScaleState* state = (UserScaleState*)user;
    return example_tuner_attach(&state->tuner, view);
}



/**
 * Animate the user scale for live runs and generated gallery media.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_frame(DvzScenarioContext* ctx, void* user)
{
    UserScaleState* state = (UserScaleState*)user;
    if (ctx == NULL || state == NULL || state->view == NULL)
        return;

    double phase = 0.0;
    if (ctx->preview_mode)
    {
        phase = dvz_scenario_preview_phase(ctx, DVZ_SCENARIO_PREVIEW_PHASE_SEAMLESS_LOOP);
    }
    else if (ctx->presentation == DVZ_RUNNER_PRESENT_GLFW && state->animate)
    {
        phase = fmod(fmax(ctx->time, 0.0) / SCALE_PERIOD, 1.0);
    }
    else
    {
        return;
    }

    state->user_scale = 1.35f + 0.55f * sinf(TAU * (float)phase);
    (void)dvz_view_set_user_scale(state->view, state->user_scale);
}



/**
 * Destroy the user-scale feature state.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_destroy(DvzScenarioContext* ctx, void* user)
{
    (void)ctx;
    UserScaleState* state = (UserScaleState*)user;
    if (state == NULL)
        return;
    example_tuner_detach(&state->tuner);
    dvz_free(state);
}



/**
 * Return the user-scale scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_user_scale_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_user_scale",
        .title = "User Scale",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_MARKER_VISUAL | DVZ_SCENARIO_REQ_CONTROLLER |
                        DVZ_SCENARIO_REQ_PANZOOM | DVZ_SCENARIO_REQ_NATIVE_VIEW |
                        DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .frame = _scenario_frame,
        .native_view = _scenario_view,
        .destroy = _scenario_destroy,
    };
}



/*************************************************************************************************/
/*  Main                                                                                         */
/*************************************************************************************************/

int main(int argc, char** argv)
{
    DvzScenarioSpec spec = dvz_example_user_scale_scenario();
    if (example_cli_wants_live_gui(argc, argv))
        spec.native_view = _scenario_live_view;
    return dvz_scenario_run_native_cli(&spec, argc, argv);
}
Example details
  • ID: features_user_scale
  • Category: feature
  • Lane: features
  • Status: supported
  • Source: examples/c/features/user_scale.c
  • Approved adaptation starter: yes
  • Python source: examples/python/gallery/features/user_scale.py
  • Python adaptation: Available; direct-engine adaptation
  • Browser support: Native only
  • Browser note: the user-scale control is a native ImGui slider on a Datoviz app view
  • Browser capability tags: marker, panzoom, path, gui
  • Validation: smoke+interaction+screenshot

Data

Field Value
kind synthetic