Build Interactive Gantt Charts with Airtable, Gatsby and React

Build Interactive Gantt Charts with Airtable, Gatsby and React

Build Interactive Gantt Charts with Airtable, Gatsby & React – SitePoint

Skip to main content

With Gatsby, it’s very easy to integrate different data sources into one application. In this article, we’ll develop a task management tool whose data is fetched from Airtable. We’ll use React for the front end, with a hybrid rendering strategy.

It’s a common scenario: you want to develop an app that connects to data from a spreadsheet application or some other data source. In this article, I’ll show you how to implement an application of this type with the Gatsby framework. In our example application, tasks will be imported from an Airtable workspace and visualized as a Gantt chart. The user can move the tasks by drag and drop, after which all changes will be synchronized with Airtable. You can use the project as a template for all kinds of scheduling apps.

A simple task table

You can try the result live on my Gatsby Cloud site. The src files of the project can be found in my GitHub repository.

Setting Up the Project

Gatsby is a static site generator. This means you write your application with React, and Gatsby translates your code into HTML files that are understandable to the browser. This build process is carried out at regular intervals on the server side, in contrast to conventional web applications where the HTML code is first assembled on the client side in the user’s browser. The HTML files are therefore statically available on the server (hence the name static site generator) and can be sent directly to the client when requested. This reduces the loading time of the application for the user.

SitePoint’s Gatsby tutorial provides all the information you need to develop an application with this framework. If you want to develop my example application step by step, you should start as outlines below.

First, you should download and install Node.js. You can check if it’s installed correctly by typing node -v on the console. The current version of Node should be displayed:

node -v
> v14.16.0

With Node we also get npm, the Node package manager. With this tool, we can now install the Gatsby CLI:

npm install -g gatsby-cli

We’re ready to create a new project using the Gatsby CLI. I name it “gantt-chart-gatsby”:

gatsby new gantt-chart-gatsby

Then move into the project folder with the command cd gantt-chart-gatsby and build the project with the command gatsby develop. Now you can open the index page of the project in the browser on http://localhost:8000. At first, you should only see the welcome page that Gatsby has prepared for us.

In the next step, you should examine the src folder of the project. The subfolder src/pages contains the React components of the individual pages of the project. For now, it’s sufficient for you to keep the index.js file for the index page, because, in our example application, we only need one page. You can delete the other files in this folder, except for 404.js (which can be useful if someone enters a wrong address).

It’s a good starting point if you overwrite the existing code in index.js with this code:

import * as React from 'react' const IndexPage = () => { return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> </main> )
} export default IndexPage;

You can build the project again with the command gatsby develop on the command line and open the index page in the browser. Now you should see an empty page with the heading “Welcome to my Gatsby Gantt Chart”.

Building the Front End with React

The first version of the index page

We will implement the Gantt chart as a reusable React component. Before I explain the implementation of the component in detail in the following sections, I’d first like to show how it’s initialized and embedded in the index page. So I’d recommend you to hold off using the gatsby develop command until we’ve finished the first version of the component. (I’ll let you know when we’re ready!)

In this example project, I use the concept of “jobs” and “resources”. Jobs are the tasks that are drawn into the chart cells and that can be moved by drag and drop. Resources contain the labels for the rows in which the jobs can be moved. These can be names for the tasks, but in other use cases also the names of people, vehicles or machines carrying out the tasks.

Jobs and resources are passed to the Gantt chart component as properties. Before connecting the task management tool to Airtable, we fill the lists with some hard-coded test data in JSON format:

import * as React from "react";
import {GanttChart} from "../GanttChart";
import "../styles/index.css"; let j = [ {id: "j1", start: new Date("2021/6/1"), end: new Date("2021/6/4"), resource: "r1"}, {id: "j2", start: new Date("2021/6/4"), end: new Date("2021/6/13"), resource: "r2"}, {id: "j3", start: new Date("2021/6/13"), end: new Date("2021/6/21"), resource: "r3"},
]; let r = [{id:"r1", name: "Task 1"}, {id:"r2", name: "Task 2"}, {id:"r3", name: "Task 3"}, {id:"r4", name: "Task 4"}]; const IndexPage = () => { return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <GanttChart jobs={j} resources={r}/> </main> )
}; export default IndexPage;

CSS styles for the Gantt chart

In the next step, we create a new index.css file in the styles folder. (If the folder doesn’t exist, create a new folder styles in the folder src of the project.) The following CSS settings control the layout and appearance of the Gantt chart:

