Gantt Chart
ULTIMATE
The Gantt extension adds a timeline lane to every row of a worksheet. The task name, start date, end date, progress, colour and predecessors are read from the columns you choose, and the bars are drawn next to the data, in the same table, so rows and bars always line up, through scrolling, sorting and filtering.
Editing works in both directions. Change a date in a cell and the bar follows. Drag a bar to move it, drag its edge to resize it, or drag the dot at its end onto another task to create a dependency, and the extension writes the new values back to the cells through the normal spreadsheet pipeline, so undo, redo, events and the collaboration server all see ordinary cell changes.
Key Features
- Bars aligned with the grid rows, inside the same table
- Drag to move, drag the edges to resize, with snapping in days
- Finish to start dependencies drawn as arrows, created and removed with the mouse
- Progress fill and a colour per task, read from the cells
- Week and day scale, today marker, resizable and zoomable timeline
- Every edit goes through
setValue: undo, redo and events work as usual- Self-contained build, with no runtime dependencies
Documentation
Settings
The extension is configured once, before it is registered with setExtensions.
| Property | Description |
|---|---|
| columns: object | Which column holds each task field. See the columns table below. Default: { label: 0, start: 1, end: 2, progress: 3, color: 4 } |
| width: number | Width of the timeline column, in pixels. Default: 420 |
| daywidth: number | Fixed width of one day, in pixels. The timeline then shows width / daywidth days. Use 0 to fit all tasks. Default: 16 |
| editable: boolean | Drag bars to move and resize them, and draw dependencies. Default: true |
| readonly: boolean | View only, regardless of editable. Use it to mirror a read-only worksheet. Default: false |
| snap: number | Drag snapping, in days. Default: 1 |
| resizable: boolean | A grip on the right border of the timeline to change its width. Default: true |
| zoom: boolean | Shift + mouse wheel over the timeline changes the day width. Default: true |
Columns
Each entry of columns is a zero-based column index.
| Field | Description |
|---|---|
| label | The task name, shown next to the bar |
| start | The start date. An ISO string such as 2026-10-05, or a calendar column |
| end | The end date. A task without an end date is one day long |
| progress? | Completion from 0 to 100, drawn as a lighter fill inside the bar |
| color? | A CSS colour for the bar, for example from a color column |
| predecessors? | Comma-separated row numbers of the tasks this one depends on, counted from 1. Enables the dependency arrows |
Rows without a valid start date have an empty lane. Dates stored as spreadsheet serial numbers, which is how typed dates are kept, are read as well.
Interaction
| Gesture | Effect |
|---|---|
| Drag a bar | Moves the task. The start and end cells are updated on drop |
| Drag the left or right edge of a bar | Changes the start or the end date |
| Drag the dot at the end of a bar to another row | Adds that row to the predecessors of the target task |
| Click an arrow | Removes that dependency |
| Drag the timeline header | Pans the visible dates |
| Shift + mouse wheel over the timeline | Zooms the day width, keeping the date under the cursor in place |
| Drag the right border of the timeline | Resizes the timeline column, keeping the day width |
Every change made on the chart is a cell change, so onchange, onafterchanges and the history behave exactly as if the user had typed the new value.
Installation
Choose one of the following options:
Using NPM
$ npm install @jspreadsheet/gantt
Using a CDN
<script src="https://cdn.jsdelivr.net/npm/@jspreadsheet/gantt/dist/index.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jspreadsheet/gantt/dist/style.css" type="text/css" />
The file is self-contained and has no runtime dependencies. It runs next to any other Jspreadsheet extension.
Code examples
A project plan with dependencies
Each row is a task. The last column lists the rows it depends on, which draws the arrows.
<html>
<script src="https://jsuites.net/v6/jsuites.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jspreadsheet@13/dist/index.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jspreadsheet@13/dist/jspreadsheet.min.css" type="text/css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" type="text/css" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons" />
<script src="https://cdn.jsdelivr.net/npm/@jspreadsheet/gantt/dist/index.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jspreadsheet/gantt/dist/style.css" type="text/css" />
<div id="spreadsheet"></div>
<script>
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// This license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate an extended demo license.
jspreadsheet.setLicense('NGIzZWQwZTBiMjcwZmMwMDZhZjM1NGUyNDcxYWJlMzZlMmExNmRhMGUyY2JlMDlkYWYyMDgwYzUyZjM0YTNjZjM4MmFiYjFjMjQ5NTRiMDA3YjU0YWZmZTVkYzI0NTA4MjgxZTI4Nzg5ODgyZTQzYzcyYzNiYmUzOTY3NjdkMDMsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpreE1EYzVNelk0TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Which columns feed the chart
gantt({
columns: { label: 0, start: 1, end: 2, progress: 3, color: 4, predecessors: 5 },
width: 480,
daywidth: 16,
});
// Spreadsheet plugin
jspreadsheet.setExtensions({ gantt });
// Create a spreadsheet
jspreadsheet(document.getElementById('spreadsheet'), {
tableOverflow: true,
tableWidth: '100%',
tableHeight: 400,
worksheets: [{
worksheetName: 'Project',
columns: [
{ title: 'Task', width: 160 },
{ title: 'Start', type: 'calendar', width: 110 },
{ title: 'End', type: 'calendar', width: 110 },
{ title: '%', type: 'number', width: 50 },
{ title: 'Color', type: 'color', render: 'square', width: 80 },
{ title: 'After', width: 70 },
],
data: [
['Discovery', '2026-10-05', '2026-10-09', 100, '#2563eb', ''],
['Requirements', '2026-10-12', '2026-10-16', 100, '#2563eb', '1'],
['Design', '2026-10-15', '2026-10-23', 60, '#7c3aed', '2'],
['API', '2026-10-26', '2026-11-06', 30, '#0d9488', '3'],
['Front end', '2026-10-28', '2026-11-11', 20, '#0d9488', '3'],
['Integration', '2026-11-12', '2026-11-18', 0, '#d97706', '4,5'],
['QA', '2026-11-19', '2026-11-26', 0, '#d97706', '6'],
['Launch', '2026-11-30', '2026-11-30', 0, '#dc2626', '7'],
],
}],
});
</script>
</html>
import React, { useRef } from "react";
import { Spreadsheet, Worksheet } from "@jspreadsheet/react";
import jspreadsheet from "jspreadsheet";
import gantt from "@jspreadsheet/gantt";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
import "@jspreadsheet/gantt/dist/style.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// This license is valid for one day, after which the spreadsheet will become read-only.
// For a longer trial period, you can create a free account and generate a demo license with a longer validity period.
jspreadsheet.setLicense('NGIzZWQwZTBiMjcwZmMwMDZhZjM1NGUyNDcxYWJlMzZlMmExNmRhMGUyY2JlMDlkYWYyMDgwYzUyZjM0YTNjZjM4MmFiYjFjMjQ5NTRiMDA3YjU0YWZmZTVkYzI0NTA4MjgxZTI4Nzg5ODgyZTQzYzcyYzNiYmUzOTY3NjdkMDMsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpreE1EYzVNelk0TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Which columns feed the chart
gantt({
columns: { label: 0, start: 1, end: 2, progress: 3, color: 4, predecessors: 5 },
width: 480,
});
// JSS data grid plugin
jspreadsheet.setExtensions({ gantt });
const columns = [
{ title: 'Task', width: 160 },
{ title: 'Start', type: 'calendar', width: 110 },
{ title: 'End', type: 'calendar', width: 110 },
{ title: '%', type: 'number', width: 50 },
{ title: 'Color', type: 'color', render: 'square', width: 80 },
{ title: 'After', width: 70 },
];
const data = [
['Discovery', '2026-10-05', '2026-10-09', 100, '#2563eb', ''],
['Requirements', '2026-10-12', '2026-10-16', 100, '#2563eb', '1'],
['Design', '2026-10-15', '2026-10-23', 60, '#7c3aed', '2'],
['API', '2026-10-26', '2026-11-06', 30, '#0d9488', '3'],
['Front end', '2026-10-28', '2026-11-11', 20, '#0d9488', '3'],
['Integration', '2026-11-12', '2026-11-18', 0, '#d97706', '4,5'],
];
export default function App() {
// Spreadsheet worksheet array
const spreadsheet = useRef();
// Rendering component
return (
<Spreadsheet ref={spreadsheet} tableOverflow tableWidth="100%" tableHeight={400}>
<Worksheet worksheetName="Project" columns={columns} data={data} />
</Spreadsheet>
);
}
<template>
<Spreadsheet ref="spreadsheet" :license="license" :extensions="extensions" :tableOverflow="true" tableWidth="100%" :tableHeight="400">
<Worksheet worksheetName="Project" :columns="columns" :data="data" />
</Spreadsheet>
</template>
<script>
import { Spreadsheet, Worksheet, jspreadsheet } from "@jspreadsheet/vue";
import gantt from "@jspreadsheet/gantt";
import "jsuites/dist/jsuites.css";
import "jspreadsheet/dist/jspreadsheet.css";
import "@jspreadsheet/gantt/dist/style.css";
// Define the data grid license
const license = 'NGIzZWQwZTBiMjcwZmMwMDZhZjM1NGUyNDcxYWJlMzZlMmExNmRhMGUyY2JlMDlkYWYyMDgwYzUyZjM0YTNjZjM4MmFiYjFjMjQ5NTRiMDA3YjU0YWZmZTVkYzI0NTA4MjgxZTI4Nzg5ODgyZTQzYzcyYzNiYmUzOTY3NjdkMDMsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpreE1EYzVNelk0TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=';
// Which columns feed the chart
gantt({
columns: { label: 0, start: 1, end: 2, progress: 3, color: 4, predecessors: 5 },
width: 480,
});
// Define the data grid extensions
const extensions = { gantt };
export default {
components: {
Spreadsheet,
Worksheet,
},
data() {
return {
license,
extensions,
columns: [
{ title: 'Task', width: 160 },
{ title: 'Start', type: 'calendar', width: 110 },
{ title: 'End', type: 'calendar', width: 110 },
{ title: '%', type: 'number', width: 50 },
{ title: 'Color', type: 'color', render: 'square', width: 80 },
{ title: 'After', width: 70 },
],
data: [
['Discovery', '2026-10-05', '2026-10-09', 100, '#2563eb', ''],
['Requirements', '2026-10-12', '2026-10-16', 100, '#2563eb', '1'],
['Design', '2026-10-15', '2026-10-23', 60, '#7c3aed', '2'],
['API', '2026-10-26', '2026-11-06', 30, '#0d9488', '3'],
['Front end', '2026-10-28', '2026-11-11', 20, '#0d9488', '3'],
['Integration', '2026-11-12', '2026-11-18', 0, '#d97706', '4,5'],
],
};
}
}
</script>
import { Component, ViewChild, ElementRef } from "@angular/core";
import jspreadsheet from "jspreadsheet";
import gantt from "@jspreadsheet/gantt";
import "@jspreadsheet/gantt/dist/style.css";
// You can use the following license for quick testing on localhost, StackBlitz, or CodeSandbox.
// This license is valid for one day, after which the spreadsheet will become read-only.
jspreadsheet.setLicense('NGIzZWQwZTBiMjcwZmMwMDZhZjM1NGUyNDcxYWJlMzZlMmExNmRhMGUyY2JlMDlkYWYyMDgwYzUyZjM0YTNjZjM4MmFiYjFjMjQ5NTRiMDA3YjU0YWZmZTVkYzI0NTA4MjgxZTI4Nzg5ODgyZTQzYzcyYzNiYmUzOTY3NjdkMDMsZXlKamJHbGxiblJKWkNJNklpSXNJbTVoYldVaU9pSktjM0J5WldGa2MyaGxaWFFpTENKa1lYUmxJam94TnpreE1EYzVNelk0TENKa2IyMWhhVzRpT2xzaWFuTndjbVZoWkhOb1pXVjBMbU52YlNJc0ltTnZaR1Z6WVc1a1ltOTRMbWx2SWl3aWFuTm9aV3hzTG01bGRDSXNJbU56WWk1aGNIQWlMQ0p6ZEdGamEySnNhWFI2TG1sdklpd2lkMlZpWTI5dWRHRnBibVZ5TG1sdklpd2liRzlqWVd4b2IzTjBJbDBzSW5Cc1lXNGlPaUl6TkNJc0luTmpiM0JsSWpwYkluWTNJaXdpZGpnaUxDSjJPU0lzSW5ZeE1DSXNJbll4TVNJc0luWXhNaUlzSW5ZeE15SXNJbU5vWVhKMGN5SXNJbVp2Y20xeklpd2labTl5YlhWc1lTSXNJbkJoY25ObGNpSXNJbkpsYm1SbGNpSXNJbU52YlcxbGJuUnpJaXdpYVcxd2IzSjBaWElpTENKaVlYSWlMQ0oyWVd4cFpHRjBhVzl1Y3lJc0luTmxZWEpqYUNJc0luQnlhVzUwSWl3aWMyaGxaWFJ6SWl3aVkyeHBaVzUwSWl3aWMyVnlkbVZ5SWl3aWMyaGhjR1Z6SWl3aVptOXliV0YwSWl3aWNHbDJiM1FpWFN3aVpHVnRieUk2ZEhKMVpYMD0=');
// Which columns feed the chart
gantt({
columns: { label: 0, start: 1, end: 2, progress: 3, color: 4, predecessors: 5 },
width: 480,
});
// Extend
jspreadsheet.setExtensions({ gantt });
@Component({
standalone: true,
selector: "app-root",
template: `<div #spreadsheet></div>`
})
export class AppComponent {
@ViewChild("spreadsheet") spreadsheet: ElementRef;
// Worksheets
worksheets: jspreadsheet.worksheetInstance[];
// Create a new data grid
ngAfterViewInit() {
// Create a spreadsheet
this.worksheets = jspreadsheet(this.spreadsheet.nativeElement, {
tableOverflow: true,
tableWidth: '100%',
tableHeight: 400,
worksheets: [{
worksheetName: 'Project',
columns: [
{ title: 'Task', width: 160 },
{ title: 'Start', type: 'calendar', width: 110 },
{ title: 'End', type: 'calendar', width: 110 },
{ title: '%', type: 'number', width: 50 },
{ title: 'Color', type: 'color', render: 'square', width: 80 },
{ title: 'After', width: 70 },
],
data: [
['Discovery', '2026-10-05', '2026-10-09', 100, '#2563eb', ''],
['Requirements', '2026-10-12', '2026-10-16', 100, '#2563eb', '1'],
['Design', '2026-10-15', '2026-10-23', 60, '#7c3aed', '2'],
['API', '2026-10-26', '2026-11-06', 30, '#0d9488', '3'],
['Front end', '2026-10-28', '2026-11-11', 20, '#0d9488', '3'],
['Integration', '2026-11-12', '2026-11-18', 0, '#d97706', '4,5'],
],
}],
});
}
}
A read-only timeline
Use readonly to show the chart without editing, for example on a shared view, while the cells stay editable or not according to the worksheet.
gantt({
columns: { label: 0, start: 1, end: 2 },
readonly: true,
daywidth: 0, // fit every task in the timeline
});