본문으로 건너뛰기
노트

The complete demo source code is available on GitHub: https://github.com/DHTMLX/gantt-migrating-from-syncfusion.

Migrating from Syncfusion Gantt to DHTMLX Gantt

Introduction

This guide will walk you through the process of migrating an existing application from Syncfusion Gantt to DHTMLX Gantt. We'll cover all necessary steps including database schema changes, server-side API modifications, and client-side code updates.

Prerequisites

Before starting the migration, ensure you have:

  • An existing working application using Syncfusion Gantt
  • Node.js (>= 20.0.0) installed
  • MySQL database with Syncfusion data structure
  • Basic knowledge of Express.js and JavaScript

Step 1: Database Migration

Understanding Current Schema

If you followed the Syncfusion demo setup, you should have one table: syncfusion_tasks.

The syncfusion_tasks table structure:

Syncfusion tasks table Syncfusion tasks table

Create DHTMLX Tables

Create two new tables compatible with DHTMLX Gantt:

CREATE TABLE IF NOT EXISTS `gantt_tasks` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`text` VARCHAR(255) NOT NULL,
`start_date` DATETIME NOT NULL,
`end_date` DATETIME NOT NULL,
`duration` INT(11) NOT NULL,
`progress` FLOAT NOT NULL DEFAULT 0,
`parent` INT(11) NOT NULL DEFAULT 0,
`notes` TEXT NULL,
`open` BOOLEAN NOT NULL DEFAULT TRUE,
PRIMARY KEY (`id`)
);

CREATE TABLE IF NOT EXISTS `gantt_links` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`source` INT(11) NOT NULL,
`target` INT(11) NOT NULL,
`type` VARCHAR(1) NOT NULL,
`lag` INT(11) DEFAULT 0,
PRIMARY KEY (`id`)
);

Migrate Existing Data

Now migrate your existing Syncfusion data to the new DHTMLX tables.

Migrate tasks:

INSERT INTO gantt_tasks (id, text, start_date, end_date, duration, progress, parent, notes, open)
SELECT
TaskID,
TaskName, -- TaskName → text
StartDate,
COALESCE(EndDate,
DATE_ADD(StartDate, INTERVAL Duration DAY)), -- Calculate end_date if missing
COALESCE(Duration,
DATEDIFF(EndDate, StartDate)), -- Calculate duration if missing
COALESCE(Progress, 0) / 100, -- Convert percentage (0-100) to decimal (0-1)
COALESCE(ParentId, 0), -- ParentId → parent (0 for root tasks)
info, -- info → notes
COALESCE(isExpand, TRUE) -- isExpand → open
FROM syncfusion_tasks;

Migrate links (dependencies)

In Syncfusion Gantt's data structure, dependencies are stored as strings in the Predecessor column:

  • Format examples: "5", "3,4", "5FS+2", "7SS-1,8FF+3", "2FS-5 days"

In DHTMLX Gantt, tasks and links are stored in separate tables. Each link is a row with:

  • id - the link id
  • source - the id of a task that the dependency will start from
  • target - the id of a task that the dependency will end with.
  • type - the dependency type: "0" (FS), "1" (SS), "2" (FF), "3" (SF)
  • lag - optional task's lag

We'll implement a Node.js migration script to parse Syncfusion's string format and convert it to DHTMLX's structured format.

Understanding Syncfusion Predecessor Format:

ExampleMeaningDHTMLX Equivalent
"5"Task depends on task 5 (default FS)source: 5, type: "0"
"3,4"Depends on tasks 3 AND 4Two separate links
"5FS"Finish-to-Start dependencysource: 5, type: "0"
"5FS+2"FS with 2 days positive lagsource: 5, type: "0", lag: 2
"5FS-3"FS with 3 days negative lagsource: 5, type: "0", lag: -3
"2FS-5 days"FS with lag including "days" textsource: 2, type: "0", lag: -5

Create a migrate-dependencies.js file and paste the following code into it:

import { pool } from './server.js';

const LINK_TYPE_MAP = {
FS: '0', // Finish-to-Start
SS: '1', // Start-to-Start
FF: '2', // Finish-to-Finish
SF: '3', // Start-to-Finish
};

