On this page

Packed vector markers

The measurements below record that development revision; see dense data for current authoring choices. Packed markers store dense vector scatter layers as immutable marker records. It reduces Python object overhead without removing observations or rasterizing the layer.

Four panels showing dense vector observations, variable bubbles, seven marker shapes and a cropped triangular point cloud

Complete recipe · Separate LaTeX caption · Raw benchmark results

The figure contains 38,300 packed markers in 10 batches, with 301 compiled scene nodes including axes and labels. It exports to SVG, PDF and PNG, and passes the figure diagnostics. The inputs are deterministic simulated data.

Use ordinary scatter calls

import inklet as i

p = i.panel(80, 50, x=(0, 1), y=(0, 1))
p.scatter([((k * 0.61803398875) % 1, (k * 0.41421356237) % 1)
           for k in range(10_000)],
          size=0.4, color="#245b8a", fill_opacity=0.35)
p.axes(x="Coordinate x", y="Coordinate y")
f = i.figure(width=100)
f.add(p.build())
f.save("cloud.svg", "cloud.pdf")

Vector scatter uses packed records automatically at 256 points. All seven marker shapes support per-point diameters and colours. Stroke widths, dashes and corner radii remain in physical units; changing a diameter does not scale its outline width. Polar scatter uses the same representation.

Small series, explicit anchor/origin placement, and axes with breaks keep individual drawing nodes. raster=True retains the existing explicit raster path. Batching does not choose an image representation on your behalf.

Geometry, paint and clipping

Each record stores two coordinates and a diameter as 64-bit floats, a 32-bit palette index and a 64-bit original source index: 36 bytes per marker. The records are little-endian immutable bytes; the palette and unit marker shape are shared. Buffers copy mutable input, reject non-finite coordinates and invalid sizes, and participate in compiled-scene reuse by identity.

Markers remain in source order and paint separately. Overlapping translucent markers accumulate correctly; series opacity composites the completed series once. The backends cache at most 128 temporary sized prototypes or palette styles during a batch export.

clip() filters off-region records. Where the existing geometric clip contract cuts a polygonal marker, the boundary geometry is rewritten in order between compact runs. Original source indices remain in retained records and in the source_index note of rewritten boundary nodes. window() retains the source buffer and clips the complete paint, including outlines, in each backend.

Layout support and connector traces use the individual marker shapes. Colour key diagnostics count the palette entries actually used. Text overlap checks inspect individual footprints rather than treating empty space inside the cloud's bounding box as occupied.

Inspection and revisions

from inklet.core import MarkerBatchPrim

scene = i.compile_scene(p.build())
print(scene.stats["marker_instances"])
print(scene.stats["marker_buffer_bytes"])
for node in scene.walk():
    if isinstance(node.prim, MarkerBatchPrim):
        first = next(node.prim.records())
        # (x_mm, y_mm, diameter_mm, palette_index, original_source_index)
        print(first)

marker_instances counts placed records; marker_buffer_bytes counts unique retained buffers once even if placed repeatedly. Geometry reuse keeps a buffer when inherited paint or placement changes. Editing the data currently replaces the complete affected buffer.

Inspection change: dense scatter has one drawing handle per batch, instead of one handle per point. resolve() and flatten() expose that batch. Retained SVG marker elements have a data-source-index attribute; combine this with the containing batch node to identify a source record. This is an inspection contract, not an interactive picking API or a spatial index.

Measurements

A 10,000-point, single-colour vector layer, using the median of three fresh Python processes per source revision on Linux/WSL, Python 3.12.3:

Work or storage Before batching Packed markers
Authoring, theme and layout measurement 0.640 s 0.0201 s
Scene compilation 0.208 s 0.000108 s
SVG serialization 0.0552 s 0.0215 s
PDF serialization 0.232 s 0.226 s
Peak process memory 113.8 MiB 44.0 MiB
Scene nodes 20,003 4
SVG size 1,630,518 bytes 1,190,567 bytes
PDF size 82,692 bytes 82,719 bytes

The largest gains are in authoring, compilation and memory. PDF still paints every marker and has essentially unchanged serialization cost and file size. The benchmark excludes imports from timings; peak process memory includes imports, construction and native exports. It does not measure document-wide layout, diagnostics or viewer rendering.

A separate million-point construction and compilation run took 1.89 s for authoring, used a 36 MB buffer and peaked at 247.5 MiB for the process. That run deliberately excludes export. It establishes storage and construction capacity, not interactive or million-point vector-viewer performance.

Reproduce the measurements with the benchmark tool:

python tools/benchmark_batches.py --count 10000
python tools/benchmark_batches.py --count 1000000 --compile-only
python tools/benchmark_batches.py --source /path/to/previous-checkout

Remaining engine work

The subsequent compiled-scene viewer adds a WebGL2 filled-marker path with Canvas fallback and native SVG artwork. Indexed picking, partial buffer updates, packed maps and meshes, and finer document-layout invalidation remain separate stages. Native vector output still contains one painted element per marker. Trace and candidate overlap queries scan records; they do not yet use a spatial index.

Search documentation

Type to search guides, examples and the API.