Skip to content

Datetime Axis

This example shows UTC datetime labels on a numeric data axis.

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/datetime_axis (build and run), or rerun ./build/examples/c/features/datetime_axis
Python Available; direct-engine adaptation python3 -m examples.python.gallery.features.datetime_axis
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 path visual uploads position, color, and linewidth arrays in compact numeric coordinates from 0 to 8. The x axis is then configured with datetime metadata and a UTC range, so the tick labels display times while the visual data stays simple. Compare the curve's x positions with the formatted time labels; this is useful for time-series data where rendering should remain numeric but the plot must communicate real dates or times.

Source

#!/usr/bin/env python3
"""UTC datetime tick labels on a numeric x axis."""

from __future__ import annotations

import ctypes

import numpy as np

import datoviz as dvz

from examples.python.gallery import common as ex


SAMPLE_COUNT = 320
TAU = 2.0 * np.pi
MAY_1_UTC_US = 1_714_554_000_000_000


def _signal():
    t = np.linspace(0.0, 1.0, SAMPLE_COUNT, dtype=np.float32)
    x = 8.0 * t
    y = 0.46 * np.sin(3.0 * TAU * t) + 0.18 * np.cos(9.0 * TAU * t + 0.3)
    positions = np.column_stack([x, y, np.zeros(SAMPLE_COUNT)]).astype(np.float32)
    colors = ex.color_array(*([ex.CYAN] * SAMPLE_COUNT))
    colors[:, 3] = 235
    widths = np.full(SAMPLE_COUNT, 4.0, dtype=np.float32)
    return positions, colors, widths


def _add_signal(scene, panel) -> None:
    positions, colors, widths = _signal()
    path = dvz.dvz_path(scene, 0)
    if not path:
        raise RuntimeError("dvz_path() failed")
    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")
    ex.add_visual(panel, path)


def _configure_axis(axis, label: bytes, vertical: bool) -> None:
    ticks = dvz.dvz_axis_tick_policy()
    ticks.target_count = 7
    ticks.min_pixel_spacing = 130.0
    ticks.minor_per_interval = 3
    if dvz.dvz_axis_set_tick_policy(axis, ctypes.byref(ticks)) != 0:
        raise RuntimeError("dvz_axis_set_tick_policy() failed")

    style = dvz.dvz_axis_style()
    style.tick_size_px = 14.0
    style.label_size_px = 18.0
    style.tick_gap_px = 9.0
    style.label_gap_px = 12.0
    style.grid_color[:] = (96, 165, 250, 72)
    style.spine_color[:] = (217, 226, 236, 190)
    style.major_tick_color[:] = (217, 226, 236, 190)
    style.minor_tick_color[:] = (217, 226, 236, 110)
    if vertical:
        style.reserve_px = 80.0
    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_label(axis, label) != 0:
        raise RuntimeError("dvz_axis_set_label() failed")


def _datetime_format(scene):
    datetime = dvz.dvz_datetime_format_create(scene)
    if not datetime:
        raise RuntimeError("dvz_datetime_format_create() failed")
    if dvz.dvz_datetime_format_timezone(datetime, b"UTC") != 0:
        raise RuntimeError("dvz_datetime_format_timezone() failed")

    rules = [
        (dvz.DVZ_TIME_INTERVAL_MICROSECOND, b"%H:%M:%S.fff"),
        (dvz.DVZ_TIME_INTERVAL_MILLISECOND, b"%H:%M:%S.fff"),
        (dvz.DVZ_TIME_INTERVAL_SECOND, b"%H:%M:%S"),
        (dvz.DVZ_TIME_INTERVAL_MINUTE, b"%H:%M"),
        (dvz.DVZ_TIME_INTERVAL_HOUR, b"%H:%M"),
        (dvz.DVZ_TIME_INTERVAL_DAY, b"%b %d"),
        (dvz.DVZ_TIME_INTERVAL_MONTH, b"%Y-%m"),
        (dvz.DVZ_TIME_INTERVAL_YEAR, b"%Y"),
    ]
    for interval, fmt in rules:
        if dvz.dvz_datetime_format_rule(datetime, interval, fmt) != 0:
            raise RuntimeError("dvz_datetime_format_rule() failed")
    return datetime


def _configure_axes(scene, 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")

    _configure_axis(x_axis, b"UTC time", False)
    _configure_axis(y_axis, b"signal", True)

    datetime = _datetime_format(scene)
    if dvz.dvz_axis_set_datetime(x_axis, datetime) != 0:
        raise RuntimeError("dvz_axis_set_datetime() failed")
    end_utc = MAY_1_UTC_US + 8 * 3600 * 1_000_000
    if dvz.dvz_axis_set_datetime_range(x_axis, 0.0, 8.0, MAY_1_UTC_US, end_utc) != 0:
        raise RuntimeError("dvz_axis_set_datetime_range() failed")


def _build_scene():
    scene, figure, panel = ex.scene_panel()
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_X, 0.0, 8.0) != 0:
        raise RuntimeError("dvz_panel_set_domain(X) failed")
    if dvz.dvz_panel_set_domain(panel, dvz.DVZ_DIM_Y, -0.9, 0.9) != 0:
        raise RuntimeError("dvz_panel_set_domain(Y) failed")
    _add_signal(scene, panel)
    _configure_axes(scene, panel)
    return scene, figure, panel