/**
* Parse a single predecessor string like "5FS+2" or "7SS-1 days"
* @param {string} predecessor - Single predecessor string
* @returns {object|null} - Parsed link object or null if invalid
*/
function parseSinglePredecessor(predecessor) {
const clean = predecessor.trim();

// Regex pattern to match: TaskID [Type] [+/-Lag]
// Matches: "5", "5FS", "5FS+2", "7SS-1", "3FS+2 days", "8SS-1 days"
const pattern = /^(\d+)(FS|SS|FF|SF)?([\+\-]\d+)?(?:\s+days?)?$/i;
const match = clean.match(pattern);

if (!match) {
console.warn(`Cannot parse predecessor: "${predecessor}"`);
return null;
}

const source = parseInt(match[1]);
const typeCode = match[2] ? match[2].toUpperCase() : 'FS';
const lag = match[3] ? parseInt(match[3]) : 0;

return {
source,
type: LINK_TYPE_MAP[typeCode] || '0',
lag,
};
}

/**
* Parse a full predecessor string that may contain multiple dependencies
* @param {string} predecessorString - Full predecessor string from database (e.g., "3,4FS+2,5SS-1")
* @returns {array} - Array of link objects
*/
function parsePredecessors(predecessorString) {
if (!predecessorString || predecessorString.trim() === '') {
return [];
}

const parts = predecessorString.split(',');
const links = [];

for (const part of parts) {
const link = parseSinglePredecessor(part);
if (link) {
links.push(link);
}
}

return links;
}

async function migrateDependencies() {
const connection = await pool.getConnection();

try {
console.log('Starting dependency migration...\n');

// Step 1: Query all tasks that have predecessors
const [tasks] = await connection.query(
'SELECT TaskID, TaskName, Predecessor FROM syncfusion_tasks WHERE Predecessor IS NOT NULL AND Predecessor != ""'
);

console.log(`Found ${tasks.length} tasks with predecessors\n`);

const linksToInsert = [];
let skippedCount = 0;

// Step 2: Parse each task's predecessor string
for (const task of tasks) {
const targetId = task.TaskID;
const predecessorString = task.Predecessor;
const links = parsePredecessors(predecessorString);

if (links.length === 0) {
console.log(`No valid links parsed`);
skippedCount++;
continue;
}

// Step 3: Create link objects for insertion
for (const link of links) {
linksToInsert.push({
source: link.source,
target: targetId,
type: link.type,
lag: link.lag || 0,
});

const typeName = Object.keys(LINK_TYPE_MAP).find((key) => LINK_TYPE_MAP[key] === link.type);
console.log(`Link: ${link.source} -> ${targetId} (${typeName})`);
}
}

console.log(`\n--- Summary ---`);
console.log(`Tasks processed: ${tasks.length}`);
console.log(`Links to create: ${linksToInsert.length}`);
console.log(`Tasks skipped: ${skippedCount}\n`);

// Step 4: Insert links into database (with transaction)
if (linksToInsert.length > 0) {
await connection.beginTransaction();

try {
// Clear existing links to avoid duplicates
await connection.query('DELETE FROM gantt_links');
console.log('Cleared existing links from gantt_links table');

// Insert each link
for (const link of linksToInsert) {
await connection.query('INSERT INTO gantt_links (source, target, type, `lag`) VALUES (?, ?, ?, ?)', [
link.source,
link.target,
link.type,
link.lag,
]);
}

await connection.commit();
console.log(`Successfully inserted ${linksToInsert.length} links\n`);

const [insertedLinks] = await connection.query('SELECT * FROM gantt_links ORDER BY id');
console.log('Inserted links:');
console.table(insertedLinks);
} catch (error) {
await connection.rollback();
throw error;
}
}

console.log('\nMigration completed successfully!');
} catch (error) {
console.error('Error during migration:', error);
throw error;
} finally {
connection.release();
await pool.end();
}
}

// Run the migration
migrateDependencies().catch(console.error);

Then add a script to your dhtmlx-demo/package.json:

{
"scripts": {
"migrate-deps": "node migrate-dependencies.js"
}
}

Run the migration:

cd dhtmlx-demo
npm run migrate-deps

You can verify that the data was migrated correctly by running the following commands:

SELECT * FROM gantt_tasks;
SELECT * FROM gantt_links;

You should see all your tasks and links properly transferred with the correct field mappings.

Mapping Syncfusion Task Fields to DHTMLX Gantt

Syncfusion FieldDHTMLX FieldNotes
TaskIDidTask id
TaskNametextTask name
StartDatestart_dateTask start date
EndDateend_dateTask end date (calculated in DHTMLX if not provided)
DurationdurationTask duration
DurationUnit(config)DHTMLX Gantt uses a global duration unit configured via gantt.config.duration_unit. During migration, it's recommended to normalize all durations to a single unit. If you want to have different duration units for different tasks, i.e. to show durations of some tasks in hours and some tasks in "days", you can use the formatter module.
ProgressprogressSyncfusion: 0-100%, DHTMLX: 0-1 (decimal)
ParentIdparentParent task ID (0 for root tasks)
Predecessor(links table)Syncfusion stores as string, DHTMLX uses separate gantt_links table
info (notes)-Can be added as a custom column. Check this article for more information: How to add a custom column in the grid
isExpandopenExpand/collapse state for parent tasks
IndicatorsmarkersDHTMLX uses gantt.addMarker() API. Learn more about adding vertical markers

