Skip to content

Basic Visual

The basic visual in Datoviz provides direct access to low-level GPU rendering using core Vulkan primitives.

This visual is well suited for rendering large-scale data as points, thin non-antialiased lines, or triangles, with one color per vertex.

Basic visual with line_strip topology

Note

Crude antialiasing techniques may be added in a future version. In the meantime, these visuals are heavily aliased but remain highly efficient.


Overview

  • Uses core Vulkan primitive topologies: point, line_strip, line_list, triangle_list, and triangle_strip
  • Accepts 3D NDC positions
  • Supports per-vertex color
  • Supports uniform size but not per-vertex size
  • Supports a group attribute to separate primitives within a single visual

When to use

Use the basic visual when you need:

  • Large-scale, uniform visual primitives
  • Full control over rendering topology
  • Minimal overhead and high performance

Attributes

Per-item

Attribute Type Description
position (N, 3) float32 Vertex positions in NDC
color (N, 4) uint8 RGBA color per vertex
group (N,) float32 Group ID used to separate primitives (line_strip and triangle_strip)

Per-visual (uniform)

Attribute Type Description
size float Pixel size for point topology

Grouping

The group attribute allows you to encode multiple disconnected geometries in one visual.

For example, rendering many time series as separate line strips:

visual = app.basic('line_strip', position=position, color=color, group=group)
  • Each group must be identified by a consecutive integer ID: 0, 0, ..., 1, 1, ..., 2, 2, ...
  • Transitions between groups break the connection in line-based topologies.

This approach is memory-efficient and avoids multiple draw calls.


Topologies

Here, we show the different primitive topologies for the Basic visual using the same set of positions, colors, and groups.

The 2D points alternate between a lower and upper sine curves. The points are split in two groups with equal size (the group is 0 if i < N/2 and 1 if i >= N/2).

Points

Renders one dot per vertex. Similar to the Pixel visual.

visual = app.basic('point_list', position=position, color=color, size=5)
Basic visual, point_list topology

Line list

Each pair of vertices forms a separate segment: A-B, C-D, E-F, etc.

visual = app.basic('line_list', position=position, color=color)
Basic visual, line_list topology

Line strip

Connects vertices into a continuous path: A-B-C-D becomes a single polyline.

visual = app.basic('line_strip', position=position, color=color, group=group)

Note

The optional group attribute can be used to draw multiple line strips in one call, which is best for performance. Note the gap in the middle, corresponding to the transition between the two groups.

Basic visual, line_strip topology

Triangle list

Each group of 3 vertices forms a triangle: A-B-C, D-E-F, etc.

visual = app.basic('triangle_list', position=position, color=color)
Basic visual, triangle_list topology

Triangle strip

Renders connected triangles using a strip pattern: A-B-C, B-C-D, C-D-E, etc.

visual = app.basic('triangle_strip', position=position, color=color, group=group)

Note

The optional group attribute can be used to draw multiple triangle strips in one call, which is best for performance. Note the gap in the middle, corresponding to the transition between the two groups.

Basic visual, triangle_strip topology

Dynamic topology change

This example displays a GUI to change the topology of the Basic visual.

Basic topology widget

Warning

Changing visual options dynamically is not fully supported yet, so we use a workaround that involves deleting and recreating the visual. This will be improved in Datoviz v0.4.

Source
import sys

import numpy as np

import datoviz as dvz

OPTIONS = dvz.TOPOLOGY_OPTIONS

N = 30
t2 = np.linspace(-1.0, +1.0, 2 * N)
y1 = -0.1 + 0.25 * np.sin(2 * 2 * np.pi * t2[0::2])
y2 = +0.1 + 0.25 * np.sin(2 * 2 * np.pi * t2[1::2])

y = np.c_[y1, y2].ravel()

position = np.c_[t2, y, np.zeros(2 * N)]
group = np.repeat([0, 1], N)
color = dvz.cmap('hsv', t2, vmin=-1, vmax=+1)

app = dvz.App()
figure = app.figure(gui=True)
panel = figure.panel()
panzoom = panel.panzoom()
visual = None


def set_primitive(primitive):
    global visual
    if visual:
        panel.remove(visual)
    visual = app.basic(primitive, position=position, color=color, group=group, size=5)
    panel.add(visual)


set_primitive(sys.argv[1] if len(sys.argv) >= 2 else 'point_list')

selected = dvz.Out(0)


@app.connect(figure)
def on_gui(ev):
    dvz.gui_pos(dvz.vec2(10, 10), dvz.vec2(0, 0))
    dvz.gui_size(dvz.vec2(300, 80))
    dvz.gui_begin('Primitive topology', 0)
    if dvz.gui_dropdown('Topology', len(OPTIONS), list(OPTIONS), selected, 0):
        primitive = OPTIONS[selected.value]
        set_primitive(primitive)
    dvz.gui_end()


app.run()
app.destroy()

Example

import matplotlib.colors as mcolors
import numpy as np

import datoviz as dvz


def generate_data():
    """Return N, positions (N,3) float32, colors (N,4) uint8"""
    # Parameters
    n_arms = 5
    n_particles_per_arm = 200_000
    n_total = n_arms * n_particles_per_arm

    rng = np.random.default_rng(seed=42)

    # Radius from center, with more points toward center
    r = rng.power(2.0, size=n_total)  # values in [0, 1), biased toward 0

    # Angle with swirl per arm and some noise
    base_theta = np.repeat(np.linspace(0, 2 * np.pi, n_arms, endpoint=False), n_particles_per_arm)
    swirl = r * 3  # spiral effect
    noise = rng.normal(scale=0.2, size=n_total)
    theta = base_theta + swirl + noise

    # Convert polar to Cartesian
    x = r * np.cos(theta) * 6.0 / 8.0  # HACK: window aspect ratio
    y = r * np.sin(theta)
    z = np.zeros_like(x)

    positions = np.stack([x, y, z], axis=1).astype(np.float32)

    # Colors based on radius and angle — create a vibrant, cosmic feel
    hue = (theta % (2 * np.pi)) / (2 * np.pi)  # hue from angle
    saturation = np.clip(r * 1.5, 0.2, 1.0)  # more saturated at edges
    value = np.ones_like(hue)

    # Convert HSV to RGB

    rgb = mcolors.hsv_to_rgb(np.stack([hue, saturation, value], axis=1))
    rgb_u8 = (rgb * 255).astype(np.uint8)

    # Alpha: slight fade with radius
    alpha = np.clip(128 * (1.0 - r), 1, 255).astype(np.uint8)
    alpha = (200 * np.exp(-5 * r * r)).astype(np.uint8)

    colors = np.concatenate([rgb_u8, alpha[:, None]], axis=1)

    return n_total, positions, colors


N, position, color = generate_data()

app = dvz.App()
figure = app.figure()
panel = figure.panel()
panzoom = panel.panzoom()

visual = app.pixel(position=position, color=color)
panel.add(visual)

app.run()
app.destroy()

Summary

The basic visual provides direct access to the core rendering capabilities of Datoviz. It's powerful, fast, and flexible — ideal for high-volume data when you don’t need complex styling.

For advanced visuals with lighting, texturing, or custom shapes, see the other visuals.