Bryntum
9 May 2023

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 […]

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:

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:

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:

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:

We also have a support forum and offer professional services.

Bryntum

Bryntum Gantt