Arsalan Khattak
15 October 2025

Using a React Bryntum Gantt as a custom Power BI visual

Power BI is Microsoft’s business intelligence platform for analyzing and visualizing data. It enables users to create interactive reports and […]

Power BI is Microsoft’s business intelligence platform for analyzing and visualizing data. It enables users to create interactive reports and data visualization dashboards. Power BI comes with a set of core visuals, which include bar charts, line charts, and pie charts. You can download or import visuals from Microsoft AppSource or Power BI. However, the available visuals may not meet your needs. In that case, you can create your own custom visuals, as we’ll do in this tutorial.

Bryntum Gantt is a performant, fully customizable Gantt chart component that works with all major JavaScript frameworks, including React. It has advanced project management features for uses such as identifying and highlighting critical paths, implementing advanced filtering, and tracking the rate of a project’s progress with an S-curve. These features can bring advanced project tracking capabilities to your Power BI reports.

In this tutorial, we’ll create a custom Bryntum Gantt Power BI visual for displaying project data in an interactive Gantt chart and add it to a Power BI report. You’ll learn to do the following:

Here’s what we’ll build:

You can find the code for the completed tutorial in the Bryntum Gantt Power BI Visual GitHub repository.

Prerequisites

Before starting, ensure you have:

Power BI is available as a web service and as the Power BI Desktop app for Windows. We’ll use the web service in this tutorial.

Set up your development environment

Before creating a custom Power BI visual, you need to set up your development environment by installing the pbiviz tool and enabling developer mode in Power BI.

Install the pbiviz tool

The pbiviz tool compiles Power BI visual source code into deployable packages. Install it globally in your terminal using npm:

npm i -g powerbi-visuals-tools@latest

⚠️ You might get some warnings during installation. You can ignore them, as they shouldn’t prevent pbiviz from installing.

Enable developer mode

To develop or upload your own Power BI visual, you must enable developer mode.

In the web service, go to the Power BI Developer settings and use the toggle to enable Developer mode:

If you’re using Power BI Desktop, follow the instructions for enabling developer mode in the Power BI documentation.

Create the development project

Create a new project folder and open it in your IDE. Then, create a new Power BI visual project in the terminal:

pbiviz new BryntumGantt

This creates a new folder with all the necessary files for a Power BI visual. Open the BryntumGantt folder in your IDE.

You can find a detailed explanation of the function of each file in the Power BI visual project structure documentation.

Install the Power BI visual tools dependencies:

npm install

These tools provide everything you need to develop visuals and test them in Power BI reports and dashboards.

Start the development server:

pbiviz start

Keep this development server running for the rest of the tutorial. It will serve your visual during development. The visual is now running locally and ready for testing in Power BI.

Create a Power BI report and upload sample tasks data from an Excel file

Let’s create a Power BI report and upload sample tasks data from an Excel file.

First, download the Excel file containing the example tasks data. The Task Name, Start Date, End Date, Percent Done, and Manually Scheduled columns in the Excel file represent some common fields of the Bryntum Gantt task model.

Then, follow these steps to create a report and upload the example tasks data:

This creates a new report:

This adds the visual to the report canvas:

Note that the Update count value increases when you resize the visual.

Power BI treats data fields as either category (descriptive text) or measure (numerical values for calculations) data. In the example tasks data, Task Name is category data because it provides descriptive labels. The rest of the columns are measure data that Power BI can use for calculations and aggregations. The measure data are displayed as the sum of the values in the column.

Now let’s make the custom Visual render a Bryntum Gantt React chart.

Install React and the Bryntum Gantt React component

Install React and its type definitions:

npm install react react-dom @types/react @types/react-dom

Next, install the Bryntum Gantt packages for React. Make sure that you have access to the Bryntum npm repository.

If you have a Bryntum Gantt license, run the following command:

npm install @bryntum/gantt @bryntum/gantt-react

To use the trial version of Bryntum Gantt, run the following command:

npm install @bryntum/gantt@npm:@bryntum/gantt-trial @bryntum/gantt-react

Replace the content of tsconfig.json with the following:

{
    "compilerOptions": {
        "jsx": "react",
        "types": [
            "react",
            "react-dom"
        ],
        "allowJs": false,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "es2022",
        "sourceMap": true,
        "outDir": "./.tmp/build/",
        "moduleResolution": "node",
        "declaration": true,
        "lib": [
            "es2022",
            "dom"
        ]
    },
    "files": [
        "src/visual.ts",
        "./src/settings.ts"
    ]
}

Here, we add "jsx": "react" and "types": ["react", "react-dom"] to enable React JSX compilation, including React type definitions. We also add the src/settings.ts file to the files array.

Create the Bryntum Gantt configuration file

Create a ganttConfig.ts file in the src folder and add the following lines of code to it:

import { BryntumGanttProps } from '@bryntum/gantt-react';

const ganttConfig: BryntumGanttProps = {
    columns    : [{ type : 'name', field : 'name', width : 250 }],
    viewPreset : 'weekAndDayLetter',
    barMargin  : 10
};

export { ganttConfig };

This creates a basic Gantt configuration with a Name column.

Create the React component

Create a BryntumGanttComponent.tsx file in the src folder and add the following lines of code to it:

import * as React from 'react';
import { useState, useEffect, useRef } from 'react';
import { BryntumGantt } from '@bryntum/gantt-react';
import { ganttConfig } from './ganttConfig';
import { TaskModel } from '@bryntum/gantt';

interface GanttState {
    tasks: TaskModel[];
}

const BryntumGanttComponent = ({ updateCallback }) => {
    const gantt = useRef(null);
    const [state, setState] = useState<GanttState>({ tasks : [] });

    useEffect(() => {
        updateCallback(setState);
    }, [updateCallback]);

    const GanttComponent = BryntumGantt as any;

    return (
        <GanttComponent
            ref={gantt}
            tasks={state.tasks}
            {...ganttConfig}
        />
    );
};

export default BryntumGanttComponent;

This code renders the Bryntum Gantt React component and passes in the ganttConfig and tasks data state as props.

Update the constructor method of the Power BI visual class to render the Bryntum Gantt React component

When the visual instantiates, it calls the constructor method in the Visual class. We’ll use it to render the Bryntum Gantt React component.

Open the src/visual.ts file and add the following imports:

import * as React from 'react';
import { createRoot, Root } from 'react-dom/client';
import BryntumGanttComponent from './BryntumGanttComponent';

Add the following private properties to the Visual class:

private root: Root;
private updateState: (newState: any) => void;

Next, replace the constructor method of the Visual class with the following:

constructor(options: VisualConstructorOptions) {
    this.updateState = () => {};

    this.target = options.element;
    this.root = createRoot(this.target);

    const reactRoot = React.createElement(BryntumGanttComponent, {
        updateCallback : (updateFunc: (newState: any) => void) => {
            this.updateState = updateFunc;
        }
    });

    this.root.render(reactRoot);
}

This creates a Bryntum Gantt React element. In the updated constructor, we pass in an updateCallback prop that we’ll use to update the Bryntum Gantt’s task state. This callback is run in the useEffect hook in the BryntumGanttComponent React component.

Add CSS styling

Update style/visual.less to import a Bryntum theme and set the size of the Gantt chart:

@import "@bryntum/gantt/gantt.stockholm.css";

#root {
  height: 100vh;
}

This imports the Bryntum Stockholm theme and ensures the Gantt chart fills the available space.

Refresh the visual in the report canvas to see the Gantt chart:

The Gantt chart doesn’t display the tasks data yet. Let’s fix that.

Configure the visual capabilities

Before adding the tasks data from Power BI to the Gantt chart, we need to configure the visual capabilities.

Replace the JSON object in capabilities.json with the following:

{
    "dataRoles": [
        {
            "displayName": "ID",
            "name": "id",
            "kind": "Grouping"
        },
        {
            "displayName": "Task Name",
            "name": "taskName",
            "kind": "Grouping"
        },
        {
            "displayName": "Start Date",
            "name": "startDate",
            "kind": "Grouping"
        },
        {
            "displayName": "End Date",
            "name": "endDate", 
            "kind": "Grouping"
        },
        {
            "displayName": "Percent Done",
            "name": "percentDone",
            "kind": "Grouping"
        },
        {
            "displayName": "Manually Scheduled",
            "name": "manuallyScheduled",
            "kind": "Grouping"
        },
        {
            "displayName": "Parent Index",
            "name": "parentIndex",
            "kind": "Grouping"
        }
    ],
    "dataViewMappings": [
        {
            "table": {
                "rows": {
                    "select": [
                        { "for": { "in": "id" } },
                        { "for": { "in": "taskName" } },
                        { "for": { "in": "startDate" } },
                        { "for": { "in": "endDate" } },
                        { "for": { "in": "percentDone" } },
                        { "for": { "in": "manuallyScheduled" } },
                        { "for": { "in": "parentIndex" } }
                    ]
                }
            }
        }
    ],
    "objects": {},
    "privileges": []
}

Here, we use the dataRoles array to define the data fields that the visual expects from Power BI. Each role corresponds to a column in the Excel file. We set the kind property to "Grouping" to allow users to drag data fields into these roles.

The dataViewMappings object tells Power BI how to structure the data for the visual. The table mapping creates a tabular data structure where each row contains values for all five data roles, making it easy to iterate through tasks in the visual code.

Update the visual update method

When the data changes or the visual refreshes, Power BI calls the update method in the Visual class.

Replace the update method in the src/visual.ts file with the following lines of code:

public update(options: VisualUpdateOptions) {
    const dataView = options.dataViews[0];

    if (dataView && dataView.table) {
        // Extract data from Power BI table dataView
        const columns = dataView.table.columns;
        const rows = dataView.table.rows;

        // Find column indices by display name
        const idIndex = columns.findIndex((col: any) => col.displayName === 'ID');
        const taskNameIndex = columns.findIndex((col: any) => col.displayName === 'Task Name');
        const startDateIndex = columns.findIndex((col: any) => col.displayName === 'Start Date');
        const endDateIndex = columns.findIndex((col: any) => col.displayName === 'End Date');
        const percentDoneIndex = columns.findIndex((col: any) => col.displayName === 'Percent Done');
        const manuallyScheduledIndex = columns.findIndex((col: any) => col.displayName === 'Manually Scheduled');
        const parentIndexIndex = columns.findIndex((col: any) => col.displayName === 'Parent Index');

        if (rows && rows.length > 0) {
            // Sort rows by parentIndex
            const sortedRows = [...rows].sort((a: any, b: any) => {
                if (parentIndexIndex >= 0) {
                    return (a[parentIndexIndex] || 0) - (b[parentIndexIndex] || 0);
                }
                return 0;
            });

            const tasks = sortedRows
                .filter((row: any) => {
                    const taskName = taskNameIndex >= 0 ? row[taskNameIndex] : null;
                    return taskName &&
                            taskName !== null &&
                            taskName !== undefined &&
                            typeof taskName === 'string' &&
                            taskName.trim() !== '';
                })
                .map((row: any) => {
                    const task = {
                        id                : idIndex >= 0 ? row[idIndex] : undefined,
                        name              : taskNameIndex >= 0 ? row[taskNameIndex] : undefined,
                        startDate: startDateIndex >= 0 ? this.convertExcelDate(row[startDateIndex]) : undefined,
                        endDate           : endDateIndex >= 0 ? this.convertExcelDate(row[endDateIndex])   : undefined,
                        percentDone       : percentDoneIndex >= 0 ? row[percentDoneIndex] : 0,
                        manuallyScheduled : manuallyScheduledIndex >= 0 ? Boolean(row[manuallyScheduledIndex]) : undefined,
                        parentIndex       : parentIndexIndex >= 0 ? row[parentIndexIndex] : undefined,
                    };
                    return task;
                });

            this.updateState({ tasks });
        }
    }
}