def _configure_view(view, scene, panel) -> None:
    controller = dvz.dvz_panzoom(scene, None)
    if not controller:
        raise RuntimeError("dvz_panzoom() failed")
    if dvz.dvz_view_bind_controller(view, panel, controller, dvz.DVZ_DIM_MASK_X) != 0:
        raise RuntimeError("dvz_view_bind_controller() failed")


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

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

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

/* datetime_axis - This example shows UTC datetime labels on a numeric data axis.
 *
 * Scenario: features_datetime_axis
 * Style: features, graphite_cyan, 1280x720 window target
 *
 * Build:  just example-c features/datetime_axis
 * Run:    ./build/examples/c/features/datetime_axis --live
 * Smoke:  ./build/examples/c/features/datetime_axis --png
 *
 * What to look for: the path visual uploads position, color, and linewidth arrays in compact
 * numeric coordinates from 0 to 8. The x axis is then configured with datetime metadata and a UTC
 * range, so the tick labels display times while the visual data stays simple. Compare the curve's
 * x positions with the formatted time labels; this is useful for time-series data where rendering
 * should remain numeric but the plot must communicate real dates or times.
 */



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

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

#include "_alloc.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 SAMPLE_COUNT 320u

static const float TAU = 6.28318530718f;

DvzScenarioSpec dvz_example_datetime_axis_scenario(void);



/*************************************************************************************************/
/*  Types                                                                                         */
/*************************************************************************************************/

typedef struct DvzDateTimeAxisState
{
    DvzPanzoom* panzoom;
} DvzDateTimeAxisState;



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

/**
 * Fill one deterministic signal in compact data coordinates.
 *
 * @param positions output data-space positions
 * @param colors output colors
 * @param widths output stroke widths
 * @param count sample count
 */
static void _fill_signal(vec3* positions, DvzColor* colors, float* widths, uint32_t count)
{
    ANN(positions);
    ANN(colors);
    ANN(widths);

    DvzColor accent = example_graphite_cyan_color(EXAMPLE_STYLE_COLOR_ACCENT_PRIMARY);
    const float inv_count = count > 1u ? 1.0f / (float)(count - 1u) : 1.0f;
    for (uint32_t i = 0; i < count; i++)
    {
        const float t = (float)i * inv_count;
        positions[i][0] = 8.0f * t;
        positions[i][1] =
            0.46f * sinf(3.0f * TAU * t) + 0.18f * cosf(9.0f * TAU * t + 0.3f);
        positions[i][2] = 0.0f;
        colors[i] = accent;
        colors[i].a = 235u;
        widths[i] = 4.0f;
    }
}


/**
 * Upload one path visual.
 *
 * @param visual path visual
 * @param positions visual-space positions
 * @param colors path colors
 * @param widths stroke widths
 * @param count sample count
 * @return true when all uploads succeed
 */
static bool _upload_path(
    DvzVisual* visual, vec3* positions, DvzColor* colors, float* widths, uint32_t count)
{
    ANN(visual);
    ANN(positions);
    ANN(colors);
    ANN(widths);

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = count},
        {.attr_name = "color", .data = colors, .item_count = count},
        {.attr_name = "stroke_width_px", .data = widths, .item_count = count},
    };
    if (dvz_visual_set_data_many(visual, updates, 3) != 0)
        return false;
    if (dvz_path_set_caps(visual, DVZ_SEGMENT_CAP_ROUND, DVZ_SEGMENT_CAP_ROUND) != 0)
        return false;
    return dvz_path_set_join(visual, DVZ_PATH_JOIN_ROUND, 4.0f) == 0;
}



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

