Quick-Start Guide
OptoGantt — Spring Boot + Thymeleaf
Follow these steps to add an interactive Gantt chart to your Spring Boot application. By the end you will have the chart shown below running in your own project, ready to connect to your own data.
Live example — what you will build
This chart is fully interactive — use the filter pane to adjust the date range and period. The stripe on the bar crossing today's date is the built-in "now" indicator.
Part 1 — Display-only Gantt chart
Start here. Steps 1–6 give you a working Gantt chart with no user interaction. If you only need to display data, you can stop after Step 6.
Download optogantt-1.0.0.zip from Gumroad and unzip it.
Install all five JARs to your local Maven repository by running the
following command once for each JAR, substituting the filename:
mvn install:install-file \ -Dfile=gantt-core-1.0.0.jar \ -DgroupId=com.optomus.gantt \ -DartifactId=gantt-core \ -Dversion=1.0.0 \ -Dpackaging=jar
Repeat for gantt-spring-1.0.0.jar (artifactId: gantt-spring).
The remaining three JARs are for other frameworks and are not needed for this guide.
Add the following dependency to your pom.xml:
<dependency>
<groupId>com.optomus.gantt</groupId>
<artifactId>gantt-spring</artifactId>
<version>1.0.0</version>
</dependency>
gantt-core is a transitive dependency of gantt-spring
and is resolved automatically. You do not need to declare it separately.
GanttChart is a Spring component that must be visible to
your application context. Add the gantt-spring package to
your @SpringBootApplication scan:
@SpringBootApplication(scanBasePackages = {
"com.example.yourapp",
"com.optomus.gantt.spring.components"
})
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
Create your bar data using GanttBar. Each bar requires a
task name, row name, start datetime, end datetime, text colour, bar
background colour, hover text, and an optional hyperlink
(null if not needed).
import com.optomus.gantt.core.model.GanttBar;
import com.optomus.gantt.core.model.GanttColour;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
// Build bars relative to today
LocalDate today = LocalDate.now();
List<GanttBar> bars = List.of(
new GanttBar(
"Project Kickoff", // task name
"Alice", // row name
LocalDateTime.of(today.minusDays(5), LocalTime.of(9, 0)),
LocalDateTime.of(today.minusDays(1), LocalTime.of(17, 0)),
GanttColour.WHITE, // text colour
new GanttColour("#2a4a7a"), // bar colour
"Project Kickoff | Alice | Completed", // hover text
null // hyperlink
),
new GanttBar(
"Development Sprint",
"Bob",
LocalDateTime.of(today.minusDays(3), LocalTime.of(9, 0)),
LocalDateTime.of(today.plusDays(4), LocalTime.of(17, 0)),
GanttColour.WHITE,
new GanttColour("#1a6b5a"),
"Development Sprint | Bob | In progress",
null
),
new GanttBar(
"Testing & QA",
"Carol",
LocalDateTime.of(today.plusDays(2), LocalTime.of(9, 0)),
LocalDateTime.of(today.plusDays(6), LocalTime.of(17, 0)),
GanttColour.WHITE,
new GanttColour("#5b3a6e"),
"Testing & QA | Carol | Upcoming",
null
)
);
Inject the request-scoped GanttChart bean, supply your
data, and call build() before adding it to the model.
GanttChart is request-scoped — a fresh instance is created
for each HTTP request automatically.
import com.optomus.gantt.spring.components.GanttChart;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
@Controller
public class YourController {
@Autowired
private GanttChart ganttChart;
@GetMapping("/schedule")
public String schedule(Model model) {
LocalDateTime chartStart = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
ganttChart.setGanttBars(bars); // your bar list from Step 4
ganttChart.setRowNames(List.of("Alice", "Bob", "Carol"));
ganttChart.setStartDate(chartStart);
ganttChart.setColumnDuration(Duration.ofDays(1)); // one column per day
ganttChart.setNumColumns(14); // 14-day window
ganttChart.build();
model.addAttribute("ganttChart", ganttChart);
return "schedule";
}
}
ganttChart.build()
after setting all parameters and before adding the bean to the model.
Forgetting this call will result in a NullPointerException at render time.
The gantt-spring JAR contains the Thymeleaf fragment template
and CSS. Include the fragment in your page template using
th:replace. The CSS is served automatically by Spring Boot
from the JAR — no copying is needed.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<!-- GanttChart.css and GanttFilterPane.css are served automatically -->
<link rel="stylesheet" th:href="@{/css/GanttChart.css}"/>
<link rel="stylesheet" th:href="@{/css/GanttFilterPane.css}"/>
</head>
<body>
<div th:replace="~{gantt/GanttChart :: ganttChart}"></div>
</body>
</html>
Part 2 — Interactive filter pane with AJAX
Add the filter pane so visitors can adjust the chart's date range and period without a full page reload.
GanttFilterPane is session-scoped — one instance persists
across requests for each visitor. Your controller must implement
GanttFilterListener to receive resolved filter parameters
after the pane validates and processes a submission.
import com.optomus.gantt.core.GanttFilterListener;
import com.optomus.gantt.core.model.GanttColumnDuration;
import com.optomus.gantt.spring.components.GanttFilterPane;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Controller
public class YourController {
@Autowired private GanttChart ganttChart;
@Autowired private GanttFilterPane ganttFilterPane;
@Autowired private SpringTemplateEngine templateEngine;
// Store resolved filter state in session-scoped fields
private LocalDateTime chartStart = defaultStart();
private Duration columnDuration = Duration.ofDays(1);
private int numColumns = 14;
private static LocalDateTime defaultStart() {
return LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
}
}
chartStart, columnDuration
and numColumns need to be stored somewhere that survives
across requests — either in a session-scoped service bean (recommended
for production) or as fields on a session-scoped controller.
For simplicity this guide uses a session-scoped controller.
Annotate your controller with @SessionAttributes or
use a dedicated @SessionScope service bean.
Add full-page fallback endpoints for non-JavaScript environments, and AJAX endpoints that return JSON containing the updated chart and filter pane HTML fragments.
@GetMapping("/schedule")
public String schedule(Model model) {
syncFilterPane();
buildAndAddToModel(model);
return "schedule";
}
@PostMapping("/schedule/filter")
public String filter(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) GanttColumnDuration columnDuration) {
processFilter(startDate, columnDuration);
return "redirect:/schedule";
}
@GetMapping("/schedule/clear")
public String clear() {
chartStart = defaultStart();
columnDuration = Duration.ofDays(1);
numColumns = 14;
syncFilterPane();
return "redirect:/schedule";
}
@PostMapping(value = "/schedule/filter/ajax",
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Map<String, Object> filterAjax(
@RequestParam(required = false) String startDate,
@RequestParam(required = false) GanttColumnDuration columnDuration,
HttpServletRequest request, HttpServletResponse response) {
processFilter(startDate, columnDuration);
return buildAjaxResponse(request, response);
}
@PostMapping(value = "/schedule/clear/ajax",
produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Map<String, Object> clearAjax(
HttpServletRequest request, HttpServletResponse response) {
chartStart = defaultStart();
columnDuration = Duration.ofDays(1);
numColumns = 14;
syncFilterPane();
return buildAjaxResponse(request, response);
}
Add these private helper methods to your controller:
private void syncFilterPane() {
if (ganttFilterPane.getStartDate() == null) {
ganttFilterPane.setStartDate(chartStart);
ganttFilterPane.setColumnDuration(GanttColumnDuration.FOURTEEN_DAYS);
}
}
private void processFilter(String startDate,
GanttColumnDuration period) {
LocalDateTime parsedStart = null;
if (startDate != null && !startDate.isBlank()) {
try { parsedStart = LocalDateTime.parse(startDate); }
catch (Exception ignored) {}
}
ganttFilterPane.setFilterListener(new GanttFilterListener() {
@Override
public void onFilter(LocalDateTime s, Duration d, Integer n) {
chartStart = s;
columnDuration = d;
numColumns = n;
ganttFilterPane.setColumnDuration(period);
}
@Override
public void onClearFilter() { chartStart = defaultStart(); }
});
ganttFilterPane.setStartDate(parsedStart);
ganttFilterPane.setColumnDuration(period);
ganttFilterPane.applyFilter();
}
private void buildAndAddToModel(Model model) {
ganttChart.setGanttBars(buildBars()); // your bar list
ganttChart.setRowNames(List.of("Alice", "Bob", "Carol"));
ganttChart.setStartDate(chartStart);
ganttChart.setColumnDuration(columnDuration);
ganttChart.setNumColumns(numColumns);
ganttChart.build();
model.addAttribute("ganttChart", ganttChart);
model.addAttribute("ganttFilterPane", ganttFilterPane);
}
private Map<String, Object> buildAjaxResponse(
HttpServletRequest request, HttpServletResponse response) {
syncFilterPane();
ganttChart.setGanttBars(buildBars());
ganttChart.setRowNames(List.of("Alice", "Bob", "Carol"));
ganttChart.setStartDate(chartStart);
ganttChart.setColumnDuration(columnDuration);
ganttChart.setNumColumns(numColumns);
ganttChart.build();
JakartaServletWebApplication webApp =
JakartaServletWebApplication
.buildApplication(request.getServletContext());
WebContext ctx = new WebContext(
webApp.buildExchange(request, response),
request.getLocale()
);
ctx.setVariable("ganttChart", ganttChart);
ctx.setVariable("ganttFilterPane", ganttFilterPane);
Map<String, Object> resp = new HashMap<>();
resp.put("chartHtml",
templateEngine.process("gantt/GanttChart", ctx));
resp.put("filterHtml",
templateEngine.process("gantt/GanttFilterPane", ctx));
return resp;
}
Add the filter pane fragment above the chart, wrap both in AJAX target
divs, and add the JavaScript that intercepts form submissions
and updates the DOM in place.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<link rel="stylesheet" th:href="@{/css/GanttChart.css}"/>
<link rel="stylesheet" th:href="@{/css/GanttFilterPane.css}"/>
</head>
<body>
<div id="ganttFilterZone">
<div th:replace="~{gantt/GanttFilterPane :: ganttFilterPane}"></div>
</div>
<div id="ganttChartZone">
<div th:replace="~{gantt/GanttChart :: ganttChart}"></div>
</div>
<script>
(function () {
var ctx = ''; // set to your context path if not deploying at root
function postAjax(url, formData, callback) {
fetch(url, { method: 'POST', body: formData })
.then(function (r) { return r.json(); })
.then(callback);
}
function updateDom(data) {
var parser = new DOMParser();
if (data.chartHtml) {
var chartZone = document.getElementById('ganttChartZone');
if (chartZone) {
var newChart = parser
.parseFromString(data.chartHtml, 'text/html')
.getElementById('ganttChartZone');
if (newChart) chartZone.replaceWith(newChart);
}
}
if (data.filterHtml) {
var filterZone = document.getElementById('ganttFilterZone');
if (filterZone) {
var newFilter = parser
.parseFromString(data.filterHtml, 'text/html')
.getElementById('ganttFilterZone');
if (newFilter) filterZone.replaceWith(newFilter);
}
}
}
// Apply filter — intercept form submit
document.addEventListener('submit', function (e) {
var form = e.target.closest('#ganttFilterZone form');
if (form) {
e.preventDefault();
postAjax(ctx + '/schedule/filter/ajax',
new FormData(form), updateDom);
}
});
// Clear filter — intercept link click
document.addEventListener('click', function (e) {
var link = e.target.closest('#ganttFilterZone a');
if (link) {
e.preventDefault();
postAjax(ctx + '/schedule/clear/ajax',
new FormData(), updateDom);
}
});
}());
</script>
</body>
</html>
/), set var ctx = '/your-context-path'
so AJAX requests resolve correctly.
Part 3 — Optional customisation
These steps are independent of each other — apply any combination that suits your use case.
Override the default heading, border, and alternate row colours by
calling the relevant setters before build(). All colours
accept CSS hex strings via GanttColour.
ganttChart.setHeadingTextColour(new GanttColour("#ffffff"));
ganttChart.setHeadingBackgroundColour(new GanttColour("#3d2b1f"));
ganttChart.setBorderColour(new GanttColour("#6b4c35"));
ganttChart.setAlternateRowsBackgroundColour(new GanttColour("#faf7f3"));
ganttChart.build();
If only setHeadingBackgroundColour() is called, border
and alternate row colours are derived automatically as darker and
lighter shades respectively.
ganttChart.setNameColumnWidth("180px"); // any valid CSS width
ganttChart.setRowHeightInPixels(40);
ganttChart.build();
OptoGantt uses Spring's MessageSource for all UI labels.
The default labels are defined in
GanttMessages.properties inside the gantt-core
JAR. To override them, add your own properties file and wire it in
application.properties.
# Point Spring's MessageSource at your own properties file. # The gantt-core defaults are in com/optomus/gantt/core/GanttMessages spring.messages.basename=messages,com/optomus/gantt/core/GanttMessages spring.messages.encoding=UTF-8
Then create src/main/resources/messages.properties
(and locale variants such as messages_fr.properties)
and override any of the following keys:
# Filter pane labels label.startDate=Start label.columnDuration=Period button.apply=Apply Filter link.clear=Clear filter # Period dropdown options duration.FOURTEEN_DAYS=14 Days duration.ONE_MONTH=1 Month duration.THREE_MONTHS=3 Months duration.SIX_MONTHS=6 Months duration.ONE_YEAR=1 Year
MessageSource behaviour. Add a
LocaleChangeInterceptor bean if you need runtime locale
switching via a URL parameter.
Pass a URL as the final constructor argument to GanttBar.
Clicking the bar will navigate to that URL. Pass null
if no link is needed.
new GanttBar(
"Sprint Review",
"Alice",
start, end,
GanttColour.WHITE,
new GanttColour("#2a4a7a"),
"Sprint Review | Alice | Click for details",
"https://your-project-tracker.example.com/sprint/42"
)
Troubleshooting
Common issues and their solutions.
Almost always caused by forgetting to call ganttChart.build()
after setting parameters. Ensure build() is the last call
before model.addAttribute().
Check that var ctx in your JavaScript matches your
application's context path. If deployed at /myapp, set
var ctx = '/myapp'. If deployed at root, leave it as
var ctx = ''.
Ensure syncFilterPane() is called at the start of your
GET handler. The filter pane is session-scoped and its
display values must be explicitly set before each render.
Confirm that gantt-spring-1.0.0.jar is on the classpath
and that your template includes both
th:href="@{/css/GanttChart.css}" and
th:href="@{/css/GanttFilterPane.css}".
Spring Boot serves these automatically from the JAR's
META-INF/resources/css/ directory.
Next steps
Visit gantt.nz to explore the full interactive demo — add your own bars, adjust the filter pane, and see all features in action.
Questions or unexpected behaviour? Email support@gantt.nz. We aim to respond within 24 hours on New Zealand business days.