Stacked Bar Chart
This example shows the inspector alongside a Visa Chart Components stacked bar chart. The chart's built-in keyboard navigation is disabled — instead, data-navigator handles navigation on the chart, and the inspector passively shows the structure being traversed.
Keyboard Controls
| Command | Key |
|---|---|
| Enter the structure | Activate the "Enter navigation area" button |
| Exit | Esc |
| Left (backward along category) | ← |
| Right (forward along category) | → |
| Up (backward along date) | ↑ |
| Down (forward along date) | ↓ |
| Drill down to child | Enter |
| Drill up to category parent | W |
| Drill up to date parent | J |
At the deepest level, left/right moves across dates (via childmostNavigation: 'across') and up/down moves across categories. Both dimensions wrap around circularly.
Chart + Inspector
Stacked Bar Chart
Structure Inspector
About This Example
The stacked bar chart is rendered by @visa/stacked-bar-chart via CDN, with its built-in keyboard navigation disabled. Data Navigator handles all navigation via its rendering and input modules on the chart wrapper, and the inspector graph passively shows the hierarchical structure being traversed.
Navigation uses childmostNavigation: 'across', which means left/right always moves across dates even at the deepest level — intuitive for a stacked bar chart where left/right should always move across time.
The Complete Code
This code is designed to work without a bundler. Run npm install data-navigator @data-navigator/inspector, copy the files into a src/ directory, and open index.html in your browser. The HTML uses an import map to resolve bare module specifiers, and loads the Visa stacked bar chart component and D3 from CDNs.
If you're using a bundler (Vite, Webpack, etc.), you can simplify the imports to import dataNavigator from 'data-navigator' and import { Inspector, buildLabel } from '@data-navigator/inspector', and remove the import map and CDN script tags from the HTML.
The structure is generated by the dimensions API — two categorical dimensions (category and date) create a dual hierarchy. Category is listed first (left/right), date is second (up/down). coordinator.js wires everything together. chart.js creates the Visa stacked bar chart and provides highlight functions. structure.js defines the data and generates the navigable graph via the dimensions API. input.js creates the keyboard handler.
import { structure, entryPoint, data } from './structure.js';
import { createChart, updateChartHighlight, clearChartHighlight } from './chart.js';
import { createInput } from './input.js';
import { Inspector, buildLabel } from '@data-navigator/inspector';
import dataNavigator from 'data-navigator';
// Assumes the page has:
// <div id="stacked-chart-wrapper" style="position: relative;"></div>
// <button id="toggle-inspector-mode"></button>
// <div id="inspector"></div>
let current = null;
let previous = null;
let input;
let exitHandler = null;
// Create the Visa stacked bar chart
const stackedBar = createChart('stacked-chart-wrapper', data);
// Create the inspector (passive — defaults to tree layout)
let currentMode = 'tree';
const createInspector = mode =>
Inspector({
structure,
container: 'inspector',
size: 325,
colorBy: 'dimensionLevel',
edgeExclusions: ['any-exit'],
nodeInclusions: ['exit'],
mode
});
let inspector = createInspector(currentMode);
// Toggle button switches between tree and force modes
const toggleBtn = document.getElementById('toggle-inspector-mode');
if (toggleBtn) {
const updateLabel = () => {
toggleBtn.textContent = currentMode === 'tree' ? 'Switch to force graph' : 'Switch to tree layout';
};
updateLabel();
toggleBtn.addEventListener('click', () => {
currentMode = currentMode === 'tree' ? 'force' : 'tree';
inspector.destroy();
inspector = createInspector(currentMode);
updateLabel();
});
}
function enter() {
const nextNode = input.enter();
if (nextNode) initiateLifecycle(nextNode);
}
const rendering = dataNavigator.rendering({
elementData: structure.nodes,
defaults: { cssClass: 'dn-node' },
suffixId: 'stacked-bar-example',
root: {
id: 'stacked-chart-wrapper',
description: 'Stacked bar chart with category and date dimensions.',
width: '100%',
height: 0
},
entryButton: { include: true, callbacks: { click: enter } },
exitElement: { include: true }
});
rendering.initialize();
exitHandler = () => {
rendering.exitElement.style.display = 'block';
input.focus(rendering.exitElement.id);
if (current) {
rendering.remove(current);
current = null;
}
inspector.clear();
clearChartHighlight(stackedBar);
};
input = createInput(structure, entryPoint, rendering.exitElement?.id);
function move(direction) {
const nextNode = input.move(current, direction);
if (nextNode) initiateLifecycle(nextNode);
}
function initiateLifecycle(nextNode) {
if (!nextNode.renderId) {
nextNode.renderId = nextNode.id;
}
if (!nextNode.spatialProperties) {
nextNode.spatialProperties = { x: 0, y: 15, width: 250, height: 200 };
}
if (!nextNode.semantics?.label) {
nextNode.semantics = { ...nextNode.semantics, label: buildLabel(nextNode) };
}
if (previous) rendering.remove(previous);
const element = rendering.render({
renderId: nextNode.renderId,
datum: nextNode
});
element.addEventListener('keydown', e => {
const direction = input.keydownValidator(e);
if (direction) {
e.preventDefault();
move(direction);
}
});
element.addEventListener('focus', () => {
inspector.highlight(nextNode.renderId);
updateChartHighlight(stackedBar, nextNode);
});
element.addEventListener('blur', () => {
inspector.clear();
});
input.focus(nextNode.renderId);
previous = current;
current = nextNode.id;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import dataNavigator from 'data-navigator';
const createValidId = s => '_' + s.replace(/[^a-zA-Z0-9_-]+/g, '_');
export const data = [
{ date: 'Jan', category: 'Group A', value: 120, count: 420, selectAll: 'yes' },
{ date: 'Feb', category: 'Group A', value: 121, count: 439, selectAll: 'yes' },
{ date: 'Mar', category: 'Group A', value: 119, count: 402, selectAll: 'yes' },
{ date: 'Apr', category: 'Group A', value: 114, count: 434, selectAll: 'yes' },
{ date: 'May', category: 'Group A', value: 102, count: 395, selectAll: 'yes' },
{ date: 'Jun', category: 'Group A', value: 112, count: 393, selectAll: 'yes' },
{ date: 'Jul', category: 'Group A', value: 130, count: 445, selectAll: 'yes' },
{ date: 'Aug', category: 'Group A', value: 124, count: 456, selectAll: 'yes' },
{ date: 'Sep', category: 'Group A', value: 119, count: 355, selectAll: 'yes' },
{ date: 'Oct', category: 'Group A', value: 106, count: 464, selectAll: 'yes' },
{ date: 'Nov', category: 'Group A', value: 123, count: 486, selectAll: 'yes' },
{ date: 'Dec', category: 'Group A', value: 133, count: 491, selectAll: 'yes' },
{ date: 'Jan', category: 'Group B', value: 89, count: 342, selectAll: 'yes' },
{ date: 'Feb', category: 'Group B', value: 93, count: 434, selectAll: 'yes' },
{ date: 'Mar', category: 'Group B', value: 82, count: 378, selectAll: 'yes' },
{ date: 'Apr', category: 'Group B', value: 92, count: 323, selectAll: 'yes' },
{ date: 'May', category: 'Group B', value: 90, count: 434, selectAll: 'yes' },
{ date: 'Jun', category: 'Group B', value: 85, count: 376, selectAll: 'yes' },
{ date: 'Jul', category: 'Group B', value: 88, count: 404, selectAll: 'yes' },
{ date: 'Aug', category: 'Group B', value: 84, count: 355, selectAll: 'yes' },
{ date: 'Sep', category: 'Group B', value: 90, count: 432, selectAll: 'yes' },
{ date: 'Oct', category: 'Group B', value: 80, count: 455, selectAll: 'yes' },
{ date: 'Nov', category: 'Group B', value: 92, count: 445, selectAll: 'yes' },
{ date: 'Dec', category: 'Group B', value: 97, count: 321, selectAll: 'yes' },
{ date: 'Jan', category: 'Group C', value: 73, count: 456, selectAll: 'yes' },
{ date: 'Feb', category: 'Group C', value: 74, count: 372, selectAll: 'yes' },
{ date: 'Mar', category: 'Group C', value: 68, count: 323, selectAll: 'yes' },
{ date: 'Apr', category: 'Group C', value: 66, count: 383, selectAll: 'yes' },
{ date: 'May', category: 'Group C', value: 72, count: 382, selectAll: 'yes' },
{ date: 'Jun', category: 'Group C', value: 70, count: 365, selectAll: 'yes' },
{ date: 'Jul', category: 'Group C', value: 74, count: 296, selectAll: 'yes' },
{ date: 'Aug', category: 'Group C', value: 68, count: 312, selectAll: 'yes' },
{ date: 'Sep', category: 'Group C', value: 75, count: 334, selectAll: 'yes' },
{ date: 'Oct', category: 'Group C', value: 66, count: 386, selectAll: 'yes' },
{ date: 'Nov', category: 'Group C', value: 85, count: 487, selectAll: 'yes' },
{ date: 'Dec', category: 'Group C', value: 89, count: 512, selectAll: 'yes' },
{ date: 'Jan', category: 'Other', value: 83, count: 432, selectAll: 'yes' },
{ date: 'Feb', category: 'Other', value: 87, count: 364, selectAll: 'yes' },
{ date: 'Mar', category: 'Other', value: 76, count: 334, selectAll: 'yes' },
{ date: 'Apr', category: 'Other', value: 86, count: 395, selectAll: 'yes' },
{ date: 'May', category: 'Other', value: 87, count: 354, selectAll: 'yes' },
{ date: 'Jun', category: 'Other', value: 77, count: 386, selectAll: 'yes' },
{ date: 'Jul', category: 'Other', value: 79, count: 353, selectAll: 'yes' },
{ date: 'Aug', category: 'Other', value: 85, count: 288, selectAll: 'yes' },
{ date: 'Sep', category: 'Other', value: 87, count: 353, selectAll: 'yes' },
{ date: 'Oct', category: 'Other', value: 76, count: 322, selectAll: 'yes' },
{ date: 'Nov', category: 'Other', value: 96, count: 412, selectAll: 'yes' },
{ date: 'Dec', category: 'Other', value: 104, count: 495, selectAll: 'yes' }
];
// Two categorical dimensions create a dual hierarchy.
// Category is first (left/right), date is second (up/down).
// Both use circular extents (wraps around) and childmostNavigation: 'across'
// (all four arrow keys available at leaf level).
export const structure = dataNavigator.structure({
data,
idKey: 'id',
addIds: true,
dimensions: {
values: [
{
dimensionKey: 'category',
type: 'categorical',
divisionOptions: {
divisionNodeIds: (dimensionKey, keyValue, i) => {
return createValidId(dimensionKey + keyValue + i);
}
},
behavior: {
extents: 'circular',
childmostNavigation: 'across'
}
},
{
dimensionKey: 'date',
type: 'categorical',
behavior: {
extents: 'circular',
childmostNavigation: 'across'
},
operations: {
sortFunction: (a, b) => {
if (a.values) {
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];
let aMonth =
a.values[Object.keys(a.values)[0]].date ||
a.values[Object.keys(a.values)[0]].data?.date;
let bMonth =
b.values[Object.keys(b.values)[0]].date ||
b.values[Object.keys(b.values)[0]].data?.date;
return months.indexOf(aMonth) - months.indexOf(bMonth);
}
}
}
}
]
},
genericEdges: [
{
edgeId: 'any-exit',
edge: {
source: (_d, c) => c,
target: () => '',
navigationRules: ['exit']
}
}
]
});
export const entryPoint = structure.dimensions[Object.keys(structure.dimensions)[0]].nodeId;2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// Creates a Visa stacked bar chart web component inside the given container.
export function createChart(containerId, data) {
const wrapper = document.getElementById(containerId);
const stackedBar = document.createElement('stacked-bar-chart');
const props = {
mainTitle: '',
subTitle: '',
data,
height: 200,
width: 250,
padding: { top: 10, bottom: 10, right: 10, left: 30 },
colors: ['#FFFFFF', '#DDDDDD', '#BBBBBB', '#999999'],
ordinalAccessor: 'category',
valueAccessor: 'value',
groupAccessor: 'date',
uniqueID: 'examples-stacked-bar',
legend: { labels: ['A', 'B', 'C', 'Other'] },
dataLabel: { visible: false },
yAxis: { visible: true, gridVisible: false },
xAxis: { label: '', visible: false },
showTotalValue: false,
suppressEvents: true,
layout: 'horizontal',
clickHighlight: [],
clickStyle: { color: '#222', strokeWidth: 1 },
interactionKeys: [],
accessibility: {
elementsAreInterface: false,
disableValidation: true,
hideDataTableButton: true,
keyboardNavConfig: { disabled: true },
hideTextures: true,
hideStrokes: false
}
};
Object.keys(props).forEach(prop => {
stackedBar[prop] = props[prop];
});
wrapper.appendChild(stackedBar);
return stackedBar;
}
// Highlights bars on the chart based on the current node type.
export function updateChartHighlight(stackedBar, node) {
if (!node.derivedNode) {
// Leaf node — highlight specific bar segment
stackedBar.clickHighlight = [{ category: node.data.category, date: node.data.date }];
stackedBar.interactionKeys = ['category', 'date'];
} else if (node.data?.dimensionKey) {
// Dimension node — highlight all bars
stackedBar.clickHighlight = [{ selectAll: 'yes' }];
stackedBar.interactionKeys = ['selectAll'];
} else {
// Division node — highlight group
const key = node.derivedNode;
const value = node.data?.[key];
stackedBar.clickHighlight = [{ [key]: value }];
stackedBar.interactionKeys = [key];
}
}
export function clearChartHighlight(stackedBar) {
stackedBar.clickHighlight = [];
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import dataNavigator from 'data-navigator';
export function createInput(structure, entryPoint, exitPointId) {
return dataNavigator.input({
structure,
navigationRules: structure.navigationRules,
entryPoint,
exitPoint: exitPointId
});
}2
3
4
5
6
7
8
9
10
<html>
<head>
<link rel="stylesheet" href="./src/style.css" />
<script type="importmap">
{
"imports": {
"data-navigator": "./node_modules/data-navigator/dist/index.mjs",
"@data-navigator/inspector": "./node_modules/@data-navigator/inspector/src/inspector.js",
"d3-array": "https://cdn.jsdelivr.net/npm/d3-array@3/+esm",
"d3-drag": "https://cdn.jsdelivr.net/npm/d3-drag@3/+esm",
"d3-force": "https://cdn.jsdelivr.net/npm/d3-force@3/+esm",
"d3-scale": "https://cdn.jsdelivr.net/npm/d3-scale@4/+esm",
"d3-scale-chromatic": "https://cdn.jsdelivr.net/npm/d3-scale-chromatic@3/+esm",
"d3-selection": "https://cdn.jsdelivr.net/npm/d3-selection@3/+esm"
}
}
</script>
</head>
<body>
<div style="display: flex; gap: 2em; flex-wrap: wrap; align-items: flex-start;">
<div>
<h3>Stacked Bar Chart</h3>
<div id="stacked-chart-wrapper" style="position: relative;"></div>
</div>
<div>
<h3>Structure Inspector</h3>
<button id="toggle-inspector-mode">Switch to force graph</button>
<div id="inspector" style="min-height: 350px;"></div>
</div>
</div>
</body>
<script
src="https://unpkg.com/@visa/stacked-bar-chart@7/dist/stacked-bar-chart/stacked-bar-chart.esm.js"
type="module"
></script>
<script type="module" src="./src/coordinator.js"></script>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.dn-node {
pointer-events: none;
background: transparent;
border: none;
position: absolute;
margin: 0;
}
.dn-node:focus {
outline: 2px solid #1e3369;
}2
3
4
5
6
7
8
9
10
11
You can also find this example as a ready-to-run project on GitHub.