/**
 * Initialize the retained datetime axis 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;

    vec3 data_positions[SAMPLE_COUNT] = {{0}};
    DvzColor colors[SAMPLE_COUNT] = {{0}};
    float widths[SAMPLE_COUNT] = {0};

    _fill_signal(data_positions, colors, widths, SAMPLE_COUNT);

    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);

    int rc = dvz_panel_set_domain(panel, DVZ_DIM_X, 0.0, 8.0);
    if (rc != 0)
        return false;
    rc = dvz_panel_set_domain(panel, DVZ_DIM_Y, -0.9, 0.9);
    if (rc != 0)
        return false;

    DvzVisual* path = dvz_path(ctx->scene, 0);
    if (path == NULL)
        return false;
    bool ok = _upload_path(path, data_positions, colors, widths, SAMPLE_COUNT);
    if (!ok)
        return false;
    rc = dvz_panel_add_visual(panel, path, NULL);
    if (rc != 0)
        return false;

    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;

    DvzAxisTickPolicy ticks = dvz_axis_tick_policy();
    ticks.target_count = 7;
    ticks.min_pixel_spacing = 130.0f;
    ticks.minor_per_interval = 3;
    ok = dvz_axis_set_tick_policy(x_axis, &ticks) == DVZ_OK;
    if (!ok)
        return false;
    ok = dvz_axis_set_tick_policy(y_axis, &ticks) == DVZ_OK;
    if (!ok)
        return false;

    ok = example_graphite_cyan_apply_axis_style(x_axis, false, NULL);
    if (!ok)
        return false;
    ok = example_graphite_cyan_apply_axis_style(y_axis, true, NULL);
    if (!ok)
        return false;
    ok = dvz_axis_set_grid(x_axis, true) == DVZ_OK;
    if (!ok)
        return false;
    ok = dvz_axis_set_grid(y_axis, true) == DVZ_OK;
    if (!ok)
        return false;

    DvzDateTimeFormat* datetime = dvz_datetime_format_create(ctx->scene);
    if (datetime == NULL)
        return false;
    rc = dvz_datetime_format_timezone(datetime, "UTC");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(
        datetime, DVZ_TIME_INTERVAL_MICROSECOND, "%H:%M:%S.fff");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(
        datetime, DVZ_TIME_INTERVAL_MILLISECOND, "%H:%M:%S.fff");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_SECOND, "%H:%M:%S");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_MINUTE, "%H:%M");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_HOUR, "%H:%M");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_DAY, "%b %d");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_MONTH, "%Y-%m");
    if (rc != 0)
        return false;
    rc = dvz_datetime_format_rule(datetime, DVZ_TIME_INTERVAL_YEAR, "%Y");
    if (rc != 0)
        return false;
    const DvzTimestamp may_1_utc = (DvzTimestamp)1714554000000000LL; /* 2024-05-01 09:00 UTC */
    ok = dvz_axis_set_datetime(x_axis, datetime) == DVZ_OK;
    if (!ok)
        return false;
    ok = dvz_axis_set_datetime_range(
             x_axis, 0.0, 8.0, may_1_utc, may_1_utc + 8LL * 3600LL * 1000000LL) == DVZ_OK;
    if (!ok)
        return false;

    ok = dvz_axis_set_label(x_axis, "UTC time") == DVZ_OK;
    if (!ok)
        return false;
    ok = dvz_axis_set_label(y_axis, "signal") == DVZ_OK;
    if (!ok)
        return false;

    DvzPanzoom* panzoom = dvz_scenario_panzoom(ctx, panel, NULL, DVZ_DIM_MASK_X);
    if (panzoom == NULL)
        return false;

    DvzDateTimeAxisState* state = (DvzDateTimeAxisState*)dvz_calloc(1, sizeof(*state));
    if (state == NULL)
        return false;
    state->panzoom = panzoom;
    if (out_user != NULL)
        *out_user = state;
    return true;
}



/**
 * Apply a deterministic eased zoom loop for generated gallery media.
 *
 * @param ctx scenario context
 * @param user scenario state
 */
static void _scenario_frame(DvzScenarioContext* ctx, void* user)
{
    DvzDateTimeAxisState* state = (DvzDateTimeAxisState*)user;
    if (ctx == NULL || !ctx->preview_mode || state == NULL || state->panzoom == NULL)
        return;

    const uint64_t count = ctx->preview_frame_count > 0 ? ctx->preview_frame_count : 1;
    const float phase = (float)(ctx->preview_frame_index % count) / (float)count;
    const float eased = 0.5f - 0.5f * cosf(TAU * phase);
    const float zoom_x = powf(8.0f, eased);
    (void)dvz_panzoom_pan(state->panzoom, (vec2){0.0f, 0.0f});
    (void)dvz_panzoom_zoom(state->panzoom, (vec2){zoom_x, 1.0f});
}


/**
 * Destroy the datetime-axis 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 datetime-axis scenario specification.
 *
 * @return scenario specification
 */
DvzScenarioSpec dvz_example_datetime_axis_scenario(void)
{
    return (DvzScenarioSpec){
        .id = "features_datetime_axis",
        .title = "Datetime Axis",
        .width = WIDTH,
        .height = HEIGHT,
        .fps = 60.0,
        .requirements = DVZ_SCENARIO_REQ_TEXT_VISUAL | DVZ_SCENARIO_REQ_CONTROLLER |
                        DVZ_SCENARIO_REQ_PANZOOM | DVZ_SCENARIO_REQ_FRAME_CALLBACKS,
        .init = _scenario_init,
        .frame = _scenario_frame,
        .destroy = _scenario_destroy,
    };
}



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

/**
 * Run the retained datetime axis feature proof 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_datetime_axis_scenario();
    return dvz_scenario_run_native_cli(&spec, argc, argv) == 0 ? 0 : 1;
}
#endif
Example details

Tags

axes, datetime, time-series

Data

Field Value
kind synthetic