Migrating from DHTMLX Gantt to Bryntum Gantt

Bryntum Gantt and DHTMLX Gantt are commercial project management tools for scheduling tasks. In this tutorial, we’ll migrate an existing implementation of a DHTMLX Gantt chart over to Bryntum Gantt.
Here are the steps we’ll follow:
- Update the library import and set up.
- Update the server API endpoints.
- Create the database tables compatible with the Bryntum Gantt chart
- Insert the existing data from the previous tables into the new Bryntum Gantt compatible tables (optionally removing the previous tables)
- Test the migration implementation
Getting started
We will use an existing DHTMLX starter project, which uses vanilla JavaScript on the client-side and Node.js with REST API endpoints on the server-side. These endpoints interact with a MySQL client to perform CRUD operations on the database. Your implementation may differ slightly depending on which client-side (React or Angular) and server-side variants are used.
If you want to follow along with our starter project instead of updating your own project code, you can clone the following DHTMLX starter GitHub repository. See the README.md
file for getting this starter project up and running.
Following the migration, we expect the Bryntum Gantt chart to display the same data as this DHTMLX Gantt chart:

Update library import and set up
To begin our migration to Bryntum Gantt, we’ll update and set up client-side and server-side.
Update the client-side
Download the Trial Distribution zip by signing up for the Bryntum trial and copy the following files to the public
folder of the DHTMLX project: gantt.module.js
and gantt.stockholm.css
.
Open the index.html
file and find the <script>
, <link>
, and <style>
tags for dhtmlxgantt.js
. Replace them with the following code to import the Bryntum styles:
<link rel="stylesheet" href="./gantt.stockholm.css" data-bryntum-theme>
<style>
html {
font-size: 100%;
}
body{
margin: 0;
display: flex;
flex-direction: column;
height: 100vh;
font-family: Lato, "Open Sans", Helvetica, Arial, sans-serif;
font-size: 0.875rem;
}
</style>
In the body
HTML tag, remove the div container and replace the DHTMLX set up code in the script
tag with the following set up code from Bryntum:
import { Gantt, ProjectModel } from './gantt.module.js';
// create project which loads data from a URL
const project = new ProjectModel({
taskStore: {
autoTree: true,
transformFlatData: true,
},
// specify data source
transport: {
load: {
url: 'http://localhost:1338/data',
},
sync: {
url: 'http://localhost:1338/api',
},
},
autoLoad: true,
autoSync: true,
validateResponse: true,
});
// create the bryntum gantt instance and append to body
const gantt = new Gantt({
appendTo: document.body,
project,
dependencyIdField: 'sequenceNumber',
columns: [{ type: 'name', width: 250, text: 'Tasks' }],
});
// load project data
project.load();
We have now imported the Bryntum Gantt component from the library and set it up to load data from the /data
endpoint on the initial load of the webpage, which will be served on port 1338
. We also specify that the /api
endpoint will be used when any create, update, or delete operation takes place to keep the client and database in sync.
Update the server-side
Install the uuid
library:
npm install uuid --save
Import the uuid
library by adding the following line to the top of the server.js
file:
export function uuid() {}
We will use the uuid
library to generate a unique id when creating tasks and dependencies.
Next, find the following line of code:
app.use(bodyParser.urlencoded({ extended: true }));
And replace it with the JSON bodyParser
:
app.use(bodyParser.json());
DHTMLX sends HTTP request data encoded in the URL, but Bryntum sends a request object in JSON format.
Update the server API endpoints
Find the POST request handler to add a new task: app.post("/data/task"
. Replace it with the following HTTP POST request:
app.post("/api", async function (req, res) {
let requestId = "";
let lastKey = "";
let err = null;
const taskUpdates = [];
console.log(
`\n${req.method} ${req.url} --> ${JSON.stringify(
req.body,
`\t`,
2
)}`
);
for (const [key, value] of Object.entries(req.body)) {
if (key === "requestId") {
requestId = value;
}
if (key === "tasks") {
for (const [changeType, changeValues] of Object.entries(value)) {
if (changeType === "added") {
changeValues.forEach((addObj) => taskUpdates.push(addObj));
changeValues[0].id = uuid.v4();
const val = await createOperation(changeValues[0], "tasks");
lastKey = val.msg;
err = val.error;
}
if (changeType === "updated") {
console.log(`updated tasks...`, changeValues);
changeValues.forEach((updateObj) =>
taskUpdates.push(updateObj)
);
const val = await updateOperation(changeValues, "tasks");
lastKey = val.msg;
err = val.error;
}
}
}
}
sendResponse(
res,
lastKey,
requestId,
err,
taskUpdates
);
});
If you’re following the reference project, also remove the getTask
helper function.
The client-side Bryntum library will make a POST request to the /api
endpoint when changes are made to keep the data in the database in sync with the client-side UI. The code we added will handle the incoming POST request and inspect the request body to determine what action should be taken on which data model. We first determine which data model to apply a sync request to and then inspect the type of operation to perform. In this case, we check if the sync request body object has a key called tasks
and then check if the nested object key is an operation called added
. If this is the case for both, we call createOperation
and insert the added task into our database using the MySQL client. Then we insert the relevant columns to be stored. We ignore keys of the object that we don’t need.
Define the createOperation
function at the bottom of server.js
:
async function createOperation(addObj, table) {
const valArr = [];
const keyArr = [];
for (const [key, value] of Object.entries(addObj)) {
if (
key !== "baselines" &&
key !== "from" &&
key !== "to" &&
key !== "$PhantomId" &&
key !== "segments" &&
key !== "ignoreResourceCalendar"
) {
keyArr.push(`\`${key}\``);
valArr.push(value);
}
}
try {
await db.query(
`INSERT INTO ${table} (${keyArr.join(
", "
)}) VALUES (${Array(keyArr.length).fill("?").join(",")})`,
valArr
);
return { msg: "added", error: null };
} catch (error) {
return { msg: "error", error: error };
}
}
⚠️ The $PhantomId
is a phantom identifier. It’s an auto-generated, unique client-side value that’s used to identify a record. You should not persist it on the server. You can read more about it in the docs.
💡 We will reuse the functions that handle the CRUD operations for the dependencies
table.
Find the sendResponse
helper function and replace it with the following:
function sendResponse(
res,
action,
requestId,
error,
taskUpdates
) {
if (action == "error") console.log(error);
const result = {
success: action === "error" ? false : true,
};
if (requestId)
result.requestId = requestId;
// updated tasks
result.tasks = {};
result.tasks.rows = [];
if (taskUpdates.length) {
result.tasks.rows = [...result.tasks.rows, ...taskUpdates];
}
res.send(result);
return;
}
In this function, we format the response to include the results of the operations applied and send the response to the client to complete the handling of the HTTP request.
DHTMLX uses separate HTTP methods for each of the CRUD operations. With Bryntum, except for the read operation, we will use a single POST request and determine the method and operation based on the request object.
In the serverConfig
function, find the GET request handler with the route /data
which fetches the data from the database using the MySQL Node.js client. Replace it with the following:
// Read
app.get("/data", async (req, res) => {
try {
const results = await Promise.all([
db.query("SELECT * FROM tasks"),
db.query("SELECT * FROM dependencies"),
]);
const tasks = results[0][0],
dependencies = results[1][0];
res.send({
success: true,
tasks: {
rows: tasks,
},
dependencies: {
rows: dependencies,
},
});
} catch (error) {
sendResponse(res, "error", null, error, [], [], [], []);
}
});
This is the endpoint to fetch and read the data from our database, which will be called by the library to keep the client-side up to date with our datastore as we configured in the index.html
script section.
💡 The sendResponse
function in the above GET request includes more arguments, which we will add soon.
In the DHTMLX project, find and delete the update task request app.put("/data/task/:id"
and delete task request app.delete("/data/task/:id"
.
Add the following functions to the bottom of the server.js
file:
async function updateOperation(updates, table) {
try {
await Promise.all(
updates.map(({ id, ...update }) => {
return db.query(
`
UPDATE ${table}
SET ${Object.keys(update)
.map((key) => `${key} = ?`)
.join(", ")}
WHERE id = ?
`,
Object.values(update).concat(id)
);
})
);
return { msg: "update", error: null };
} catch (error) {
return { msg: "error", error };
}
}
async function deleteOperation(id, table) {
try {
await db.query(`DELETE FROM ${table} WHERE id = ?`, [id]);
return { msg: "deleted", error: null };
} catch (error) {
return { msg: "error", error: error };
}
}
Add the dependency server API endpoints
We can now complete our handling of tasks and add the links to API endpoints, which are called dependencies
in the Bryntum library.
We’ll handle the create, update, and delete operations for dependencies
in the POST request to /api
:
// Create, Update, Delete (Tasks & Dependencies)
app.post("/api", async function (req, res) {
let requestId = "";
let lastKey = "";
let err = null;
const taskUpdates = [];
const tasksRemoved = [];
const dependencyUpdates = [];
const dependenciesRemoved = [];
console.log(
`\n${req.method} ${req.url} --> ${JSON.stringify(
req.body,
`\t`,
2
)}`
);
for (const [key, value] of Object.entries(req.body)) {
if (key === "requestId") {
requestId = value;
}
if (key === "tasks") {
for (const [changeType, changeValues] of Object.entries(value)) {
if (changeType === "added") {
changeValues.forEach((addObj) => taskUpdates.push(addObj));
changeValues[0].id = uuid.v4();
const val = await createOperation(changeValues[0], "tasks");
lastKey = val.msg;
err = val.error;
}
if (changeType === "updated") {
console.log(`updated tasks...`, changeValues);
changeValues.forEach((updateObj) =>
taskUpdates.push(updateObj)
);
const val = await updateOperation(changeValues, "tasks");
lastKey = val.msg;
err = val.error;
}
if (changeType === "removed") {
tasksRemoved.push(changeValues[0]);
const val = await deleteOperation(changeValues[0].id, "tasks");
lastKey = val.msg;
err = val.error;
}
}
}
if (key === "dependencies") {
for (const [changeType, changeValues] of Object.entries(value)) {
if (changeType === "added") {
changeValues[0].id = uuid.v4();
changeValues.forEach((addObj) =>
dependencyUpdates.push(addObj)
);
const val = await createOperation(
changeValues[0],
"dependencies"
);
lastKey = val.msg;
err = val.error;
}
if (changeType === "updated") {
changeValues.forEach((updateObj) =>
dependencyUpdates.push(updateObj)
);
const val = await updateOperation(changeValues, "dependencies");
lastKey = val.msg;
err = val.error;
}
if (changeType === "removed") {
dependenciesRemoved.push(changeValues[0]);
const val = await deleteOperation(
changeValues[0].id,
"dependencies"
);
lastKey = val.msg;
err = val.error;
}
}
}
}
sendResponse(
res,
lastKey,
requestId,
err,
taskUpdates,
dependencyUpdates,
tasksRemoved,
dependenciesRemoved
);
});
Find the create link POST request app.post("/data/link"
and remove it.
If you are following the reference project, also remove the getLink
function.
The starter template doesn’t have an implementation of the Update Link operation because there is no UI in the DHTMLX Gantt chart to do so. You can, however, implement it yourself. See the example implementation for editing link values provided in the docs.
If you have implemented it, find the update link operation updateOrder
and remove it.
Find the delete link operation app.delete("/data/link/:id"
and remove it.
We can now update our sendResponse
function to include the operations we added for tasks
and dependencies
:
function sendResponse(
res,
action,
requestId,
error,
taskUpdates,
dependencyUpdates,
tasksRemoved,
dependenciesRemoved
) {
if (action == "error") console.log(error);
const result = {
success: action === "error" ? false : true,
};
if (requestId !== undefined && requestId !== null)
result.requestId = requestId;
// updated tasks
result.tasks = {};
result.tasks.rows = [];
if (taskUpdates.length) {
result.tasks.rows = [...result.tasks.rows, ...taskUpdates];
}
// deleted tasks
result.tasks.removed = [];
if (tasksRemoved.length) {
result.tasks.removed = [
...result.tasks.removed,
...tasksRemoved,
];
}
// updated dependencies
result.dependencies = {};
result.dependencies.rows = [];
if (dependencyUpdates.length) {
result.dependencies.rows = [
...result.dependencies.rows,
...dependencyUpdates,
];
}
// deleted dependencies
result.dependencies.removed = [];
if (dependenciesRemoved.length) {
result.dependencies.removed = [
...result.dependencies.removed,
...dependenciesRemoved,
];
}
res.send(result);
return;
}
The API endpoints have now been successfully updated to be compatible with our implementation of Bryntum Gantt chart.
Create database tables compatible with Bryntum Gantt
If you followed the instructions in the README.md
file, you will have configured your database already and populated it with some data.
The tasks
table should look like this:

And the links
table like this:

We will now create two separate tables with a schema compatible with the Bryntum Gantt Chart implementation.
Run the following query to create the tasks
table for our tasks data:
CREATE TABLE `tasks` (
`id` varchar(80) NOT NULL,
`parentId` varchar(80) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
`startDate` datetime DEFAULT NULL,
`endDate` datetime DEFAULT NULL,
`effort` float(11, 2) DEFAULT NULL,
`effortUnit` varchar(255) DEFAULT 'hour',
`duration` float(11, 2) unsigned DEFAULT NULL,
`durationUnit` varchar(255) DEFAULT 'day',
`percentDone` float(11, 2) unsigned DEFAULT 0,
`schedulingMode` varchar(255) DEFAULT NULL,
`note` text,
`constraintType` varchar(255) DEFAULT NULL,
`constraintDate` datetime DEFAULT NULL,
`manuallyScheduled` tinyint DEFAULT 1,
`effortDriven` tinyint DEFAULT 0,
`inactive` tinyint DEFAULT 0,
`cls` varchar(255) DEFAULT NULL,
`iconCls` varchar(255) DEFAULT NULL,
`color` varchar(255) DEFAULT NULL,
`parentIndex` INT(11) DEFAULT 0,
`expanded` tinyint DEFAULT 0,
`calendar` int(11) DEFAULT NULL,
`deadline` datetime DEFAULT NULL,
`direction` varchar(255) DEFAULT NULL,
`$PhantomId` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX (`parentId`),
CONSTRAINT `fk_tasks_tasks` FOREIGN KEY (`parentId`) REFERENCES `tasks`(`id`),
INDEX (`calendar`)
) ENGINE=MyISAM AUTO_INCREMENT=1;
The Bryntum Gantt has more features than the DHTMLX Gantt, which is why there are many columns in the table.
Create the dependencies
table with the following query:
CREATE TABLE `dependencies` (
`id` varchar(80) NOT NULL,
`fromEvent` varchar(80) DEFAULT NULL,
`toEvent` varchar(80) DEFAULT NULL,
`type` int(11) DEFAULT 2,
`cls` varchar(255) DEFAULT NULL,
`lag` float(11, 2) DEFAULT 0,
`lagUnit` varchar(255) DEFAULT 'day',
`$PhantomId` varchar(255) DEFAULT NULL,
`active` boolean,
`fromSide` varchar(255) DEFAULT NULL,
`toSide` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
INDEX (`fromEvent`),
CONSTRAINT `fk_dependencies_tasks` FOREIGN KEY (`fromEvent`) REFERENCES `tasks`(`id`),
INDEX (`toEvent`),
CONSTRAINT `fk_dependencies_tasks1` FOREIGN KEY (`toEvent`) REFERENCES `tasks`(`id`)
) ENGINE=MyISAM AUTO_INCREMENT=1;
The dependencies
table is equivalent to the gantt_links
table used for the DHTMLX Gantt chart.
Insert the existing data from the previous tables into the new Bryntum-compatible tables
Run the following query to insert the existing data from the gantt_tasks
table into the newly created tasks
table:
START TRANSACTION;
INSERT INTO tasks (
id,
parentId,
name,
startDate,
endDate,
effort,
effortUnit,
duration,
durationUnit,
percentDone,
schedulingMode,
note,
constraintType,
constraintDate,
manuallyScheduled,
effortDriven,
inactive,
cls,
iconCls,
color,
parentIndex,
expanded,
calendar,
deadline,
direction,
$PhantomId
)
SELECT
id,
IF(parent=0, NULL, parent),
text,
start_date,
DATE_ADD(start_date, INTERVAL duration DAY),
DATEDIFF(DATE_ADD(start_date, INTERVAL duration DAY), start_date)*24,
'hour',
CAST(duration AS FLOAT),
'day',
progress * 100,
NULL,
NULL,
NULL,
NULL,
0,
0,
0,
NULL,
NULL,
NULL,
parent,
0,
NULL,
NULL,
NULL,
NULL
FROM gantt_tasks
COMMIT;
Next, run the following query to insert the existing data from the gantt_links
table into the newly created dependencies
table:
START TRANSACTION;
INSERT INTO dependencies (
id,
fromEvent,
toEvent,
type,
cls,
`lag`,
lagUnit,
$PhantomId,
active,
fromSide,
toSide
)
SELECT
id,
source,
target,
2,
NULL,
0.00,
'day',
NULL,
1,
NULL,
NULL
FROM gantt_links
COMMIT;
Test the migration implementation
Open the root folder of the project in the terminal and run:
npm run start
You should now see the migrated data from DHTMLX Gantt chart loaded into the Bryntum Gantt chart, ready to take your project management to the next level with an extremely feature-rich and highly customizable Gantt chart.