body{ font-family: Arial, Helvetica, sans-serif;
} #gantt-container{ display: grid; } .gantt-row-resource{ background-color:whitesmoke; color:rgba(0, 0, 0, 0.726); border:1px solid rgb(133, 129, 129); text-align: center; padding: 15px;
} .gantt-row-period{ background-color:whitesmoke; color:rgba(0, 0, 0, 0.726); border:1px solid rgb(133, 129, 129); text-align: center; display:grid; grid-auto-flow: column; grid-auto-columns: minmax(40px, 1fr);
} .period{ padding: 10px 0 10px 0;
} .gantt-row-item{ border: 1px solid rgb(214, 214, 214); padding: 10px 0 10px 0; position: relative; background-color:white;
} .job{ position: absolute; height:38px; top:5px; z-index: 100; background-color:rgb(167, 171, 245); cursor: pointer;

Implementing the GanttChart component

Now I’ll explain the implementation of the GanttChart component in more detail. First, we need a file named GanttChart.js in the src folder. In this tutorial, I use a simplified version of the GanttChart for only one month (June 2021). An extended version with select fields for starting month and end month can be found at GitHub under the name GanttChart_extended.js.

The chart table is built up in three steps, represented by the functions initFirstRow, initSecondRow and initGanttRows:

import React from 'react'; export class GanttChart extends React.Component { names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; constructor(props) { super(props); this.state = { dateFrom: new Date(2021,5,1), dateTo: new Date(2021,5,30), }; } render(){ let month = new Date(this.state.dateFrom.getFullYear(), this.state.dateFrom.getMonth(), 1); let grid_style = "100px 1fr"; let firstRow = this.initFirstRow(month); let secondRow = this.initSecondRow(month); let ganttRows = this.initGanttRows(month); return ( <div className="gantt-chart"> <div id="gantt-container" style={{gridTemplateColumns : grid_style}}> {firstRow} {secondRow} {ganttRows} </div> </div> ); } initFirstRow(month){...} initSecondRow(month){...} initGanttRows(month){...} formatDate(d){ return d.getFullYear()+"-"+this.zeroPad(d.getMonth()+1)+"-"+this.zeroPad(d.getDate()); } zeroPad(n){ return n<10 ? "0"+n : n; } monthDiff(d1, d2) { let months; months = (d2.getFullYear() - d1.getFullYear()) * 12; months -= d1.getMonth(); months += d2.getMonth(); return months <= 0 ? 0 : months; } dayDiff(d1, d2){ let diffTime = Math.abs(d2 - d1); let diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; } }

In the initFirstRow function, the first row of the chart table is generated. As you can see from the picture above, the first row consists of two grid cells. These are generated as divs, which in turn are inserted as children into the “gantt-container” (see the listing above). The second div also contains the label for the current month.

React requires a unique “key” property for all elements that are part of an enumeration. This helps to optimize the rendering performance:

 initFirstRow(month){ let elements = []; let i = 0; elements.push(<div key={"fr"+(i++)} className="gantt-row-resource"></div>); elements.push(<div key={"fr"+(i++)} className="gantt-row-period"><div className="period">{this.names[month.getMonth()] + " " + month.getFullYear()}</div></div>); return elements; }

The next row of the chart table is generated in the initSecondRow function. We use the same principle again: for each table cell, a div is created. You have to make sure that the divs are nested correctly (the second div in the row contains individual divs for each day of the month) so that the CSS Grid settings (see the index.css file) will produce the desired layout:

initSecondRow(month){ let elements = []; let i=0; elements.push(<div key={"sr"+(i++)} style={{borderTop : 'none'}} className="gantt-row-resource"></div>); let days = []; let f_om = new Date(month); let l_om = new Date(month.getFullYear(), month.getMonth()+1, 0); let date = new Date(f_om); for(date; date <= l_om; date.setDate(date.getDate()+1)){ days.push(<div key={"sr"+(i++)} style={{borderTop: 'none'}} className="gantt-row-period period">{date.getDate()}</div>); } elements.push(<div key={"sr"+(i++)} style={{border: 'none'}} className="gantt-row-period">{days}</div>); return elements; }

The remaining rows of the chart table are generated in the initGanttRows function. They contain the grid cells into which the jobs are drawn. Again, the rendering is done row by row: for each row we first place the name of the resource, then we iterate over the individual days of the month. Each grid cell is initialized as a ChartCell component for a specific day and resource. With the cell_jobs list, the individual cell is assigned the jobs that need to be drawn into it (typically this is exactly one job):

initGanttRows(month){ let elements = []; let i=0; this.props.resources.forEach(resource => { elements.push(<div key={"gr"+(i++)} style={{borderTop : 'none'}} className="gantt-row-resource">{}</div>); let cells = []; let f_om = new Date(month); let l_om = new Date(month.getFullYear(), month.getMonth()+1, 0); let date = new Date(f_om); for(date; date <= l_om; date.setDate(date.getDate()+1)){ let cell_jobs = => job.resource == && job.start.getTime() == date.getTime()); cells.push(<ChartCell key={"gr"+(i++)} resource={resource} date={new Date(date)} jobs={cell_jobs}/>); } elements.push(<div key={"gr"+(i++)} style={{border: 'none'}} className="gantt-row-period">{cells}</div>); }); return elements;

Now add the following code for the ChartCell component at the end of GanttChart.js. The component renders a single table cell of the chart as a div containing one or more jobs as child elements. The HTML code for displaying a job is provided by the getJobElement function:

class ChartCell extends React.Component { constructor(props) { super(props); this.state = { jobs: } } render(){ let jobElements = => this.getJobElement(job)); return ( <div style={{borderTop: 'none', borderRight: 'none', backgroundColor: ( || ? "whitesmoke" : "white" }} className="gantt-row-item"> {jobElements} </div> ); } getJobElement(job){ let d = this.dayDiff(job.start, job.end); return ( <div style={{width: "calc("+(d*100)+"% + "+ d + "px)"}} className="job" id={} key={} > </div> ); } dayDiff(d1, d2){ let diffTime = Math.abs(d2 - d1); let diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }

At this point, you can build the project from the root folder using the gatsby develop command. The hard-coded jobs from the index page should be visible in the Gantt chart. They can’t be dragged and dropped yet, but we’ll take care of that later.

Integrating Data from Airtable

It’s time to connect our application to Airtable so we can import jobs and resources from there. First, create a free account at Airtable. After logging in, you’ll see an “Untitled Base” (see image below). Click Add a base, then Start from scratch, and enter a name for your base. I entered “Task Manager”.

Adding a base

Setting up the Airtable base with the “Jobs” and “Resources” tables

Now you can define the tables for your base, in the following steps:

  1. Define the table “Jobs” with the fields id (field type: text), start (field type: Date) and end (field type: Date).
  2. Define the table “Resources” with the fields id (field type: text) and name (field type: text).
  3. Go to the table “Jobs”, add a field resource with the field type “Link to another record”, then choose the field id as a lookup field for the table “Resource”.

After these steps, your tables should look like in the images below.

Task manager

Task manager

Importing data from Airtable with GraphQL and Gatsby’s Airtable plugin

Next, we want to import data from Airtable into our application. For this purpose, install the plugin “gatsby-source-airtable” with npm install --save gatsby-source-airtable. Then, modify the gatsby-config.js file in your project folder as shown in the list below:

module.exports = { siteMetadata: { siteUrl: "https://www.yourdomain.tld", title: "Gatsby Gantt Chart", }, plugins: [ "gatsby-plugin-gatsby-cloud", { resolve: "gatsby-source-airtable", options: { apiKey: "XXX", concurrency: 5, tables: [ { baseId: "YYY", tableName: "Jobs", }, { baseId: "YYY", tableName: "Resources", } ] } } ],

Now we can try to fetch data from Airtable. Start your application with gatsby develop, then open the GraphiQL editor in the browser at http://localhost:8000/___graphql and paste the following query into the area on the left:

{ jobs: allAirtable(filter: {table: {eq: "Jobs"}, data: {}}) { edges { node { data { id start end id__from_resource_ resource } recordId } } } resources: allAirtable( filter: {table: {eq: "Resources"}} sort: {fields: [data___name], order: ASC} ) { edges { node { data { id name } } } }

Click on the arrow symbol to run the query. The result of the query should appear on the right side.

The result of the query

Now it’s time to remove the hardcoded lists with jobs and resources in index.js. Update the code in index.js as shown in the following listing. What’s happening here? First, at the end of the file you can see a so-called “page query” that requests all jobs and resources. The result of the query is automatically assigned to the data property of the component IndexPage. Thus, the data property stores exactly what you’ve seen as a query result in the GraphiQL editor on the right side. We can use the map function to transform the jobs and resources arrays into our preferred format.

Even if it seems a bit cumbersome, we have to keep the properties recordID and id__from_resource, which are automatically created by Airtable, for all jobs. This is necessary so that we can later save changes to the jobs via the Airtable REST API:

import * as React from "react"
import { useStaticQuery, graphql } from "gatsby"
import {GanttChart} from "../GanttChart"
import '../styles/index.css'; const IndexPage = (data) => { let j = => { let s = new Date(; s.setHours(0); let e = new Date(; e.setHours(0); return { airtable_id: edge.node.recordId,, start: s, end: e, resource:[0], resource_airtable_id:[0] }; }); let r = => { return{ id:, name: } }); if(r && j){ return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <GanttChart jobs={j} resources={r}/> </main> ) }else{ return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <p>Missing data...</p> </main> ) }
} export const query = graphql` query{ jobs: allAirtable(filter: {table: {eq: "Jobs"}, data: {}}) { edges { node { data { id start end id__from_resource_ resource } recordId } } } resources: allAirtable( filter: {table: {eq: "Resources"}} sort: {fields: [data___name], order: ASC} ) { edges { node { data { id name } } } } } `
export default IndexPage;

If you build and start your application locally with gatsby develop, the data is fetched from Airtable and displayed in your Gantt chart. If you’ve set up a Gatsby Cloud site according to the Gatsby tutorial, the site is updated as soon as you push the code changes to the associated GitHub account. However, you’ll notice that the Airtable query is only executed when the project is built (regardless of whether that happens locally or on the Gatsby Cloud site). If you modify the data in your Airtable base, the changes aren’t reflected in the Gantt chart unless you re-build the project. This is typical for the server-side rendering process of Gatsby.

In the next section, we’ll discuss how to deal with changes in the data.

Realizing a Two-way Synchronization between Gatsby and Airtable

In our example, changes to the data can be made in Airtable (by editing the table cells) or in the Gantt chart (by drag and drop). For synchronizing these parts, I follow a hybrid strategy that involves both server-side and client-side update operations.

1. Transfer changes from Airtable to the Gantt chart (server-side)

Gatsby offers webhooks to remotely trigger the server-side build process. It’s possible to configure Airtable to automatically trigger the build hook on certain events (such as creating or changing records), provided you have a pro membership there. (You can find more detailed information about the settings that are necessary for this purpose here).

2. Transfer changes from Airtable to the Gantt chart (client-side)

While the application is used in the browser, the Gantt chart should load updates from Airtable dynamically (for example, at a certain time interval). To make the process simple, we just want to re-download the complete lists of jobs and resources at the specified interval. For this, we’ll use the official Airtable API.

In the IndexPage component, we use React’s useState hook to set the lists with the jobs and resources as the component’s state. Then we apply the useEffect hook to set an interval at which the function loadDataFromAirtable should be called once the component has been initialized:

const IndexPage = (data) => { let j = => {...}); let r = => {...}); const [resources, setResources] = useState(r); const [jobs, setJobs] = useState(j); useEffect(() => { const interval = setInterval(() => { let jobsLoaded = (j) => { setJobs(j) }; let resourcesLoaded = (r) => { setResources(r) }; loadDataFromAirtable(jobsLoaded, resourcesLoaded); }, 60000); return () => clearInterval(interval); }, []); if(resources && jobs){ return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <GanttChart jobs={jobs} resources={resources}/> </main> ) }else{ return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <p>Missing data...</p> </main> ) }