Step 2: Backend Migration (server.js)

Remove Syncfusion Endpoints

Delete the following Syncfusion-specific endpoints from your server.js:

  • app.post('/api/getTasks', ...) - Syncfusion data loading endpoint
  • app.post('/api/batchTasks', ...) - Syncfusion batch sync endpoint

Install DHTMLX Gantt Package and Vite

Remove Syncfusion dependency:

npm uninstall @syncfusion/ej2

Install DHTMLX Gantt following the installation guide.

For this tutorial, we will use the trial version of DHTMLX Gantt:

npm install @dhx/trial-gantt

Let's also install Vite as a build tool:

npm install --save-dev vite

Add Data Loading Endpoint

We'll use the date-format-lite library to format dates from MySQL DATETIME format to the format expected by DHTMLX.

Install the library:

npm install date-format-lite

Add the GET endpoint to load data in DHTMLX format:

import dateFormat from 'date-format-lite';

// GET /data - Load tasks and links
app.get('/data', async (req, res) => {
try {
const [tasks] = await pool.query('SELECT * FROM gantt_tasks ORDER BY id');
const [links] = await pool.query('SELECT * FROM gantt_links');

tasks.forEach((task) => {
if (task.start_date) {
task.start_date = task.start_date.format('YYYY-MM-DD hh:mm:ss');
}
if (task.end_date) {
task.end_date = task.end_date.format('YYYY-MM-DD hh:mm:ss');
}
});

res.json({
tasks,
links,
});
} catch (error) {
console.error('Error loading data:', error);
res.status(500).json({ error: 'Failed to load data' });
}
});

Note: The response format is different from Syncfusion ({ result: [...], count: number }). DHTMLX expects { tasks: [], links: [] }.

DHTMLX Gantt's DataProcessor uses RESTful endpoints to synchronize data with the server. Each operation (create, update, delete) is sent as a separate HTTP request with the appropriate method. Learn more about Server-side integration.

Add handlers for task operations:

// Create a new task
app.post('/data/task', async (req, res) => {
try {
const task = getTask(req.body);

const [result] = await pool.query(
`INSERT INTO gantt_tasks (text, start_date, end_date, duration, progress, parent, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[task.text, task.start_date, task.end_date, task.duration, task.progress, task.parent, task.notes]
);
sendResponse(res, 'inserted', result.insertId);
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// Update an existing task
app.put('/data/task/:id', async (req, res) => {
try {
const taskId = req.params.id;
const task = getTask(req.body);
await pool.query(
`UPDATE gantt_tasks
SET text = ?, start_date = ?, end_date = ?, duration = ?, progress = ?, parent = ?, notes = ?
WHERE id = ?`,
[task.text, task.start_date, task.end_date, task.duration, task.progress, task.parent, task.notes, taskId]
);
sendResponse(res, 'updated');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// Delete a task
app.delete('/data/task/:id', async (req, res) => {
try {
const taskId = req.params.id;
await pool.query('DELETE FROM gantt_tasks WHERE id = ?', [taskId]);
sendResponse(res, 'deleted');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

Add handlers for link (dependency) operations:

// POST /data/link - Create new link
app.post('/data/link', async (req, res) => {
try {
const link = getLink(req.body);
const [result] = await pool.query('INSERT INTO gantt_links (source, target, type) VALUES (?, ?, ?)', [
link.source,
link.target,
link.type,
]);
sendResponse(res, 'inserted', result.insertId);
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// PUT /data/link/:id - Update existing link
app.put('/data/link/:id', async (req, res) => {
try {
const linkId = req.params.id;
const link = getLink(req.body);
await pool.query('UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?', [
link.source,
link.target,
link.type,
linkId,
]);
sendResponse(res, 'updated');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

// DELETE /data/link/:id - Delete link
app.delete('/data/link/:id', async (req, res) => {
try {
const linkId = req.params.id;
await pool.query('DELETE FROM gantt_links WHERE id = ?', [linkId]);
sendResponse(res, 'deleted');
} catch (error) {
sendResponse(res, 'error', null, error);
}
});

Add Helper Functions

Also, let's add utility functions to process data and send responses:

// Helper: Parse task data from request
function getTask(data) {
return {
text: data.text,
start_date: data.start_date,
end_date: data.end_date,
duration: data.duration || 1,
progress: parseFloat(data.progress) || 0,
parent: data.parent || 0,
notes: data.notes || null,
};
}

// Helper: Parse link data from request
function getLink(data) {
return {
source: data.source,
target: data.target,
type: data.type,
};
}

// Helper: Send response to DataProcessor
function sendResponse(res, action, tid = null, error = null) {
if (error) {
console.error('Error:', error);
return res.status(500).json({ action: 'error', message: error.message });
}
const result = { action };
if (tid !== null) result.tid = tid;
res.json(result);
}

Step 3: Frontend Migration with Vite

Set Up Vite Configuration

Create a vite.config.js file in the root of your project:

import { defineConfig } from 'vite';

export default defineConfig({
root: './src',
server: {
port: 5173,
proxy: {
'/data': {
target: 'http://localhost:1337',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

Organize your project with this structure:

dhtmlx-demo/
├── src/ # Frontend source code
│ ├── app/
│ │ └── app.ts # Main Gantt initialization
│ ├── index.html # Main HTML file
│ ├── resources/
│ └── styles/
├── e2e/ # End-to-end tests (optional)
├── .env.example
├── .gitignore
├── migrate-dependencies.js # Dependency migration script
├── package.json # Project dependencies
├── server.js # Express server
├── setup.sql # Database setup script
├── tsconfig.json # TypeScript configuration
└── vite.config.js # Vite configuration

Update index.html

Update index.html with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
<title>DHTMLX Gantt Chart Demo</title>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<meta name="description" content="DHTMLX Gantt Chart - MySQL Integration Demo" />
<meta name="author" content="DHTMLX" />

<style>
html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
font-family: Arial, sans-serif;
}

.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
}

h1 {
color: #333;
margin: 0 0 20px 0;
}

#gantt_here {
flex-grow: 1;
width: 100%;
}
</style>
</head>

<body>
<div class="container">
<h1>DHTMLX Gantt Chart - MySQL Integration Demo</h1>
<div id="gantt_here"></div>
</div>

<script type="module" src="./app/app.ts"></script>
</body>
</html>

Note: The container ID changed to gantt_here, which is DHTMLX Gantt's conventional container ID.

Update src/app/app.ts

In the src/app/app.ts file, remove all Syncfusion-related imports and code.

Replace with DHTMLX Gantt initialization:

import '@dhx/trial-gantt/codebase/dhtmlxgantt.css';
import { Gantt } from '@dhx/trial-gantt';

const gantt = Gantt.getGanttInstance();

gantt.config.date_format = '%Y-%m-%d %H:%i:%s';
gantt.config.scale_height = 50;

gantt.config.scales = [
{ unit: 'month', step: 1, format: '%F %Y' },
{ unit: 'day', step: 1, format: '%d' },
];

gantt.config.lightbox.sections = [
{ name: 'description', height: 38, map_to: 'text', type: 'textarea', focus: true },
{ name: 'time', height: 72, type: 'duration', map_to: 'auto' },
{ name: 'notes', height: 70, map_to: 'notes', type: 'textarea' },
];

gantt.init('gantt_here');
gantt.load('/data');

const dp = gantt.createDataProcessor({
url: '/data', // Base URL for REST endpoints
mode: 'REST', // Use RESTful mode
});

The DataProcessor will automatically:

  • Send POST requests to /data/task when creating tasks
  • Send PUT requests to /data/task/:id when updating tasks
  • Send DELETE requests to /data/task/:id when deleting tasks
  • Handle links similarly with /data/link endpoints

Update package.json Scripts

Update your package.json scripts to use Vite:

{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"server": "nodemon server.js",

"serve": "gulp e2e-serve",
"test": "gulp e2e-test",
"migrate-deps": "node migrate-dependencies.js",
"update-webdriver": "gulp e2e-webdriver-update"
}
}

Step 4: Testing the Migration

Running the Application

For development mode, you need to run two processes:

Terminal 1 - Backend (Express):

npm run server

This starts the API server on http://localhost:1337 (or your configured port)

Terminal 2 - Frontend (Vite):

npm run dev

This starts the Vite dev server on http://localhost:5173. Open your browser and navigate to http://localhost:5173. Vite will proxy API requests to the Express backend automatically.

You should see the DHTMLX Gantt chart with your data loaded from the database:

Gantt with data loaded

Next Steps

Need help?
Got a question about the documentation? Reach out to our technical support team for help and guidance. For custom component solutions, visit the Services page.