With a Gantt chart, you can visualize schedules and assign tasks. In this article, we will code a Gantt chart as a reusable Web component. We will focus on the architecture of the component, rendering the calendar with CSS Grid and managing the state of the draggable tasks with JavaScript Proxy Objects.
If you work with time data in your app, a graphical visualization as a calendar or Gantt chart is often very useful. At first glance, developing your own chart component seems quite complicated. Therefore, in this article, I will develop the foundation for a Gantt chart component whose appearance and functionality you can customize for any use case.
These are the basic features of the Gantt chart that I would like to implement:
- The user can choose between two views: year/month or month/day.
- The user can define the planning horizon by selecting a start date and an end date.
- The chart renders a given list of jobs that can be moved by drag and drop. The changes are reflected in the state of the objects.
- Below you can see the resulting Gantt chart in both views. In the monthly version,
Below you can see the resulting Gantt chart in both views. In the monthly version, I have included three jobs as an example.
Sample Files And Instructions For Running The Code
You can find the full code snippets of this article in the following files:
Since the code contains JavaScript modules, you can only run the example from an HTTP server and not from the local file system. For testing on your local PC, I’d recommend the module live-server, which you can install via npm.
Basic Structure Of The Web Component
I decided to implement the Gantt chart as a web component. This allows us to create a custom HTML element, in my case <gantt-chart></gantt-chart>
, which we can easily reuse anywhere on any HTML page.
You can find some basic information about developing web components in the MDN Web Docs. The following listing shows the structure of the component. It is inspired by the “counter” example from Alligator.io.
The component defines a template containing the HTML code needed to display the Gantt chart. For the complete CSS specifications, please refer to the sample files. The specific selection fields for year, month or date cannot be defined here yet, as they depend on the selected level of the view.
The selection elements are projected in by one of the two renderer classes instead. The same applies to the rendering of the actual Gantt chart into the element with the ID gantt-container
, which is also handled by the responsible renderer class.
The class VanillaGanttChart
now describes the behavior of our new HTML element. In the constructor, we first define our rough template as the shadow DOM of the element.
The component must be initialized with two arrays, jobs
, and resources
. The jobs
array contains the tasks that are displayed in the chart as movable green bars. The resources
array defines the individual rows in the chart where tasks can be
assigned. In the screenshots above, for example, we have 4 resources
labeled Task 1 to Task 4. The resources can therefore
represent the individual tasks, but also people, vehicles, and other
physical resources, allowing for a variety of use cases.
Currently, the YearMonthRenderer
is used as the default renderer. As soon as the user selects a different level, the renderer is changed in the changeLevel
method: First, the renderer-specific DOM elements and listeners are deleted from the Shadow DOM using the clear
method of the old renderer. Then the new renderer is initialized with
the existing jobs and resources and the rendering is started.
Before we get deeper into the rendering process, I would like to give you an overview of the connections between the different scripts:
- index.html is your web page where you can use the tag
<gantt-chart></gantt-chart>
- index.js is a script in which you initialize the instance of the web component that is associated with the Gantt chart used in index.html with the appropriate jobs and resources (of course you can also use multiple Gantt charts and thus multiple instances of the web component)
- The component
VanillaGanttChart
delegates rendering to the two renderer classesYearMonthRenderer
andDateTimeRenderer
.
Rendering Of The Gantt chart With JavaScript And CSS Grid
In the following, we discuss the rendering process using the YearMonthRenderer
as an example. Please note that I have used a so-called constructor function instead of the class
keyword to define the class. This allows me to distinguish between public properties (this.render
and this.clear
) and private variables (defined with var
).
The rendering of the chart is broken down into several sub-steps:
initSettings
Rendering of the controls which are used to define the planning horizon.initGantt
Rendering of the Gantt chart, basically in four steps:initFirstRow
(draws 1 row with month names)initSecondRow
(draws 1 row with days of the month)initGanttRows
(draws 1 row for each resource with grid cells for each day of the month)initJobs
(positions the draggable jobs in the chart)
Rendering The Grid
I recommend CSS Grid for drawing the diagram area because it makes it very easy to create multi-column layouts that adapt dynamically to the screen size.
In the first step, we have to determine the number of columns of the grid. In doing so, we refer to the first row of the chart which (in the case of the YearMonthRenderer
) represents the individual months.
Consequently, we need:
- one column for the names of the resources, e.g. with a fixed width of 100px.
- one column for each month, of the same size and using the full space available.
This can be achieved with the setting 100px repeat(${n_months}, 1fr)
for the property gridTemplateColumns
of the chart container.
This is the initial part of the initGantt
method:
var container = shadowRoot.querySelector("#gantt-container");
container.innerHTML = "";
var first_month = new Date(getYearFrom(), getMonthFrom(), 1);
var last_month = new Date(getYearTo(), getMonthTo(), 1);
//monthDiff is defined as a helper function at the end of the file
var n_months = monthDiff(first_month, last_month)+1;
container.style.gridTemplateColumns = `100px repeat(${n_months},1fr)`;
After we have defined the outer columns, we can start filling the grid. Let’s stay with the example from the picture above. In the first row, I insert 3 div
s with the classes gantt-row-resource
and gantt-row-period
. You can find them in the following snippet from the DOM inspector.
In the second row, I use the same three div
s to keep the vertical alignment. However, the month div
s get child elements for the individual days of the month.
For the child elements to be arranged horizontally as well, we need the setting display: grid
for the class gantt-row-period
.
In addition, we do not know exactly how many columns are required for
the individual months (28, 30, or 31). Therefore, I use the setting grid-auto-columns
. With the value minmax(20px, 1fr);
I can ensure that a minimum width of 20px is maintained and that otherwise the available space is fully utilized:
The remaining rows are generated according to the second row, however as empty cells.
Here is the JavaScript code for generating the individual grid cells of the first row. The methods initSecondRow
and initGanttRows
have a similar structure.
Rendering The Jobs
Now each job
has to be drawn into the diagram at the correct position. For this I make use of the HTML data attributes: every grid cell in the main chart area is associated with the two attributes data-resource
and data-date
indicating the position on the horizontal and vertical axis of the chart (see function initGanttRows
in the files YearMonthRenderer.js
and DateTimeRenderer.js
).
Let’s now see what this means for the function initJobs
. With the help of the function querySelector
, it is now quite easy to find the grid cell into which a job should be placed.
The next challenge is to determine the correct width for a job
element. Depending on the selected view, each grid cell represents a unit of one day (level month/day
) or one hour (level day/time
). Since each job is the child element of a cell, the job
duration of 1 unit (day or hour) corresponds to a width of 1*100%
, the duration of 2 units corresponds to a width of 2*100%
, and so on. This makes it possible to use the CSS calc
function to dynamically set the width of a job
element, as shown in the following listing.
In order to make a job
draggable, there are three steps required:
- Set the property
draggable
of the job element totrue
(see listing above). - Define an event handler for the event
ondragstart
of the job element (see listing above). - Define an event handler for the event
ondrop
for the grid cells of the Gantt chart, which are the possible drop targets of the job element (see functioninitGanttRows
in the fileYearMonthRenderer.js
).
The event handler for the event ondrop
is defined as follows:
All changes to the job data made by drag and drop are thus reflected in the list jobs
of the Gantt chart component.
Integrating The Gantt Chart Component In Your Application
You can use the tag <gantt-chart></gantt-chart>
anywhere in the HTML files of your application (in my case in the file index.html
) under the following conditions:
- The script
VanillaGanttChart.js
must be integrated as a module so that the tag is interpreted correctly. - You need a separate script in which the Gantt chart is initialized with
jobs
andresources
(in my case the fileindex.js
).
For example, in my case the file index.js
looks as follows:
However, there is still one requirement open: when the user makes changes by dragging jobs in the Gantt chart, the respective changes in the property values of the jobs should be reflected in the list outside the component.
We can achieve this with the use of JavaScript Proxy Objects: Each job
is nested in a proxy object, which we provide with a so-called validator. It becomes active as soon as a property of the object is changed (function set
of the validator) or retrieved (function get
of the validator). In the set function of the validator, we can store
code that is executed whenever the start time or the resource of a task
is changed.
The following listing shows a different version of the file index.js
. Now a list of proxy objects is assigned to the Gantt chart component instead of the original jobs. In the validator set
I use a simple console output to show that I have been notified of a property change.
Outlook
The Gantt chart is an example that shows how you can use the technologies of Web Components, CSS Grid, and JavaScript Proxy to develop a custom HTML element with a somewhat more complex graphical interface. You are welcome to develop the project further and/or use it in your own projects together with other JavaScript frameworks.
No comments:
Post a Comment