Arsalan Khattak
11 March 2026

Integrating Bryntum Calendar with Highcharts

Calendar with HighCharts
The Bryntum Calendar is a modern, high-performance calendar component offering multiple views out of the box. Built with pure JavaScript, […]

The Bryntum Calendar is a modern, high-performance calendar component offering multiple views out of the box. Built with pure JavaScript, it integrates seamlessly with React, Angular, and Vue applications.

In this guide, you’ll learn how to integrate Bryntum Calendar with Highcharts, a powerful JavaScript charting library for building interactive, responsive, and accessible data visualizations. We’ll combine both libraries to create a unified view where calendar data is visualized in dynamic charts, enabling deeper insights directly from your scheduling data.

By the end of this tutorial, you’ll have a Bryntum Calendar with an interactive chart panel beside it, similar to the following:

The application provides:

You can explore the complete, working demo on the Bryntum website: Calendar with Highcharts. This demo showcases the final result of the integration you’ll build in this guide.

Getting Started

We’ll begin by creating a vanilla JavaScript project. While Bryntum Calendar works excellently with React, Angular, and Vue, we’ll use vanilla JavaScript to keep the focus on the integration itself.

Clone the Starter Template

We’ve prepared a starter template based on Vite that includes the basic project structure and configuration. Clone it from GitHub:

git clone https://github.com/bryntum/bryntum-calendar-highcharts.git

cd bryntum-calendar-highcharts

This starter template includes:

Install Dependencies

First, install the project dependencies including Highcharts:

npm install

Next, install Highcharts

npm install highcharts

Install Bryntum Calendar

Bryntum components are licensed commercial products, but you can use the free trial version for this tutorial.

For the trial version:

npm install @bryntum/calendar@npm:@bryntum/calendar-trial

For licensed users:

npm install @bryntum/calendar

Run the Development Server

Start the Vite development server:

npm run dev

Your application will be available at http://localhost:5173/.

Create the Custom Highcharts Widget

First, we’ll create a custom Bryntum widget that wraps Highcharts. Create lib/HighchartsWidget.js:

import { Widget } from '@bryntum/calendar';
import Highcharts from 'highcharts';

export default class HighchartsWidget extends Widget {
    static $name = 'Chart';
    static type = 'chart';

    static configurable = {
        chartOptions : null
    };

    compose() {
        return {
            class    : 'b-chart',
            style    : 'width:100%;height:100%;',
            children : {
                chartElement : {
                    tag   : 'div',
                    class : 'b-highcharts-container',
                    style : 'width:100%;height:100%;'
                }
            }
        };
    }

    renderChart() {
        if (!this.chartElement) {
            return;
        }

        const options = this._chartOptions || this.chartOptions || {};
        this.chart = Highcharts.chart(this.chartElement, options);
    }

    changeChartOptions(options) {
        this._chartOptions = Highcharts.merge(
            this._chartOptions || {},
            options || {}
        );
        if (this.chart) {
            this.chart.update(options, true, true);
        }
        else {
            this.renderChart();
        }
    }

    doDestroy() {
        if (this.chart) {
            this.chart.destroy();
            this.chart = null;
        }
        super.doDestroy();
    }
}

HighchartsWidget.initClass();

This widget extends Bryntum’s Widget class and provides:

Creating the Calendar and Chart Panel Component

Set Up the Main Container

First, create the container with a horizontal box layout in main.js:

import { DateHelper, Container, Splitter } from '@bryntum/calendar';
import Highcharts from 'highcharts';
import './lib/HighchartsWidget.js';
import './style.css';

const container = new Container({
    appendTo : 'app',
    cls      : 'outer-container',
    flex     : 1,
    layout   : {
        type : 'hbox'
    },
    resourceImagePath : '/users/',
    items : {
        // Calendar and chart panel will go here
    }
});

This container uses an hbox layout to position the calendar and chart side by side. The resourceImagePath points to user avatar images for display in tooltips.

Configure the Calendar

Add the calendar configuration inside the items object:

calendar : {
    type    : 'calendar',
    date    : '2026-06-02',
    flex    : 2.5,
    sidebar : {
        collapsed      : true,
        resourceFilter : {
            filterResources : true,
            onChange        : ({ value }) => {
                updateChart(value);
            }
        }
    },
    crudManager : {
        loadUrl       : 'data/data.json',
        autoLoad      : true,
        resourceStore : {
            fields : ['barColor']
        },
        listeners : {
            load() {
                updateChart();
            },
            hasChanges() {
                updateChart();
            }
        }
    },
    modes : {
        year   : false,
        agenda : false
    },
    hideNonWorkingDays : true,
    listeners          : {
        dateChange({ source }) {
            source.eventStore.count && updateChart();
        }
    }
},

Key calendar features:

Configure the Chart Panel

Add the chart panel configuration after the calendar:

chartPanel : {
    type     : 'panel',
    flex     : 1,
    minWidth : 400,
    layout   : 'fit',
    items    : {
        chart : {
            type         : 'chart',
            chartOptions : {
                chart : {
                    type  : 'column',
                    style : {
                        fontFamily : 'inherit'
                    }
                },
                legend : {
                    enabled : false
                },
                yAxis : {
                    title : {
                        text : ''
                    }
                },
                tooltip : {
                       useHTML : true,
                       style   : {
                           fontSize   : '14px',
                           lineHeight : '1.6'
                       },
                       borderRadius : 10,
                       padding      : 20,
                       shadow       : {
                           color   : 'rgba(0, 0, 0, 0.12)',
                           offsetX : 0,
                           offsetY : 1,
                           width   : 5,
                           opacity : 1
                       }
                },
                plotOptions : {
                    column : {
                        borderWidth  : 2,
                        borderColor  : 'transparent',
                        borderRadius : 5,
                        animation    : {
                            duration : 500
                        }
                    }
                },
                series : [{ name : 'Events' }]
            }
        }
    },
    tbar : {
        items : {
            datasetButtons : {
                type        : 'buttongroup',
                toggleGroup : true,
                rendition   : 'padded',
                items       : {
                    perDay      : { text : 'Events per day', value : 'perDay' },
                    perResource : {
                        text    : 'Events per resource',
                        value   : 'perResource',
                        pressed : true
                    }
                },
                onToggle({ pressed }) {
                    if (pressed) {
                        updateChart();
                    }
                }
            }
        }
    }
}

Add a Splitter

To allow users to dynamically resize the calendar and chart panels, add a splitter between them. Place this in the items object between the calendar and chart panel configurations:

splitter : {
    type : 'splitter'
},

The splitter provides:

With all three components configured (calendar, splitter, and chart panel), your container is now complete and ready to display the integrated calendar and chart visualization.

Implementing the Chart Update Logic

The chart needs data from Calendar on the load. It also needs to refresh the data on any changes. To make it easy, we will write a function that we can call on different actions.

Add the following function before the Container :

const updateChart = filteredResources => {
    const
        { datasetButtons, chart, calendar }       = container.widgetMap,
        { resourceStore, eventStore, activeView } = calendar,
        { startDate, endDate }                    = activeView,
        days                                      = DateHelper.diff(startDate, endDate, 'day'),
        datasetName                               = datasetButtons.value;

    let data,
        chartTitle,
        accessibilityDescription,
        tooltipFormatter;

    switch (datasetName) {
        case 'perResource':
            chartTitle = 'Events per resource';
            accessibilityDescription =
                'Column chart showing the number of events per resource for the current calendar view.';

            // Count events per resource and store event references for tooltips
            data = (filteredResources || resourceStore.records)
                .map(resource => {
                    const resourceEvents = resource.events.filter(
                        event => event.startDate >= startDate && event.startDate <= endDate
                    );
                    return {
                        label  : resource.name,
                        value  : resourceEvents.length,
                        color  : resource.barColor,
                        events : resourceEvents
                    };
                })
                .sort((a, b) => a.label.localeCompare(b.label));
            break;

        case 'perDay':
            chartTitle = 'Events per day';
            accessibilityDescription = 'Column chart showing the number of events per day for the current calendar view.';

            data = [];
            for (let i = 0; i < days; i++) {
                const from = DateHelper.add(startDate, i, 'day'),
                    to   = DateHelper.add(from, 1, 'day');
                const dayEvents = eventStore.getEvents({
                    startDate : from,
                    endDate   : to
                });

                data.push({
                    label :
                        DateHelper.format(from, 'ddd') +
                        (activeView.isMonthView ? ' ' + DateHelper.format(from, ' D') : ''),
                    value : dayEvents.length,
                    color : '#a3eea3',
                    fullDayName :
                        DateHelper.format(from, 'dddd') +
                        (activeView.isMonthView ? ' ' + DateHelper.format(from, 'D') : ''),
                    dayEvents : dayEvents
                });
            }
            break;
    }

    const
        categories = data.map(d => d.label),
        seriesData = data.map(d => ({
            y           : d.value,
            color       : d.color,
            fullDayName : d.fullDayName,
            events      : d.events,
            dayEvents   : d.dayEvents
        }));

    chart.chartOptions = {
        title : {
            text : chartTitle
        },
        xAxis : {
            categories
        },
        tooltip : {
            formatter : tooltipFormatter
        },
        series : [{
            data : seriesData
        }],
        accessibility : {
            description : accessibilityDescription
        }
    };
};