For the implementation of the loadDataFromAirtable function, we take a look at the documentation of the Airtable API. The documentation is adapted to the selected base (in our case “Task Manager”). If you click on Jobs Table and List records on the left side, you’ll see the exact structure of a GET request to retrieve the data of all jobs in the “curl” area. This request can be implemented very easily in JavaScript using the “fetch” method.

So, to download the data of all jobs and resources, we execute two asynchronous GET requests to Airtable in sequence. I’ve masked the exact URLs because they contain my personal API key:

function loadDataFromAirtable(onJobsLoaded, onResourcesLoaded){ let j,r; let url_j= "XXXX"; let url_r= "YYYY"; fetch(url_j, {headers: {"Authorization": "ZZZZ"}}) .then(response => response.json()) .then(data => { j = => { let s = new Date(record.fields.start); s.setHours(0); let e = new Date(record.fields.end); e.setHours(0); return { airtable_id:, id:, start: s, end: e, resource: record.fields['id (from resource)'][0], resource_airtable_id: record.fields.resource[0] }; }); onJobsLoaded(j); }); fetch(url_r, {headers: {"Authorization": "ZZZZ"}}) .then(response => response.json()) .then(data => { r = => { return { id:, name: }; }); onResourcesLoaded(r); });

As a test, you can make some changes to the job data in your Airtable base. After the given interval time (here one minute) the Gantt chart should update automatically in your browser.

