Skip to content

Line Chart (multiple series)

A multi-line chart showing monthly average temperatures for three cities. The wrapper uses type: 'multiline' with groupField: 'city' to create a two-level structure: navigate between cities at the top level, then drill in to move through monthly data points.

Live example

Structure

For type: 'multiline' the navigation hierarchy is:

city dimension
  └─ New York division
       ├─ Jan: 0°C
       ├─ Feb: 2°C
       └─ ... (12 months)
  └─ London division
       └─ ...
  └─ Sydney division
       └─ ...
  • left / right navigates between city groups at the top level
  • child drills into the months for the current city
  • left / right (inside a city) navigates between months
  • parent returns to the city level

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.

js
import { addDataNavigator } from '@data-navigator/bokeh-wrapper';
import { data, drawChart } from './chart.js';

let wrapper = null;
let focusedCity = null;
let focusedMonth = null;

function initWrapper(mode) {
    wrapper?.destroy();
    focusedCity = null;
    focusedMonth = null;
    drawChart({ focusedCity, focusedMonth });
    wrapper = addDataNavigator({
        plotContainer: 'line-plot',
        chatContainer: 'line-chat',
        mode,
        data,
        type: 'multiline',
        xField: 'month',
        yField: 'temp_c',
        groupField: 'city',
        commandLabels: {
            left: 'Move to previous city',
            right: 'Move to next city',
            child: 'Drill into monthly data for this city',
            parent: 'Go back to cities'
        },
        onNavigate(node) {
            if (node.derivedNode) {
                const city = node.data?.city ?? node.data?.[node.derivedNode] ?? null;
                focusedCity = city ?? '__all__';
                focusedMonth = null;
            } else {
                focusedCity = node.data?.city ?? null;
                focusedMonth = node.data?.month ?? null;
            }
            drawChart({ focusedCity, focusedMonth });
        },
        onExit() {
            focusedCity = null;
            focusedMonth = null;
            drawChart({ focusedCity, focusedMonth });
        }
    });
}

initWrapper('text');

document.getElementById('line-keyboard')?.addEventListener('change', e => {
    initWrapper(e.target.checked ? 'keyboard' : 'text');
});
js
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

const cityData = {
    'New York': [0, 2, 7, 13, 19, 24, 27, 26, 22, 15, 9, 3],
    London: [5, 5, 7, 10, 14, 17, 19, 18, 15, 11, 7, 5],
    Sydney: [22, 22, 20, 17, 14, 11, 10, 11, 13, 16, 19, 21]
};

const colors = { 'New York': '#e41a1c', London: '#377eb8', Sydney: '#ff7f00' };

// Flatten for data-navigator: one row per (city, month) combination.
export const data = [];
for (const [city, temps] of Object.entries(cityData)) {
    months.forEach((month, i) => {
        data.push({ city, month, month_index: i, temp_c: temps[i] });
    });
}

export function drawChart({ focusedCity = null, focusedMonth = null } = {}) {
    const container = document.getElementById('line-chart-inner');
    container.innerHTML = '';
    const plt = Bokeh.Plotting;
    const p = plt.figure({
        height: 300,
        width: 550,
        title: 'Monthly average temperatures',
        x_range: months,
        y_axis_label: '°C',
        toolbar_location: null,
        output_backend: 'svg'
    });

    for (const [city, temps] of Object.entries(cityData)) {
        const isFocused = focusedCity === '__all__' || city === focusedCity;
        const dimmed = focusedCity != null && focusedCity !== '__all__' && !isFocused;
        p.line(months, temps, {
            line_color: colors[city],
            line_width: isFocused && focusedMonth == null ? 3 : 1.5,
            line_alpha: dimmed ? 0.3 : 1.0,
            legend_label: city
        });
        if (isFocused && focusedCity !== '__all__' && focusedMonth != null) {
            const idx = months.indexOf(focusedMonth);
            if (idx >= 0) {
                p.scatter({
                    x: [focusedMonth],
                    y: [temps[idx]],
                    marker: 'circle',
                    size: 12,
                    fill_color: colors[city],
                    line_color: '#000',
                    line_width: 2
                });
            }
        }
    }

    p.legend.location = 'top_left';
    plt.show(p, '#line-chart-inner');
}
html
<!doctype html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Line Chart — 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="line-plot" style="display:inline-block;">
            <div id="line-chart-inner"></div>
        </div>
        <label>
            <input type="checkbox" id="line-keyboard" />
            Use keyboard navigation
        </label>
        <div id="line-chat" style="max-width:500px;"></div>
        <script type="module" src="./wrapper.js"></script>
    </body>
</html>

Released under the MIT License.