Home Try Online Gallery Docs GitHub

Simple Tooltip

This builds on the Simple Camera by adding a tooltip that displays data values for the mark under the pointer. It uses a second renderer in segment mode to map pixel positions to data rows.

The example on this page can be tried directly using the link below. To build and run your own version, make sure you have the prerequisites in place.

Overview

The differences from the simple camera are:

  1. The spec includes inline data and scales to create a data-driven bar chart
  2. A second renderer runs in segment mode with idSource set to 'pick', rendering a single frame to produce a pick image
  3. On mouse move, the pick image pixel under the pointer is read, decoded to a pick ID, and looked up via Core.Pick.get() to retrieve the corresponding data row
  4. A tooltip element displays the data values for the picked row
  5. The pick image is regenerated after camera moves settle

Complete Example

Rendered result — a tooltip showing data values for the mark under the pointer
Figure 1. Rendered result — a tooltip showing data values for the mark under the pointer.

Try Online

Step-by-Step

The canvas, imports, renderer, spec parsing, scene loading, and camera manipulation steps are the same as the Simple Camera. The differences start at the segment renderer.

Data-Driven Spec

Unlike the previous examples, this spec includes inline data and scales to create a bar chart. The marks reference the data via from and map values through scales:

const specJSON = {
  "width": 640,
  "height": 360,
  "data": [
    {
      "name": "table",
      "values": [
        { "category": "A", "size": 20 },
        { "category": "B", "size": 40 },
        ...
      ]
    }
  ],
  "scales": [
    { "name": "x", "type": "band", "domain": { "data": "table", "field": "category" }, "range": "width" },
    { "name": "y", "type": "linear", "domain": { "data": "table", "field": "size" }, "range": "height" }
  ],
  "marks": [
    {
      "type": "rect",
      "from": { "data": "table" },
      "encode": {
        "enter": {
          "xc": { "scale": "x", "field": "category" },
          "height": { "scale": "y", "field": "size" },
          ...
        }
      }
    }
  ]
};

Segment Renderer

A second renderer renders a single frame in segment mode with idSource set to 'pick'. Each pixel in the segment image encodes a unique pick ID for the mark it covers:

const segmentCanvas = document.createElement('canvas');
segmentCanvas.width = canvas.width;
segmentCanvas.height = canvas.height;
const segmentRenderer = new WebGPURenderer.Main(segmentCanvas, {
  renderMode: 'segment',
});
segmentRenderer.idSource = 'pick';
await segmentRenderer.initializeAsync();
segmentRenderer.loadScene(scene);

The segment image is read back into a pixel array via a 2D canvas for CPU-side lookup. A regenerateSegmentImage function handles rendering and readback, and is called once at startup and again after camera moves settle:

const readbackCanvas = document.createElement('canvas');
readbackCanvas.width = canvas.width;
readbackCanvas.height = canvas.height;
const readbackCtx = readbackCanvas.getContext('2d', { willReadFrequently: true });
let segmentPixels;

async function regenerateSegmentImage() {
  segmentRenderer.copyCamera(camera);
  segmentRenderer.frameCount = 0;
  await segmentRenderer.updateAsync(0);
  await segmentRenderer.renderAsync(0);
  readbackCtx.drawImage(segmentCanvas, 0, 0);
  segmentPixels = readbackCtx.getImageData(0, 0, canvas.width, canvas.height).data;
  segmentDirty = false;
  if (lastMouseX >= 0) { updateTooltip(lastMouseX, lastMouseY); }
}

await regenerateSegmentImage();

Pick Lookup

On mouse move, the pixel under the pointer is read from the segment image and decoded to a pick ID. Core.Pick.get() maps the pick ID to a dataset and rowIndex:

const offset = (py * canvas.width + px) * 4;
const color = [
  segmentPixels[offset] / 255,
  segmentPixels[offset + 1] / 255,
  segmentPixels[offset + 2] / 255
];
const pickId = Core.Color.colorRGBToNumber(color);
const info = Core.Pick.get(pickId);
if (info) {
  const row = info.dataset.rows[info.rowIndex];
  // Display row data in tooltip
}

Tooltip Display

A tooltipFields array defines which data columns to show and their labels. The tooltip is positioned above the pointer:

const tooltipFields = [
  { field: 'category', label: 'Category' },
  { field: 'size', label: 'Size' },
];

// Build tooltip lines from matched fields
const lines = [];
for (const f of tooltipFields) {
  const col = dataset.headings.indexOf(f.field);
  if (col < 0) continue;
  lines.push(f.label + ': ' + row[col]);
}

The tooltip is hidden during camera drag, while the segment image is stale, and when the pointer leaves the canvas. After regeneration, the tooltip is automatically refreshed at the last known pointer position.

Segment Regeneration

After the camera moves, the pick image becomes stale. Camera interactions set segmentDirty = true and record the time of the last move. In the render loop, once segmentRegenerateDelay milliseconds have elapsed since the last interaction, regenerateSegmentImage is called to re-render and read back the segment image.