Basic Scatter Plot
A scatter plot of the classic Iris dataset — sepal length vs. petal length coloured by species. The wrapper uses type: 'cartesian' to build a dual-dimension navigation structure: navigate across sepal-length bins with ← →, navigate across petal-length bins with ↑ ↓. At the deepest level all four arrow keys stay active so users can roam freely across both axes.
Live example
Structure
For type: 'cartesian' the navigation hierarchy has two independent numerical dimensions that share the same leaf nodes. Each numerical dimension is automatically divided into bins:
chart root
├─ sepal_length dimension (← →)
│ ├─ bin 1 [~4.7 – 5.3]
│ │ ├─ s3: sepal 4.7, petal 1.3 ← leaf (all four arrow keys active)
│ │ ├─ s2: sepal 4.9, petal 1.4
│ │ └─ ... (setosa points)
│ ├─ bin 2 [~5.3 – 5.9]
│ │ └─ ...
│ └─ ... (bins continue to max sepal length)
└─ petal_length dimension (↑ ↓)
├─ bin 1 [~1.0 – 2.25]
│ └─ ... (same leaf nodes, reached via a different path)
└─ ...Navigation summary
| Location | ← → | ↑ ↓ | Enter | W | J | Backspace |
|---|---|---|---|---|---|---|
| Chart root | — | — | Go to sepal_length dimension | — | — | — |
| sepal_length dimension root | Go to petal_length dimension | — | Go to first bin | — | — | Chart root |
| sepal_length bin | Previous / next bin | — | Go to first leaf | sepal_length dimension | — | — |
| petal_length dimension root | Go to sepal_length dimension | — | Go to first bin | — | — | Chart root |
| petal_length bin | — | Previous / next bin | Go to first leaf | — | petal_length dimension | — |
| Leaf | Previous / next sepal bin | Previous / next petal bin | — | Parent sepal bin | Parent petal bin | — |
- The bin count is derived automatically as
ceil(sqrt(N))(minimum 3), where N is the number of data points. - Bins are sorted in ascending order and use terminal extents — navigation stops at the first / last bin rather than wrapping.
- Use
idFieldto specify which data field uniquely identifies each point; without it the wrapper generates sequential IDs.
Full code
Create three files in the same directory and serve them with a local server (e.g. npx serve . or python -m http.server). Bokeh is loaded from CDN; the wrapper is loaded via import map. The wrapper tab is the integration layer; chart is the Bokeh rendering code.
import { addDataNavigator } from '@data-navigator/bokeh-wrapper';
import { data, globalXMin, globalXMax, globalYMin, globalYMax, drawChart } from './chart.js';
let wrapper = null;
let rects = [];
let points = [];
let divisionRectsByDimension = {};
function buildDivisionRects() {
divisionRectsByDimension = {};
if (!wrapper) return;
for (const node of Object.values(wrapper.structure.nodes)) {
if (node.dimensionLevel === 2 && node.data?.numericalExtents) {
const dimKey = node.derivedNode;
const [lo, hi] = node.data.numericalExtents;
if (!divisionRectsByDimension[dimKey]) divisionRectsByDimension[dimKey] = [];
if (dimKey === 'sepal_length') {
divisionRectsByDimension[dimKey].push({ x1: lo, x2: hi, y1: globalYMin, y2: globalYMax, lineWidth: 1 });
} else {
divisionRectsByDimension[dimKey].push({ x1: globalXMin, x2: globalXMax, y1: lo, y2: hi, lineWidth: 1 });
}
}
}
}
function initWrapper(mode) {
wrapper?.destroy();
rects = [];
points = [];
divisionRectsByDimension = {};
drawChart({ rects, points });
wrapper = addDataNavigator({
plotContainer: 'scatter-plot',
chatContainer: 'scatter-chat',
mode,
data,
type: 'cartesian',
xField: 'sepal_length',
yField: 'petal_length',
idField: 'pt',
title: 'Iris: sepal length vs petal length',
onNavigate(node) {
const level = node.dimensionLevel;
if (level === 0) {
// Chart root — show all bins from both dimensions at once
points = [];
rects = Object.values(divisionRectsByDimension).flat();
} else if (level === 1) {
// Dimension root — show every bin of this dimension
points = [];
const dimKey = node.data?.dimensionKey;
rects = divisionRectsByDimension[dimKey] ?? [];
} else if (node.derivedNode) {
// Division node — exact bin boundary, 2px stroke
points = [];
const [lo, hi] = node.data?.numericalExtents ?? [0, 0];
if (node.derivedNode === 'sepal_length') {
rects = [{ x1: lo, x2: hi, y1: globalYMin, y2: globalYMax, lineWidth: 2 }];
} else {
rects = [{ x1: globalXMin, x2: globalXMax, y1: lo, y2: hi, lineWidth: 2 }];
}
} else {
// Leaf node — individual point indicator
rects = [];
points = [{ x: +node.data.sepal_length, y: +node.data.petal_length }];
}
drawChart({ rects, points });
},
onExit() {
rects = [];
points = [];
drawChart({ rects, points });
}
});
buildDivisionRects();
}
initWrapper('text');
document.getElementById('scatter-keyboard')?.addEventListener('change', e => {
initWrapper(e.target.checked ? 'keyboard' : 'text');
});export const data = [
{ pt: 's1', sepal_length: 5.1, petal_length: 1.4, species: 'setosa' },
{ pt: 's2', sepal_length: 4.9, petal_length: 1.4, species: 'setosa' },
{ pt: 's3', sepal_length: 4.7, petal_length: 1.3, species: 'setosa' },
{ pt: 's4', sepal_length: 5.8, petal_length: 1.2, species: 'setosa' },
{ pt: 's5', sepal_length: 5.0, petal_length: 1.0, species: 'setosa' },
{ pt: 'v1', sepal_length: 7.0, petal_length: 4.7, species: 'versicolor' },
{ pt: 'v2', sepal_length: 6.4, petal_length: 4.5, species: 'versicolor' },
{ pt: 'v3', sepal_length: 6.9, petal_length: 4.9, species: 'versicolor' },
{ pt: 'v4', sepal_length: 5.5, petal_length: 4.0, species: 'versicolor' },
{ pt: 'v5', sepal_length: 6.5, petal_length: 4.6, species: 'versicolor' },
{ pt: 'g1', sepal_length: 6.3, petal_length: 6.0, species: 'virginica' },
{ pt: 'g2', sepal_length: 5.8, petal_length: 5.1, species: 'virginica' },
{ pt: 'g3', sepal_length: 7.1, petal_length: 5.9, species: 'virginica' },
{ pt: 'g4', sepal_length: 6.3, petal_length: 5.6, species: 'virginica' },
{ pt: 'g5', sepal_length: 6.5, petal_length: 5.8, species: 'virginica' }
];
export const colors = { setosa: '#e41a1c', versicolor: '#377eb8', virginica: '#4daf4a' };
export const globalXMin = Math.min(...data.map(d => d.sepal_length));
export const globalXMax = Math.max(...data.map(d => d.sepal_length));
export const globalYMin = Math.min(...data.map(d => d.petal_length));
export const globalYMax = Math.max(...data.map(d => d.petal_length));
export function drawChart({ rects = [], points = [] } = {}) {
const container = document.getElementById('scatter-chart-inner');
container.innerHTML = '';
const plt = Bokeh.Plotting;
const p = plt.figure({
height: 320,
width: 480,
title: 'Iris: sepal length vs petal length',
x_axis_label: 'Sepal length (cm)',
y_axis_label: 'Petal length (cm)',
toolbar_location: null,
output_backend: 'svg'
});
// Focus rectangles (stroke only, no fill) drawn behind the data points
for (const rect of rects) {
p.quad({
left: [rect.x1],
right: [rect.x2],
bottom: [rect.y1],
top: [rect.y2],
fill_alpha: 0,
line_color: '#333',
line_width: rect.lineWidth
});
}
// All scatter points rendered identically — no dimming or size changes
data.forEach(d => {
p.scatter({
marker: 'circle',
x: [d.sepal_length],
y: [d.petal_length],
size: 8,
fill_color: colors[d.species],
line_color: colors[d.species],
line_width: 1,
fill_alpha: 0.7
});
});
// Focus indicator over individual points at leaf level
for (const point of points) {
p.scatter({
marker: 'circle',
x: [point.x],
y: [point.y],
size: 8,
fill_color: '#333',
line_color: '#333',
line_width: 2,
fill_alpha: 0
});
}
plt.show(p, '#scatter-chart-inner');
}<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Scatter Plot — Data Navigator Bokeh Wrapper</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/data-navigator/text-chat.css" />
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-3.7.3.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.7.3.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.7.3.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.bokeh.org/bokeh/release/bokeh-api-3.7.3.min.js" crossorigin="anonymous"></script>
<script type="importmap">
{
"imports": {
"@data-navigator/bokeh-wrapper": "https://esm.sh/@data-navigator/bokeh-wrapper",
"data-navigator": "https://esm.sh/data-navigator"
}
}
</script>
</head>
<body>
<div id="scatter-plot" style="display:inline-block;">
<div id="scatter-chart-inner"></div>
</div>
<label>
<input type="checkbox" id="scatter-keyboard" />
Use keyboard navigation
</label>
<div id="scatter-chat" style="max-width:500px;"></div>
<script type="module" src="./wrapper.js"></script>
</body>
</html>