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

Filter
Tue, 21 AprWed, 22 AprThu, 23 AprFri, 24 AprSat, 25 AprSun, 26 AprMon, 27 AprTue, 28 AprWed, 29 AprThu, 30 AprFri, 1 MaySat, 2 MaySun, 3 May
Alice
  • Project Kickoff Project Kickoff | Project Kickoff | Alice | Completed
Bob
  • Development Sprint Development Sprint | Development Sprint | Bob | In progress
Carol

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.

1
Install the JAR files

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:

Shell
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.

2
Add the Maven dependency

Add the following dependency to your pom.xml:

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.

3
Enable component scanning

GanttChart is a Spring component that must be visible to your application context. Add the gantt-spring package to your @SpringBootApplication scan:

YourApplication.java
@SpringBootApplication(scanBasePackages = {
    "com.example.yourapp",
    "com.optomus.gantt.spring.components"
})
public class YourApplication {
    public static void main(String[] args) {
        SpringApplication.run(YourApplication.class, args);
    }
}
4
Build your Gantt bars

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).

YourController.java
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
    )
);
Row order: rows appear in the chart in the order they are first encountered in the bar list, unless you specify row names explicitly in Step 5.
5
Configure and build the chart in your controller

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.

YourController.java
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";
    }
}
Important: always call 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.
6
Add the chart fragment to your Thymeleaf template

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.

schedule.html
<!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>
If you only need a display-only chart, stop here. Steps 1–6 are all you need. Continue below to add the interactive filter pane with AJAX support.

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.

7
Inject GanttFilterPane and implement GanttFilterListener

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.

YourController.java
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);
    }
}
Note: 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.
8
Add filter and clear endpoints

Add full-page fallback endpoints for non-JavaScript environments, and AJAX endpoints that return JSON containing the updated chart and filter pane HTML fragments.

YourController.java — full-page fallbacks
@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";
}
YourController.java — AJAX endpoints
@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);
}
9
Add the filter helper methods

Add these private helper methods to your controller:

YourController.java — helper methods
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;
}
10
Add the filter pane fragment and AJAX script to your template

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.

schedule.html
<!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>
Context path: if your application is not deployed at the root (/), set var ctx = '/your-context-path' so AJAX requests resolve correctly.
Your chart is now fully interactive with AJAX filter support. Continue below for optional customisation.

Part 3 — Optional customisation

These steps are independent of each other — apply any combination that suits your use case.

11
Customise chart colours Optional

Override the default heading, border, and alternate row colours by calling the relevant setters before build(). All colours accept CSS hex strings via GanttColour.

YourController.java
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.

12
Customise row height and name column width Optional
YourController.java
ganttChart.setNameColumnWidth("180px");  // any valid CSS width
ganttChart.setRowHeightInPixels(40);
ganttChart.build();
13
Internationalise labels via a properties file Optional

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.

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:

messages.properties — overridable 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
Locale resolution follows standard Spring MessageSource behaviour. Add a LocaleChangeInterceptor bean if you need runtime locale switching via a URL parameter.
14
Add hyperlinks to bars Optional

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.

YourController.java
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.

!
NullPointerException at render time

Almost always caused by forgetting to call ganttChart.build() after setting parameters. Ensure build() is the last call before model.addAttribute().

!
AJAX calls return 404

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 = ''.

!
Filter pane does not reflect chart state after redirect

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.

!
CSS not loading

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

Try the live demo

Visit gantt.nz to explore the full interactive demo — add your own bars, adjust the filter pane, and see all features in action.

Get support

Questions or unexpected behaviour? Email support@gantt.nz. We aim to respond within 24 hours on New Zealand business days.