This function:

  1. Retrieves references to the calendar, chart, and button group widgets
  2. Switches between two visualization modes: per-resource and per-day
  3. Processes calendar events and creates data arrays for Highcharts
  4. Stores additional event metadata for rich tooltips (we’ll implement these next)
  5. Updates the chart with the new configuration

Adding Rich Tooltips

Let’s implement a rich and informative tooltip for both chart views (per day and per resource).

Resource View Tooltip

Add this tooltip formatter in the perResource case block (after accessibilityDescription) in the updateChart:

tooltipFormatter = function() {
    const
        imagePath    = container.resourceImagePath +
                        this.point.category.toLowerCase() +
                        '.png',
        events       = this.point.events || [],
        totalMinutes = events.reduce((sum, e) =>
            sum + DateHelper.diff(e.startDate, e.endDate, 'minutes'), 0
        ),
        hours        = Math.floor(totalMinutes / 60),
        minutes      = totalMinutes % 60,
        timeDisplay  = minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`,
        totalEvents  = eventStore.getEvents({
            startDate, endDate
        }).length,
        eventList    = events.slice(0, 3).map(e =>
        `<li style="margin: 2px 0">${e.name} <span style="color: var(--b-neutral-40); margin-inline-start: 4px;">(${DateHelper.format(e.startDate, 'MMM D')})</span></li>`
        ).join('');

    return `<div>
        <div style="display: flex; align-items: center; gap: 10px; margin-block-end: 8px;">
            <img class="b-resource-avatar b-resource-image" style="background-color:${this.point.color}" src="${imagePath}"></img>
            <b>${this.point.category}</b>
        </div>
        <div>
            ${this.series.name}: <b>${this.point.y}</b> <span style="color: var(--b-neutral-40)">of ${totalEvents}</span><br/>
            <span style="color: var(--b-neutral-30);">Total hours: ${timeDisplay}</span><br/>
            ${events.length > 0 ? `<ul style="margin: 8px 0; padding-left: 20px;">${eventList}</ul>` : ''}
            ${events.length > 3 ? `<i style="color: var(--b-neutral-40);">...and ${events.length - 3} more</i>` : ''}
        </div>
    </div>`;
};

This tooltip displays:

Day View Tooltip

Add this tooltip formatter in the perDay case block:

tooltipFormatter = function() {
    const
        fullDayName       = this.point.fullDayName || this.point.category,
        dayEvents         = (this.point.dayEvents || []).sort((a, b) => a.startDate - b.startDate),
        uniqueResources   = new Set(dayEvents.map(e => e.resourceId).filter(Boolean));

    let timeRange = '';
    if (dayEvents.length > 0) {
        const
            earliest = new Date(Math.min(...dayEvents.map(e => e.startDate))),
            latest   = new Date(Math.max(...dayEvents.map(e => e.endDate)));
        timeRange = `<span style="color: var(--b-neutral-30)">${DateHelper.format(earliest, 'HH:mm')} - ${DateHelper.format(latest, 'HH:mm')}</span>`;
    }

    const eventList = dayEvents.slice(0, 3).map(e =>
        `<li style="margin: 2px 0">${e.name}</li>`
    ).join('');

    return `<div>
        <b style="margin-block-end: 8px; display: inline-block;">${fullDayName}</b><br/>
        ${this.series.name}: <b>${this.point.y}</b><br/>
        ${uniqueResources.size > 0 ? `<span style="color: var(--b-neutral-30)">${uniqueResources.size} resource${uniqueResources.size > 1 ? 's' : ''}</span><br/>` : ''}
        ${timeRange ? `${timeRange}<br/>` : ''}
        ${dayEvents.length > 0 ? `<ul style="margin: 5px 0; padding-left: 20px;">${eventList}</ul>` : ''}
        ${dayEvents.length > 3 ? `<i style="color: var(--b-neutral-40);">...and ${dayEvents.length - 3} more</i>` : ''}
    </div>`;
};

This tooltip shows:

The tooltips use Bryntum CSS variables (var(--b-neutral-30), etc.) to ensure they adapt correctly to light and dark themes.

Styling for Theme Compatibility

To ensure Highcharts looks great in both light and dark Bryntum themes, we need to add custom CSS that bridges the two libraries’ styling systems. Append the following to style.css file:

html, body {
    --highcharts-background-color : var(--b-neutral-100);
}

.highcharts-title,
.b-highcharts-container text {
    fill: var(--b-neutral-10) !important;
}

.highcharts-menu {
    background: var(--b-menu-background) !important;
    border-radius: var(--b-menu-border-radius) !important;
}

.highcharts-menu-item {
    color: var(--b-neutral-10) !important;
}

.highcharts-menu-item:hover {
    color: var(--b-neutral-30) !important;
    background-color: var(--b-primary-80) !important;
}

.highcharts-exporting-group rect {
    fill: var(--b-neutral-100) !important;
}

.highcharts-tooltip > span {
    color: var(--b-neutral-10) !important;
}

.highcharts-tooltip-box {
    fill: var(--b-neutral-100);
}

.highcharts-background {
    fill: var(--b-neutral-100) !important;
}

This CSS:

Testing Dark Mode: To see the theme compatibility in action, open style.css and change svalbard-light.css to svalbard-dark.css. You’ll notice that the chart background, text colors, tooltips, and export menu all adapt seamlessly to match the Bryntum theme, providing a cohesive user experience.

Print and Download Charts

To enable print and download functionality for your charts, you’ll need to add additional Highcharts modules. These modules provide a hamburger menu in the top-right corner of each chart, allowing users to export charts in various formats.

Add the following imports to your HighchartsWidget.js file (after the main Highcharts import):

// Import Highcharts modules as needed
import 'highcharts/modules/exporting.js';
import 'highcharts/modules/accessibility.js';

This will result in a hamburger menu appearing in the top-right corner of the charts, providing options to:

Configure Highcharts Export Settings

To ensure proper font inheritance when exporting charts, add this event handler to lib/HighchartsWidget.js:

// Event handler to ensure font family is inherited for export
Highcharts.addEvent(Highcharts.Chart, 'init', function(e) {
    if (e.args[0].chart?.forExport) {
        e.args[0].chart.style = {
            fontFamily : window.getComputedStyle(
                document.getElementById('app')
            ).fontFamily
        };
    }
});

This event listener ensures that when charts are exported (as images or PDFs), they maintain the same font family as displayed in the browser, providing consistent typography across all output formats.

Running the Complete Application

With all the pieces in place, your application should now be running at http://localhost:5173/. If you stopped the dev server earlier, restart it:

npm run dev

Conclusion

You’ve successfully integrated Bryntum Calendar with Highcharts using Vite and npm! If you’re new to Bryntum, we offer a 45-day free trial so you can fully explore our components and all their features before making a commitment. This gives you plenty of time to build and test your integration in a real-world environment.

Arsalan Khattak

Bryntum Calendar