Creating a Gantt chart with Frappe Gantt
A Gantt chart is a commonly used project management tool for scheduling tasks. In a previous article, we described how to make a basic drag-and-drop Gantt chart component using vanilla JavaScript. In this article, we’ll make a similar Gantt chart using Frappe Gantt, which is an open-source JavaScript Gantt library. We’ll also create a basic Gantt chart using our commercial Bryntum Gantt chart and look at the differences between building your own chart using Frappe Gantt versus using an off-the-shelf solution.
The basic Gantt chart that we’ll create using Frappe Gantt will have the following features:
- Tasks can be added and deleted.
- The task date can be changed using drag and drop.
- The task can be resized by clicking and dragging the sides of the taskbars.
- The tasks have a progress bar indicating task completion as a percentage.
- The Gantt chart view can be changed: day, week or month.
- The tasks can have dependencies.
- The tasks can be labeled as important, which changes the taskbar color.
We’ll also build a Gantt chart using Bryntum Gantt for comparison. Once you’re done, you’ll have built both of the following charts.
You can find the code for the completed Frappe Gantt chart in our GitHub repository.
Getting started
We’ll start this project by cloning a Frappe Gantt chart starter GitHub repository. The starter repository contains all of the files that you’ll need to create the Gantt chart. It uses Vite, which is a development server and JavaScript bundler. You’ll need Node.js version 14.18+ for Vite to work. The dependencies needed for our Gantt chart are Frappe Gantt and SASS. The Frappe Gantt chart is dependent on SASS. Now install the dependencies by running the following command:
npm install
The following Vite commands are available:
npm run dev
runs vite to start the dev server.npm run build
runs vite build to build the production bundle to thedist
folder.npm run preview
runs vite preview to serve the production bundle.
Run the local development server using npm run dev
. You’ll see a page with the text “Loading…” in the middle of the screen.
Let’s take a look at how the Frappe Gantt chart starter code is structured.
Adding styling with CSS
The CSS we’ll use for the Gantt chart is included in the starter code. The styles are in the styles.css
file. There are styles for the Frappe Gantt chart as well as the forms and buttons that we’ll create.
Utility functions, helpers, constants, and data
The fetchWrapper.js
file in the utils
folder contains a fetch API wrapper function that we’ll use to make an HTTP request to fetch our data. This wrapper function is from Kent C. Dodd’s article Replace Axios with a simple custom fetch wrapper. We’ll use this fetch wrapper function to fetch the example Gantt chart data, which is in the data.json
file in the public/data
folder.
The dateFunctions.js
file in the utils
folder contains a createFormattedDateFromStr
function that we’ll use to format date strings. The constants.js
file contains a months
array that we’ll use for getting month names from their index value.
Now let’s start creating the Frappe Gantt chart component.
Creating the basic Frappe Gantt chart
First, we’ll create a basic Frappe Gantt chart by fetching the tasks data from the data.json
file in the public/data
folder. We’ll then create a Frappe Gantt instance and pass the data to it. Add the following code to the main.js
file:
import Gantt from "frappe-gantt";
import { client } from "./utils/fetchWrapper.js";
let ganttChart;
let tasks;
async function fetchData() {
client("data/data.json").then(
(data) => {
tasks = data;
ganttChart = new Gantt("#gantt", tasks, {
bar_height: 25,
view_mode: "Week",
});
showGantt();
},
(error) => {
showErrorMsg();
},
hideLoader()
);
}
function hideLoader() {
document.getElementsByClassName("loading")[0].style.display = "none";
}
function showGantt() {
document.getElementsByClassName("gantt-wrapper")[0].style.display = "block";
}
function showErrorMsg() {
document.getElementsByClassName("error")[0].style.display = "flex";
}
fetchData();
We’ll store the tasks state in the tasks
variable. The fetchData
function will run on page load; this async function will use our client fetch function to get the task data. This is then passed into the created Gantt chart instance. The first argument of the Gantt constructor function is the id
of the <svg>
element that will contain the Gantt chart. This <svg>
element is part of the starter code in the index.html
file. The second argument is the data and the third argument is an options object for configuring the Gantt chart.
We set the bar_height
of the taskbars and the view_mode
of the calendar. On page load, the “…loading” <div>
is displayed. It is then hidden using the hideLoader
function after the data fetch request. If there is an error, the error message <div>
will be displayed. If the request is successful, the Gantt chart is shown using the showGantt
function.
If you run your local server now, you’ll see the following Gantt chart in your browser window:
The Frappe Gantt has drag, resize, task progress display and dependencies built in. The last task, “Set up test strategy”, is red because it has the custom class “is-important”. You can see this in the data.json
file. The styling for it is in the styles.css
file. Now let’s customize our Gantt chart and add more features.
Customizing the task tooltip pop-up
We’re going to customize the tooltip that’s displayed when you double-click a taskbar. Add the following property in the Gantt chart constructor function’s options argument, below the view_mode
property:
custom_popup_html: function (task) {
const start_day = task._start.getDate();
const start_month = months[task._start.getMonth()];
const end_day = task._end.getDate();
const end_month = months[task._end.getMonth()];
return `
<div class='details-container'>
<h5>${task.name}</h5>
<br>
<p>Task started on: ${start_day} ${start_month}</p>
<p>Expected to finish by ${end_day} ${end_month}</p>
<p>${task.progress}% completed!</p>
</div>
`;
},
The value of this property is a function that accepts a task as an argument. It returns a string that is displayed in the task. The “details-container” class is defined in our styles.css
file. We display the task name, start date, end date and progress. If you use this in production, make sure that you sanitize the task data as creating HTML with user-supplied data is a security risk.
We also need to import the months
array:
import { months } from "./constants.js";
Now when you double-click a taskbar, you’ll see our custom tooltip:
Adding tasks
Let’s create a form that will allow us to add new tasks. In the index.html
file, add the following HTML code as the second child of the <div>
with a class of “gantt-wrapper”:
<div id="controls-container">
<form id="add-task">
<h2>Add task</h2>
<label for="task-name">Task name</label>
<input id="task-name" name="task-name" required />
<label for="start-date">Start date</label>
<input
type="date"
id="start-date"
name="start-date"
value="2022-11-08"
min="2022-01-01"
max="2050-12-31"
/>
<label for="end-date">End date</label>
<input
type="date"
id="end-date"
name="end-date"
value="2022-11-10"
min="2022-01-01"
max="2050-12-31"
/>
<label for="progress">Progress (%)</label>
<input
type="number"
id="progress"
name="progress"
value="0"
min="0"
max="100"
/>
<div class="important-checkbox-container">
<label for="is-important">Is it an important task?</label>
<input type="checkbox" id="is-important" name="is-important" />
</div>
<button>Add</button>
</form>
</div>
This form contains all of the task properties except for the dependencies. We’ll add dependency inputs later.
Now in the main.js
file, add the following line at the top to select the “Add task” form:
const addForm = document.getElementById("add-task");
At the bottom of the main.js
file, add the following event listener to listen for the form submit:
addForm.addEventListener("submit", addTask);
Add the following addTask
function below the showErrorMsg
function:
function addTask(e) {
e.preventDefault();
const formElements = e.target.elements;
const name = formElements["task-name"].value;
const start = formElements["start-date"].value;
const end = formElements["end-date"].value;
const progress = parseInt(formElements["progress"].value);
const isImportant = formElements["is-important"].checked;
const newtask = {
id: `${Date.now()}`,
name,
start,
end,
progress,
custom_class: isImportant ? "is-important" : "",
};
tasks.push(newtask);
ganttChart.refresh(tasks);
}
This function gets the values from the “Add task” form, creates a new task object and then updates the tasks
variable, which is the tasks state of our Gantt chart. We use the Frappe Gantt refresh
method to update our Gantt chart with the new task state.
You’ll now be able to add new tasks to your Gantt chart. However, there’s a problem with updating the Gantt chart state. Try changing the date of a task by dragging or resizing it and then add a new task. After the new task is added, the task date changes back to its original value. To fix this, we’ll use two event listeners provided by Frappe Gantt: on_date_change
and on_progress_change
. First, import our date formatting function:
import { createFormattedDateFromStr } from "./utils/dateFunctions.js";
Now add the following properties in the Gantt chart constructor function’s options argument, below the custom_popup_html
property:
on_date_change: function (task, start, end) {
updateDate(task, start, end);
},
on_progress_change: function (task, progress) {
updateProgress(task, progress);
},
The on_date_change
listener accepts a callback function that’s called when a task date is changed. This function calls the updateDate
function. The on_progress_change
listener is called when a task progress value is changed. This function calls the updateProgress
function. Let’s define the updateDate
and updateProgress
functions. Add the following lines below the addTask
function:
function updateDate(task, start, end) {
const startYear = start.getFullYear();
const startMonth = start.getMonth();
const startDay = start.getDate();
const endYear = end.getFullYear();
const endMonth = end.getMonth();
const endDay = end.getDate();
const startStr = createFormattedDateFromStr(
startYear,
startMonth + 1,
startDay
);
const endStr = createFormattedDateFromStr(endYear, endMonth + 1, endDay);
const taskToUpdate = tasks.find((tsk) => tsk.id === task.id);
taskToUpdate.start = startStr;
taskToUpdate.end = endStr;
ganttChart.refresh(tasks);
}
function updateProgress(task, progress) {
const taskToUpdate = tasks.find((tsk) => tsk.id === task.id);
taskToUpdate.progress = progress;
ganttChart.refresh(tasks);
}
The updateDate
function uses the passed-in date arguments as well as our createFormattedDateFromStr
utility function to get the new start
and end
date strings of the task. We then update the tasks state and refresh the Gantt chart with the new state.
The updateProgress
function is similar to the updateDate
function. We update the task progress
, which does not require formatting like updating the start and end dates.
Adding dependencies
In our Gantt chart, most of the tasks from the data.json
file have tasks that they are dependent on. This is indicated by arrows that point from the dependency to the task. Let’s add dependency inputs to our “Add task” form.
In the index.html
file, add the following HTML code just above the <button>Add</button>
line:
<fieldset class="tasks-checkbox-container">
<legend>Dependencies</legend>
</fieldset>
Add the following variable at the top of the main.js
file:
const tasksCheckboxContainers = document.querySelectorAll(
".tasks-checkbox-container"
);
We use querySelectorAll
because we’ll add a similar task checkbox container in the “Delete tasks” form that we’ll create later.
Now add the following function below the updateProgress
function:
function addTaskCheckboxes() {
tasksCheckboxContainers.forEach((container, i) => {
container.innerHTML = "";
const fragment = new DocumentFragment();
const legend = document.createElement("legend");
if (i === 0) {
legend.appendChild(document.createTextNode("Dependencies"));
} else {
legend.appendChild(document.createTextNode("Tasks"));
}
fragment.appendChild(legend);
tasks.map((task) => {
const div = document.createElement("div");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.id = task.id;
checkbox.name = "task";
checkbox.value = task.id;
const label = document.createElement("label");
label.htmlFor = task.id;
label.appendChild(document.createTextNode(task.name));
div.appendChild(checkbox);
div.appendChild(label);
fragment.appendChild(div);
});
container.appendChild(fragment);
});
}
This function is used to dynamically create checkboxes for all of the dependencies, which are the tasks. For each task, we create a checkbox <input>
element. We then append the checkboxes to the form <fieldset>
element.
Now we need to update the addTask
function so that we can add dependencies to a new task. Add the following lines below the const isImportant = formElements["is-important"].checked;
line:
const taskCheckboxes = formElements["task"];
const timeDiff = new Date(end).getTime() - new Date(start).getTime();
if (timeDiff <= 0) return;
const depIds = [];
if (tasks.length === 1) {
if (taskCheckboxes.checked) {
depIds.push(taskCheckboxes.id);
}
} else {
taskCheckboxes.forEach((dep) => {
if (dep.checked) {
depIds.push(dep.id);
}
});
}
Add the following property to the newTasks
object:
dependencies: depIds.join(", "),
Now add the following line below the ganttChart.refresh(tasks);
line:
addTaskCheckboxes();
The timeDiff
variable is a check to make sure that the user does not select an invalid input range. The depIds
array stores the id
values of the checked dependencies. We loop through the taskCheckboxes
and add the task ID if the checkbox is checked. We then add the dependencies
as a property to the newTask
object.
After the Gantt chart is refreshed with the new tasks, we call the addTaskCheckboxes
function so that the dependency checkboxes are updated. You’ll see that at the start of the addTaskCheckboxes
function, the checkboxes <fieldset>
container is cleared before adding the new dependencies by setting the innerHTML
to an empty string.
We also need to create the checkboxes when the Gantt chart is first created. Add the following function call below the showGantt()
line in the fetchData
function:
addTaskCheckboxes();
You’ll now be able to add dependencies to your new tasks:
Deleting tasks
Let’s create a form that will allow us to delete tasks. In the index.html
file, add the following “Delete tasks” form below the “Add task” form:
<form id="delete-tasks">
<h2>Delete tasks</h2>
<fieldset class="tasks-checkbox-container">
<legend>Tasks</legend>
</fieldset>
<small class="delete-msg">
The Gantt chart can't have no tasks. See this
<a
target="_blank"
rel="noreferrer"
href="https://github.com/frappe/gantt/issues/84"
>
Frappe Gantt GitHub issue</a
>.
</small>
<button>Delete</button>
</form>
We’ll create a dynamic group of checkboxes like we did with the “Add tasks” form dependencies. We’ll also show the user a message if they try to delete all of the tasks. There is an open GitHub issue with the Frappe Gantt chart: you can’t make a chart without any tasks.
Now add the following variables at the top of the main.js
file:
const deleteForm = document.getElementById("delete-tasks");
const msgEl = document.querySelector(".delete-msg");
Add the following deleteTasks
function below the addTask
function:
function deleteTasks(e) {
e.preventDefault();
const formElements = e.target.elements;
const taskCheckboxes = formElements["task"];
if (tasks.length === 1) {
msgEl.style.display = "block";
return;
}
const taskIds = [];
taskCheckboxes.forEach((task) => {
if (task.checked) {
taskIds.push(task.id);
}
});
if (taskIds.length === 0) return;
if (taskIds.length === tasks.length) {
msgEl.style.display = "block";
return;
}
// remove deleted tasks
const filteredTasks = tasks.filter((task) => !taskIds.includes(task.id));
// remove deleted dependencies
const newTasks = filteredTasks.map((tsk) => {
if (tsk.dependencies.length === 0) {
return tsk;
}
const depsArr = tsk.dependencies;
const newDeps = depsArr.filter((dep) => !taskIds.includes(dep));
return {
...tsk,
dependencies: newDeps,
};
});
tasks = newTasks;
ganttChart.refresh(tasks);
addTaskCheckboxes();
msgEl.style.display = "none";
}
This function gets the tasks to delete from the checkboxes that were checked in the “Delete tasks” form and stores their task IDs in the taskIds
variable. The code used is similar to the code used in the addTask
function to get the checked checkboxes. We loop through the tasks
variable and remove the deleted tasks. We then loop through these tasks and remove any dependencies that no longer exist. We then update the Gantt chart with the new tasks state using the refresh
method.
We also do some checks to determine if the user is trying to delete all of the tasks. If they are, we return from the function and display the message showing that they can’t delete all of the tasks.
We also want to remove this message if the user adds a task. Add the following line inside of the addTask
function, at the bottom of it:
msgEl.style.display = "none";
Lastly, we need to add an event listener that calls the deleteTasks
function when the “Delete tasks” form is submitted. Add the following line at the bottom of the main.js
file:
deleteForm.addEventListener("submit", deleteTasks);
You’ll now be able to delete tasks.
Changing view modes
The last feature that we’ll add to our Frappe Gantt chart will be some buttons that will allow us to change the time range view of the Gantt chart. In the index.html
file, add the following lines below the “Delete tasks” form:
<div class="chart-controls">
<h2>Tracker Time Range</h2>
<div class="button-container">
<button id="day-btn">Day</button>
<button id="week-btn">Week</button>
<button id="month-btn">Month</button>
</div>
</div>
We’ll add some event listeners and functions to make these buttons functional. Add the following function in the main.js
file, below the addTaskCheckboxes
function:
function addViewModes() {
document.getElementById("day-btn").addEventListener("click", () => {
ganttChart.change_view_mode("Day");
});
document.getElementById("week-btn").addEventListener("click", () => {
ganttChart.change_view_mode("Week");
});
document.getElementById("month-btn").addEventListener("click", () => {
ganttChart.change_view_mode("Month");
});
}
This function selects the buttons and adds “click” event listeners to them that change the Gantt chart’s view mode.
We need to call this function. Add the following function call inside of the fetchData
function, below the addTaskCheckboxes();
line:
addViewModes();
Now if you click one of the buttons, the time range view will change.
We’ve completed our basic Frappe Gantt chart, but the process was a little tedious and the library is a bit buggy. Now let’s create a Gantt chart using our Bryntum Gantt component for comparison.
Creating a Gantt chart using Bryntum
We’ll start by cloning the Bryntum Gantt chart starter GitHub repository. The starter repository contains all of the files that you’ll need to create the Gantt chart. It also uses Vite. Install the dependencies by running the following command:
npm install
Run the local development server using npm run dev
. You’ll see a blank page.
To install Bryntum Gantt with npm, you’ll just need to install one library with zero external dependencies. If you are unfamiliar with Bryntum products, you can follow the guide to installing the Gantt chart component using npm.
The styles.css
file contains some basic styling for the Gantt chart. We set the <HTML>
and <body>
elements to have a height of 100vh so that the Bryntum Gantt chart will take up the full height of the screen. We also have custom styling for important tasks using the “is-important” class, as we did with the Frappe Gantt.
The data for the Gantt chart is in the data.json
file in the public/data
folder. There is one important task in the data. It has a cls
property value of “is-important”. This is a custom CSS class.
The data is similar to the Frappe Gantt data. There is a parent row called “Launch SaaS Product” that contains the tasks data. This row is expandable. This feature is especially useful if there is a lot of data.
Now let’s create a simple Bryntum Gantt chart. Add the following lines to the main.js
file:
import { Gantt } from "@bryntum/gantt";
import "@bryntum/gantt/gantt.stockholm.min.css";
const gantt = new Gantt({
appendTo: document.body,
project: {
transport: {
load: {
url: "data/data.json",
},
},
autoLoad: true,
},
columns: [{ type: "name", width: 250, text: 'Tasks' }],
});
We import the Bryntum Gantt component and the CSS for the Stockholm theme, which is one of five available themes. You can see a demo showing the different themes here. You can also create custom themes. A theme is needed to render the Bryntum Gantt correctly. We then create a new Gantt instance and pass it a configuration object.
We configured the project with the transport property to populate the Gantt chart’s data stores. We’ve configured the url
to load data using the load property. You can also configure the transport to sync data changes to a specific URL. For more information, you can read the project data guide in our docs.
We also created a single task column to display the tasks.
Now, when you run your development server, you’ll see the Bryntum Gantt chart:
This basic example of the Bryntum Gantt component has more features by default than the Frappe Gantt chart that we created. The features include:
- Collapsible task groups.
- Drag-and-drop re-ordering of tasks.
- Add, edit, copy, and delete tasks. Right-click on a task to see a pop-up menu with these actions and more, including editing dependencies.
- Draggable task durations.
- Resizeable task durations. Hover over the left or right side of a task duration until the cursor changes to a resize icon. Then click and drag left or right.
- Create task dependencies. Hover over the left or right side of a task duration until you see a circle icon. Then click and drag to connect the task to another task.
- Filter tasks by name.
- Change date ranges.
Try out some of these features in your app.
Comparing the Frappe Gantt and Bryntum Gantt charts
Let’s compare the Frappe Gantt chart that we created with the basic example of the Bryntum Gantt chart. The Frappe Gantt chart was easier to create than the vanilla JavaScript Gantt that we created in a previous article. It also has a dependencies feature, which wouldn’t be easy to implement yourself. However it has some issues. You can’t have no tasks in the Gantt chart, as can be seen in this open GitHub issue. The project also has many other open issues and it’s not actively maintained. Another bug in our Frappe Gantt is that if you open a tasks tooltip by double-clicking it and then you delete the task, the tooltip will remain.
The Frappe Gantt also has limited functionality, so we had to code a lot of the functionality ourselves. It’s also not easy to customize, for example changing the date rows, because it’s an SVG.
The Bryntum Gantt chart was easy to set up. The basic one that we created has more functionality than the Frappe Gantt that we made. It also has a very customizable API that you can learn about in the API docs. You can see all the available features here and view demos of the features here. The Bryntum Gantt chart is built with pure JavaScript/ES6+ and it uses a very fast rendering engine, which makes its performance great even with large data sets.
Next steps
There are many ways that you can add to or improve the Frappe Gantt chart component as described in the “Next steps” section of our previous article where we used vanilla JavaScript to create a Gantt chart. You could start by adding a way to edit a task’s name or its dependencies.
You could also learn how to make a Gantt chart using Next.js in our tutorial: Creating a Gantt chart with React using Next.js.
Build or buy?
This article gives you a starting point for building a Gantt chart using Frappe Gantt. If you’re instead looking to buy an off-the-shelf, battle-tested solution that just works, take a look at some examples of our Bryntum Gantt. With the Bryntum Gantt component, you can:
- Schedule tasks using dependencies and constraints.
- Leverage calendars for projects, tasks, and resources.
- Use recurrent and fixed time intervals.
- Customize rendering and styling.
- Customize user experience through many different column types and task editors.
- Deal with extensive data sets and performance tuning.
We also have a support forum and offer professional services.