Power BI calls the update method whenever data changes or the visual needs to refresh. In the code above, we get the tasks data from options.dataViews[0].table. Then, we find the column indices of the tasks data using their display names, sort the rows by the parentIndex column, and filter out invalid rows with empty task names. Each valid data row is transformed into a Gantt task object. We call this.updateState(), which passes the tasks data to the BryntumGanttComponent React component.

Refresh the visual in the report canvas to see the Gantt chart with the tasks data. Make sure that you select all of the tasks data in the Data pane.

The date values are incorrect. We need to convert the Excel dates to JavaScript dates.

Convert Excel dates to JavaScript dates

We’ll use the SheetJS library to convert the Excel dates to JavaScript dates. Install it using npm:

npm install xlsx

Import the library modules at the top of the src/visual.ts file:

import * as xlsx from 'xlsx';

Add the following method to the Visual class to convert Excel dates to JavaScript dates:

private convertExcelDate(excelDate: any): string {
    if (typeof excelDate === 'number') {
        // Use xlsx library to parse Excel date numbers
        const dateObj = xlsx.SSF.parse_date_code(excelDate);
        const date = new Date(
            dateObj.y,
            dateObj.m - 1, // months are zero-indexed in JavaScript
            dateObj.d,
            dateObj.H,
            dateObj.M,
            dateObj.S
        );
        return date.toISOString().split('T')[0];
    }
    // If it's already a string date, return as is
    if (typeof excelDate === 'string' && excelDate.match(/^\d{4}-\d{2}-\d{2}$/)) {
        return excelDate;
    }
    return new Date().toISOString().split('T')[0];
}

The convertExcelDate method uses the xlsx library’s spreadsheet format (SSF) object and its parse_date_code method to parse the Excel number into an SSF date object. We use this SSF date object to create a JavaScript Date object. To construct the JavaScript Date object, we subtract one from the SSF date object’s month value, as months are zero-indexed in JavaScript. The method provides fallbacks for string dates or returns today’s date for invalid values.

Find the tasks object in the map function that is used in the update method. Then, in the tasks object, replace the startDate and endDate values with the following:

startDate         : startDateIndex >= 0 ? this.convertExcelDate(row[startDateIndex]) : new Date().toISOString().split('T')[0],
endDate           : endDateIndex >= 0 ? this.convertExcelDate(row[endDateIndex]) : new Date().toISOString().split('T')[0],

The convertExcelDate method converts the Excel dates to JavaScript dates.

You’ll now see the correct dates in the Gantt chart:

If you don’t see the correct dates, try refreshing the visual in the report canvas by clicking the Refresh button, at the top left of the report page.

⚠️ Clicking the Reload Visual Code button, which is the circular arrow at the bottom of the visual in the report canvas, will show a blank Gantt chart because the visual reinitializes with empty state and Power BI doesn’t automatically trigger the update() method in the Power BI visual class with cached data during development. You’ll need to interact with the visual, for example resize the visual, or click the Refresh button at the top left of the report page to see the data in the Gantt chart.

If you encounter any issues during testing, you can debug your visual using browser developer tools. See the Power BI visual debugging guide for detailed debugging instructions.

Packaging your visual

Before you can load your custom visual into Power BI Desktop or share it with the community, you need to package it. Update pbiviz.json with the necessary author information and run the following command:

pbiviz package

This creates a .pbiviz file in the /dist/ directory of your visual project. The package contains everything required to import the custom visual into either the Power BI service or a Power BI Desktop report.

For detailed packaging and distribution instructions, see the Microsoft documentation on packaging Power BI visuals.

Next steps

Now that you know how to create a custom Bryntum Gantt Power BI visual, you can further customize it by adding extra features.

Arsalan Khattak

Bryntum Gantt Microsoft