Quick-Start Guide
OptoGantt — Jakarta Faces / JSF
Follow these steps to add an interactive Gantt chart to your Jakarta Faces 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 both JARs to your local Maven repository by running the
following command once for each, 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-jsf-1.0.0.jar (artifactId: gantt-jsf).
The remaining 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-jsf</artifactId>
<version>1.0.0</version>
</dependency>
gantt-core is a transitive dependency of gantt-jsf
and is resolved automatically. You do not need to declare it separately.
The gantt-jsf JAR packages GanttChart.css and
GanttFilterPane.css as JSF library resources under the
gantt library name. Add them to your page's
<h:head> using h:outputStylesheet:
<h:head>
<h:outputStylesheet library="gantt" name="GanttChart.css"/>
<h:outputStylesheet library="gantt" name="GanttFilterPane.css"/>
</h:head>
META-INF/resources/gantt/ directory
inside the JAR. The library="gantt" attribute maps to that
directory name.
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 CDI bean, supply your
data, and call build() in your @PostConstruct
method. GanttChart is request-scoped — a fresh instance is
created for each HTTP request automatically.
import com.optomus.gantt.jsf.components.GanttChart;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named
@RequestScoped
public class YourBackingBean {
@Inject
private GanttChart ganttChart;
@PostConstruct
public void init() {
LocalDateTime chartStart = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
ganttChart.setGanttBars(buildBars()); // 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();
}
}
ganttChart.build()
after setting all parameters. Forgetting this call will result in a
NullPointerException at render time.
The gantt-jsf JAR contains the Facelets template.
Include it in your page using ui:include. The template
references the ganttChart CDI bean by name via EL
expressions — no additional model wiring is needed.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<h:outputStylesheet library="gantt" name="GanttChart.css"/>
<h:outputStylesheet library="gantt" name="GanttFilterPane.css"/>
</h:head>
<h:body>
<div id="ganttChartZone">
<ui:include src="/WEB-INF/gantt/GanttChart.xhtml"/>
</div>
</h:body>
</html>
Part 2 — Interactive filter pane with AJAX
Add the filter pane so visitors can adjust the chart's date range and period.
The filter pane uses JSF's built-in f:ajax for partial page
updates — only the chart and filter zones are re-rendered, avoiding a full
page reload.
GanttFilterPane is session-scoped — one instance persists
across requests for each visitor. Inject it into your backing bean and
register a GanttFilterListener from your
@PostConstruct method. The listener receives 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.jsf.components.GanttChart;
import com.optomus.gantt.jsf.components.GanttFilterPane;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.time.*;
import java.util.List;
@Named
@RequestScoped
public class YourBackingBean {
@Inject private GanttChart ganttChart;
@Inject private GanttFilterPane ganttFilterPane;
// Store resolved filter state — use a session-scoped service
// bean in production rather than fields on a request-scoped bean
private LocalDateTime chartStart = defaultStart();
private Duration columnDuration = Duration.ofDays(1);
private int numColumns = 14;
@PostConstruct
public void init() {
ganttFilterPane.setFilterListener(new GanttFilterListener() {
@Override
public void onFilter(LocalDateTime start,
Duration duration, Integer cols) {
chartStart = start;
columnDuration = duration;
numColumns = cols;
buildChart();
}
@Override
public void onClearFilter() {
chartStart = defaultStart();
columnDuration = Duration.ofDays(1);
numColumns = 14;
buildChart();
}
});
buildChart();
}
private void buildChart() {
ganttChart.setGanttBars(buildBars());
ganttChart.setRowNames(List.of("Alice", "Bob", "Carol"));
ganttChart.setStartDate(chartStart);
ganttChart.setColumnDuration(columnDuration);
ganttChart.setNumColumns(numColumns);
ganttChart.build();
}
private static LocalDateTime defaultStart() {
return LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
}
}
chartStart,
columnDuration and numColumns must survive
across requests. Use a dedicated @SessionScoped CDI bean
to hold filter state in production rather than fields on a
request-scoped backing bean.
Include the filter pane template above the chart. The template's Apply
Filter button and Clear filter link already use f:ajax
internally — they re-render only ganttFilterZone and
ganttChartZone on each interaction, with full-page
fallback if JavaScript is disabled.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets">
<h:head>
<h:outputStylesheet library="gantt" name="GanttChart.css"/>
<h:outputStylesheet library="gantt" name="GanttFilterPane.css"/>
</h:head>
<h:body>
<div id="ganttFilterZone">
<ui:include src="/WEB-INF/gantt/GanttFilterPane.xhtml"/>
</div>
<div id="ganttChartZone">
<ui:include src="/WEB-INF/gantt/GanttChart.xhtml"/>
</div>
</h:body>
</html>
f:ajax partial
rendering is declared in the filter pane template itself. The
ganttFilterZone and ganttChartZone
div ids must match exactly — the template targets them by
these ids when re-rendering after each filter interaction.
The filter pane renders an input[type="datetime-local"]
field. On desktop browsers, native datetime pickers vary significantly
across browsers. Flatpickr provides a consistent, polished picker on
all desktop browsers while mobile browsers retain their superior native
pickers.
Add the following script to your page, after the closing
</h:form> tag or at the bottom of
<h:body>. The handleAjaxEvent function
reinitialises Flatpickr after each AJAX partial update so the picker
remains active after Apply Filter or Clear filter:
<h:outputScript>
(function () {
if (/Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i
.test(navigator.userAgent)) return;
var css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.css';
document.head.appendChild(css);
var style = document.createElement('style');
style.textContent =
'input.flatpickr-input[readonly] {' +
'width:100%;padding:8px 10px;min-height:2.4em;' +
'box-sizing:border-box;border:1px solid #e0d5c8;' +
'border-radius:6px;font-family:Source Sans 3,sans-serif;' +
'font-size:0.875rem;color:#2d1f14;background:#ffffff;' +
'cursor:pointer;transition:border-color 0.2s,box-shadow 0.2s;}' +
'input.flatpickr-input[readonly]:focus {' +
'outline:none;border-color:#4a9b8e;' +
'box-shadow:0 0 0 3px rgba(74,155,142,0.15);}';
document.head.appendChild(style);
var config = {
enableTime: true,
dateFormat: 'Y-m-dTH:i',
altInput: true,
altFormat: 'd M Y h:i K',
time_24hr: false,
minuteIncrement: 60,
allowInput: true
};
function initFlatpickr() {
var fields = document.querySelectorAll(
'input[type="datetime-local"]');
fields.forEach(function (el) { flatpickr(el, config); });
}
// Reinitialise Flatpickr after each JSF AJAX partial update
window.handleAjaxEvent = function(data) {
if (data.status === 'success') { initFlatpickr(); }
};
var script = document.createElement('script');
script.src =
'https://cdnjs.cloudflare.com/ajax/libs/flatpickr/4.6.13/flatpickr.min.js';
script.onload = initFlatpickr;
document.head.appendChild(script);
}());
</h:outputScript>
handleAjaxEvent is
the onevent callback referenced in the filter pane template's
f:ajax declarations. JSF calls it automatically after each
partial page update — you do not need to wire it up manually.
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 loads all filter pane labels from
GanttMessages.properties inside the
gantt-core JAR using JSF's f:loadBundle.
To override any label, place your own
GanttMessages.properties earlier on the classpath — for
example in src/main/resources/com/optomus/gantt/core/.
# 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
ResourceBundle via JSF's
f:loadBundle — no framework-specific configuration is
required. Locale-specific files such as
GanttMessages_fr.properties are picked up automatically
based on the active JSF locale.
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
in your @PostConstruct method before the page renders.
Confirm that the ganttFilterZone and
ganttChartZone div ids in your Facelets page
match exactly what the filter pane template targets. If the ids are
missing or misnamed, JSF's f:ajax cannot find the elements
to re-render and falls back to a full page reload.
Confirm that gantt-jsf-1.0.0.jar is on the classpath and
that your template includes both
h:outputStylesheet library="gantt" name="GanttChart.css"
and
h:outputStylesheet library="gantt" name="GanttFilterPane.css"
inside <h:head>. JSF serves these automatically from
the JAR's META-INF/resources/gantt/ directory.
Ensure chart state (chartStart, columnDuration,
numColumns) is stored in a @SessionScoped CDI
bean rather than in a request-scoped backing bean. Request-scoped beans
are destroyed and recreated on each request — any state held in them
is lost between requests.
Ensure the handleAjaxEvent function is defined in your
page before the Flatpickr script loads (Step 9). JSF calls
handleAjaxEvent after each partial page update — if the
function is not defined at that point, Flatpickr will not be
reinitialised and the enhanced date picker will disappear after the
first filter interaction.
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.