3. Transfer changes from the Gantt chart to the Airtable base (client-side)

Before the user can modify the Gantt chart, we must first make the jobs draggable. For this, update the ChartCell component as follows:

class ChartCell extends React.Component { constructor(props) { super(props); } render(){ let jobElements = => this.getJobElement(job)); let dragOver = (ev) => {ev.preventDefault()}; let drop = (ev) => { ev.preventDefault(); let job_id = ev.dataTransfer.getData("job"); this.props.onDropJob(job_id,, }; return ( <div style={{borderTop: 'none', borderRight: 'none', backgroundColor: ( || ? "whitesmoke" : "white" }} className="gantt-row-item" onDragOver={dragOver} onDrop={drop}> {jobElements} </div> ); } getJobElement(job){ let d = this.dayDiff(job.start, job.end); return ( <div style={{width: "calc("+(d*100)+"% + "+ d + "px)"}} className="job" id={} key={} draggable="true" onDragStart={this.dragStart}> </div> ); } dragStart(ev){ ev.dataTransfer.setData("job",;} dayDiff(d1, d2){ let diffTime = Math.abs(d2 - d1); let diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); return diffDays; }

Implementing drag and drop isn’t particularly complicated with JavaScript. You have to implement handlers for the events onDragStart (for the draggable elements), onDragOver and onDrop (for the drop targets), as shown in this tutorial.

We need to specify which handler function is called on the onDropJob event, which is triggered by the drop handler. In the initGanttRows function, update the following line:

cells.push(<ChartCell key={"gr"+(i++)} resource={resource} date={new Date(date)} jobs={cell_jobs} onDropJob={this.dropJob}/>);

In the GanttChart component, add the function dropJob:

dropJob(id, newResource, newDate){ let job = => == id ); let newJob = {}; newJob.resource = newResource; let d = this.dayDiff(job.start, job.end); let end = new Date(newDate); end.setDate(newDate.getDate()+d); newJob.start = newDate; newJob.end = end; this.props.onUpdateJob(id, newJob); };

The actual modification of the job list is done in the parent IndexPage component in index.js. The slice method is used to create a copy of the job list. The job that was moved using drag and drop is located in the list based on its ID and is given the new properties. After that, the state of the IndexPage component is updated by calling setJobs. Please note that, exactly now, a re-render of the Gantt chart component is triggered and now the job element appears at its new position:

const IndexPage = (data) => { ... let updateJob = (id, newJob) => { let new_jobs = jobs.slice(); let job = new_jobs.find(j => == id ); job.resource = newJob.resource; job.start = newJob.start; job.end = newJob.end; setJobs(new_jobs); updateJobToAirtable(job); } if(resources && jobs){ return ( <main> <title>Gantt Chart</title> <h1>Welcome to my Gatsby Gantt Chart</h1> <GanttChart jobs={jobs} resources={resources} onUpdateJob={updateJob}/> </main> ) }else{ ... }

In the last step, we have to implement the updateJobToAirtable function. Again, we follow the Airtable API documentation, this time in the section Update records:

function updateJobToAirtable(job){ let data = { records: [ { id: job.airtable_id, fields: { id:, start: formatDate(job.start), end: formatDate(job.end), resource: [ job.resource_airtable_id ] } } ]}; fetch("XXX", { method: "PATCH", headers: {"Authorization": "ZZZ", "Content-Type": "application/json"}, body: JSON.stringify(data) });

Now you can move jobs in the Gantt chart and watch how the table “Jobs” updates in real time in your Airtable base.

Final Thoughts

The simple task management application in this article shows that server-side rendering can also be used for applications with rich client-side interaction. The main advantage is the fast initial loading time, because the DOM is prepared on the server. Especially for applications with a very complex user interface (for example, dashboards for planning tasks), this can be crucial. The periodical fetching of new data on the client side usually doesn’t lead to major performance problems, because React uses a sophisticated algorithm to determine which changes to the DOM are actually necessary.

The Gatsby framework greatly simplifies the process of developing such hybrid applications by providing seamless support for server-side rendering as well as numerous plugins for importing data from external sources.

0 Comments Leave a reply

    Leave a comment

    Your comment(click button to send)


    This is a unique website which will require a more modern browser to work!

    Please upgrade today!

    Open chat