Skip to content

Dimensions API Example

The dimensions API is one of the most powerful features of Data Navigator. Instead of manually defining every node and edge, you describe your data's dimensions — the meaningful axes along which a user might want to navigate — and Data Navigator automatically builds a hierarchical navigation structure.

This example uses the same dataset as the Stacked Bar Chart example but rendered as a line chart, to show that the same structure works across different visual representations.

Keyboard Controls

At the deepest level, left/right moves across categories and up/down moves across dates. Both dimensions wrap around circularly.

Chart + Inspector

Line Chart

Structure Inspector

How the Dimensions API Works

When you pass a dimensions configuration to dataNavigator.structure(), Data Navigator builds a multi-level hierarchy from your flat data automatically. Each dimension becomes a level in the navigation tree.

Date dimension (left/right). The first dimension listed, date, is categorical. Data Navigator groups the data by date and creates a dimension node at the top with division nodes underneath — one for each unique date value (Jan through Dec). Pressing Enter from the date dimension drills down into these divisions. Use and to move between divisions. Since this dimension uses circular extents, movement wraps around from December back to January. The custom sortFunction ensures dates appear in calendar order rather than alphabetical order.

Category dimension (up/down). The second dimension, category, is also categorical. Within each date division, data points are further grouped by category. Pressing Enter from a date division drills into the category sub-groups within that date. Use and to move between category divisions (Group A, Group B, Group C, Other).

At the child-most level, all four arrow keys are available regardless of which dimension you drilled down from. This is because both dimensions use childmostNavigation: 'across', which tells Data Navigator to make the sibling navigation for each dimension available at the leaf level. So and move across categories while and move across dates — even though you may have arrived by drilling down through dates first. This means you can freely explore data points in any direction once you reach the bottom of the hierarchy.

Drilling back up. Each dimension gets its own "drill up" key. Press W to drill up to the date parent, or J to drill up to the category parent. These keys are automatically assigned from Data Navigator's default key pool.

This dual-hierarchy structure — where two dimensions create an interconnected tree that users can traverse in any direction at the leaves — is what makes the dimensions API powerful. You describe the structure declaratively and Data Navigator handles all the edge generation.

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 line 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 (date and category) create a dual hierarchy that users can traverse in any direction at the leaf level. coordinator.js wires everything together. chart.js creates the Visa line 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.

js
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="line-chart-wrapper" style="position: relative;"></div>
//   <div id="inspector"></div>

let current = null;
let previous = null;
let input;
let exitHandler = null;

// Create the Visa line chart
const lineChart = createChart('line-chart-wrapper', data);

// Create the inspector (passive — just visualizes the structure)
const inspector = Inspector({
    structure,
    container: 'inspector',
    size: 325,
    colorBy: 'dimensionLevel',
    edgeExclusions: ['any-exit'],
    nodeInclusions: ['exit']
});

function enter() {
    const nextNode = input.enter();
    if (nextNode) initiateLifecycle(nextNode);
}

const rendering = dataNavigator.rendering({
    elementData: structure.nodes,
    defaults: { cssClass: 'dn-node' },
    suffixId: 'dimensions-example',
    root: {
        id: 'line-chart-wrapper',
        description: 'Line 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(lineChart);
};

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: 450, 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(lineChart, nextNode);
    });

    element.addEventListener('blur', () => {
        inspector.clear();
    });

    input.focus(nextNode.renderId);
    previous = current;
    current = nextNode.id;
}
js
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.
// Date is first (left/right), category 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: '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);
                        }
                    }
                }
            },
            {
                dimensionKey: 'category',
                type: 'categorical',
                divisionOptions: {
                    divisionNodeIds: (dimensionKey, keyValue, i) => {
                        return createValidId(dimensionKey + keyValue + i);
                    }
                },
                behavior: {
                    extents: 'circular',
                    childmostNavigation: 'across'
                }
            }
        ]
    },
    genericEdges: [
        {
            edgeId: 'any-exit',
            edge: {
                source: (_d, c) => c,
                target: () => '',
                navigationRules: ['exit']
            }
        }
    ]
});

export const entryPoint = structure.dimensions[Object.keys(structure.dimensions)[0]].nodeId;
js
// Creates a Visa line chart web component inside the given container.
export function createChart(containerId, data) {
    const wrapper = document.getElementById(containerId);
    const lineChart = document.createElement('line-chart');
    const props = {
        mainTitle: '',
        subTitle: '',
        data,
        height: 200,
        width: 450,
        padding: { top: 10, bottom: 30, right: 0, left: 20 },
        colors: ['#999999', '#BBBBBB', '#DDDDDD', '#FFFFFF'],
        ordinalAccessor: 'date',
        seriesLabel: { visible: false },
        valueAccessor: 'value',
        seriesAccessor: 'category',
        uniqueID: 'examples-line-chart',
        dataLabel: { visible: false },
        legend: { visible: false },
        yAxis: { visible: true, gridVisible: false },
        xAxis: { visible: true, label: '' },
        suppressEvents: true,
        clickHighlight: [],
        clickStyle: { color: '#222', strokeWidth: 2 },
        interactionKeys: [],
        strokeWidth: 1,
        accessibility: {
            elementsAreInterface: false,
            disableValidation: true,
            hideDataTableButton: true,
            keyboardNavConfig: { disabled: true },
            hideTextures: true,
            hideStrokes: false
        }
    };
    Object.keys(props).forEach(prop => {
        lineChart[prop] = props[prop];
    });
    wrapper.appendChild(lineChart);
    return lineChart;
}

// Highlights lines/points based on the current node type.
export function updateChartHighlight(lineChart, node) {
    if (!node.derivedNode) {
        // Leaf node — highlight specific data point
        lineChart.clickHighlight = [{ category: node.data.category, date: node.data.date }];
        lineChart.interactionKeys = ['category', 'date'];
    } else if (node.data?.dimensionKey) {
        // Dimension node — highlight all lines
        lineChart.clickHighlight = [{ selectAll: 'yes' }];
        lineChart.interactionKeys = ['selectAll'];
    } else {
        // Division node — highlight group
        const key = node.derivedNode;
        const value = node.data?.[key];
        lineChart.clickHighlight = [{ [key]: value }];
        lineChart.interactionKeys = [key];
    }
}

export function clearChartHighlight(lineChart) {
    lineChart.clickHighlight = [];
}
js
import dataNavigator from 'data-navigator';

export function createInput(structure, entryPoint, exitPointId) {
    return dataNavigator.input({
        structure,
        navigationRules: structure.navigationRules,
        entryPoint,
        exitPoint: exitPointId
    });
}
html
<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>Line Chart</h3>
                <div id="line-chart-wrapper" style="position: relative;"></div>
            </div>
            <div>
                <h3>Structure Inspector</h3>
                <div id="inspector" style="min-height: 350px;"></div>
            </div>
        </div>
    </body>
    <script src="https://unpkg.com/@visa/line-chart@6/dist/line-chart/line-chart.esm.js" type="module"></script>
    <script type="module" src="./src/coordinator.js"></script>
</html>
css
.dn-node {
    pointer-events: none;
    background: transparent;
    border: none;
    position: absolute;
    margin: 0;
}

.dn-node:focus {
    outline: 2px solid #1e3369;
}

You can also find this example as a ready-to-run project on GitHub.

Released under the MIT License.