Quick-Start Guide
OptoGantt — Apache Tapestry
Follow these steps to add an interactive Gantt chart to your Apache Tapestry 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-tapestry-1.0.0.jar (artifactId: gantt-tapestry).
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-tapestry</artifactId>
<version>1.0.0</version>
</dependency>
gantt-core is a transitive dependency of
gantt-tapestry and is resolved automatically.
You do not need to declare it separately.
Extract GanttChart.css, GanttFilterPane.css,
and filter-icon.png from the gantt-tapestry-1.0.0.jar
and copy them into your application's static asset directory:
jar xf gantt-tapestry-1.0.0.jar \ META-INF/resources/gantt/GanttChart.css \ META-INF/resources/gantt/GanttFilterPane.css \ META-INF/resources/gantt/filter-icon.png
Place the extracted files in your application:
src/main/webapp/
css/
GanttChart.css
GanttFilterPane.css
static/
filter-icon.png
Then add stylesheet links to your application's layout component:
<link rel="stylesheet" href="${context:css/GanttChart.css}"/>
<link rel="stylesheet" href="${context:css/GanttFilterPane.css}"/>
src/main/webapp/),
not directly from JAR files. This one-time copy is all that is needed —
no build plugin configuration required.
Tapestry discovers components in external JARs via a contribution to
ComponentClassResolver in your application's IoC module
class (typically AppModule.java). Add the following
contribution to register the gantt library prefix:
import org.apache.tapestry5.services.ComponentClassResolver;
import org.apache.tapestry5.ioc.Configuration;
public static void contributeComponentClassResolver(
Configuration<LibraryMapping> configuration) {
configuration.add(new LibraryMapping(
"gantt", "com.optomus.gantt.tapestry.components"));
}
Once registered, the GanttChart component is available
in your templates as <t:ganttchart .../> using
the gantt: namespace, or simply as
<t:ganttchart .../> if Tapestry can infer the
library from the prefix.
xmlns:gantt="tapestry-library:gantt". You can then
use <gantt:ganttchart .../> and
<gantt:ganttfilterpane .../>.
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.ArrayList;
import java.util.Collection;
// Build bars relative to today
LocalDate today = LocalDate.now();
Collection<GanttBar> bars = new ArrayList<>();
bars.add(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
));
bars.add(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
));
bars.add(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
));
rowNames explicitly in Step 6.
Unlike the Spring edition, Tapestry components receive their data
directly via template parameters — there is no separate
build() call. Supply startDate,
columnDuration, numColumns, and
ganttBarItems from your page class via
@Property fields.
import com.optomus.gantt.core.model.GanttBar;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SetupRender;
public class YourPage {
@Property
@Persist
private LocalDateTime startDate;
@Property
@Persist
private Duration columnDuration;
@Property
@Persist
private Integer numColumns;
@Property
private Collection<GanttBar> ganttBarItems;
@Property
private List<String> rowNames;
@SetupRender
public void setup() {
if (startDate == null) {
startDate = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
columnDuration = Duration.ofDays(1);
numColumns = 14;
}
ganttBarItems = buildBars(); // your bar list from Step 5
rowNames = Arrays.asList("Alice", "Bob", "Carol");
}
}
<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:gantt="tapestry-library:gantt">
<head>
<link rel="stylesheet" href="${context:css/GanttChart.css}"/>
<link rel="stylesheet" href="${context:css/GanttFilterPane.css}"/>
</head>
<body>
<gantt:ganttchart
startDate="startDate"
columnDuration="columnDuration"
numColumns="numColumns"
ganttBarItems="ganttBarItems"
rowNames="rowNames"/>
</body>
</html>
@Persist is required on
startDate, columnDuration, and
numColumns. Without it, Tapestry resets these fields
to null on every request and the chart always renders
with default parameters regardless of any filter the visitor has applied.
Part 2 — Interactive filter pane with AJAX
Add the filter pane so visitors can adjust the chart's date range and period. Tapestry handles all AJAX communication natively via its zone mechanism — no JavaScript fetch calls, no JSON endpoints, no manual DOM updates required.
Add a @Persist field for the selected
GanttColumnDuration enum value, and implement
onFilter() and onClearFilter() event
handlers. These are called automatically by the
GanttFilterPane component via Tapestry's event system
when the visitor applies or clears the filter.
import com.optomus.gantt.core.model.GanttColumnDuration;
import com.optomus.gantt.tapestry.components.GanttFilterPane;
import org.apache.tapestry5.annotations.InjectComponent;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.annotations.SetupRender;
import org.apache.tapestry5.corelib.components.Zone;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
public class YourPage {
// Chart state — all @Persist to survive across requests
@Property @Persist private LocalDateTime startDate;
@Property @Persist private Duration columnDuration;
@Property @Persist private Integer numColumns;
@Property @Persist private GanttColumnDuration ganttColumnDuration;
// Bar data — rebuilt each request from your data source
@Property private Collection<GanttBar> ganttBarItems;
@Property private List<String> rowNames;
@InjectComponent private Zone ganttZone;
@Inject private AjaxResponseRenderer ajaxResponseRenderer;
@SetupRender
public void setup() {
if (startDate == null) {
startDate = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
ganttColumnDuration = GanttColumnDuration.FOURTEEN_DAYS;
columnDuration = Duration.ofDays(1);
numColumns = 14;
}
ganttBarItems = loadBarsFromDataSource(); // your data source
rowNames = Arrays.asList("Alice", "Bob", "Carol");
}
/**
* Called by GanttFilterPane when the visitor applies a filter.
* GanttFilterPaneCore has already resolved the enum selection to a
* concrete Duration and numColumns — pass them directly to the chart.
*/
public void onFilter() {
GanttFilterPane filterPane = // @InjectComponent reference — see Step 8
startDate = filterPane.getStartDate() != null
? filterPane.getStartDate().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDateTime()
: LocalDateTime.of(LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
columnDuration = filterPane.getResolvedColumnDuration();
numColumns = filterPane.getResolvedNumColumns();
ganttColumnDuration = filterPane.getColumnDuration();
ganttBarItems = loadBarsFromDataSource();
ajaxResponseRenderer.addRender(ganttZone);
}
/**
* Called by GanttFilterPane when the visitor clears the filter.
* Reset to defaults and re-render the chart zone.
*/
public void onClearFilter() {
startDate = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
ganttColumnDuration = GanttColumnDuration.FOURTEEN_DAYS;
columnDuration = Duration.ofDays(1);
numColumns = 14;
ganttBarItems = loadBarsFromDataSource();
ajaxResponseRenderer.addRender(ganttZone);
}
}
ajaxResponseRenderer.addRender(zone) call tells Tapestry
which zone to re-render and stream back to the browser automatically.
Include the GanttFilterPane component above the chart.
Wrap the chart in a t:zone so Tapestry can re-render
it in place after each filter interaction. The filter pane manages
its own zone internally — no additional wrapping is needed.
<!DOCTYPE html>
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_3.xsd"
xmlns:gantt="tapestry-library:gantt">
<head>
<link rel="stylesheet" href="${context:css/GanttChart.css}"/>
<link rel="stylesheet" href="${context:css/GanttFilterPane.css}"/>
</head>
<body>
<gantt:ganttfilterpane
startDate="startDate"
columnDuration="ganttColumnDuration"/>
<t:zone t:id="ganttZone" id="ganttZone">
<gantt:ganttchart
startDate="startDate"
columnDuration="columnDuration"
numColumns="numColumns"
ganttBarItems="ganttBarItems"
rowNames="rowNames"/>
</t:zone>
</body>
</html>
GanttFilterPane
takes ganttColumnDuration (the GanttColumnDuration
enum) for its dropdown display, while GanttChart takes
columnDuration (a java.time.Duration) for
rendering. Both are updated in onFilter() from the
resolved values provided by GanttFilterPaneCore.
To read resolved filter values in onFilter(), inject
the GanttFilterPane component using
@InjectComponent. This gives your page direct access
to getResolvedColumnDuration(),
getResolvedNumColumns(), and
getColumnDuration() after the filter has been applied.
@InjectComponent("ganttFilterPane")
private GanttFilterPane ganttFilterPane;
@InjectComponent
private Zone ganttZone;
@Inject
private AjaxResponseRenderer ajaxResponseRenderer;
public void onFilter() {
// Read resolved values from GanttFilterPane —
// GanttFilterPaneCore has already extrapolated the enum selection
// to a concrete Duration and numColumns
startDate = toLocalDateTime(ganttFilterPane.getStartDate());
columnDuration = ganttFilterPane.getResolvedColumnDuration();
numColumns = ganttFilterPane.getResolvedNumColumns();
ganttColumnDuration = ganttFilterPane.getColumnDuration();
ganttBarItems = loadBarsFromDataSource();
ajaxResponseRenderer.addRender(ganttZone);
}
public void onClearFilter() {
startDate = LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
ganttColumnDuration = GanttColumnDuration.FOURTEEN_DAYS;
columnDuration = Duration.ofDays(1);
numColumns = 14;
ganttBarItems = loadBarsFromDataSource();
ajaxResponseRenderer.addRender(ganttZone);
}
private LocalDateTime toLocalDateTime(java.util.Date date) {
if (date == null) return LocalDateTime.of(
LocalDate.now().minusDays(6), LocalTime.MIDNIGHT);
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
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
passing them as parameters to the GanttChart component.
All colours accept CSS hex strings via GanttColour.
@Property
private GanttColour headingTextColour = new GanttColour("#ffffff");
@Property
private GanttColour headingBackgroundColour = new GanttColour("#3d2b1f");
@Property
private GanttColour borderColour = new GanttColour("#6b4c35");
@Property
private GanttColour alternateRowsBackground = new GanttColour("#faf7f3");
<gantt:ganttchart
startDate="startDate"
columnDuration="columnDuration"
numColumns="numColumns"
ganttBarItems="ganttBarItems"
rowNames="rowNames"
headingTextColour="headingTextColour"
headingBackgroundColour="headingBackgroundColour"
borderColour="borderColour"
alternateRowsBackgroundColour="alternateRowsBackground"/>
If only headingBackgroundColour is supplied, border
and alternate row colours are derived automatically as darker and
lighter shades respectively.
@Property private String nameColumnWidth = "180px"; // any valid CSS width @Property private Integer rowHeightInPixels = 40;
<gantt:ganttchart
...
nameColumnWidth="nameColumnWidth"
rowHeightInPixels="rowHeightInPixels"/>
All filter pane labels are loaded from
GanttMessages.properties inside the
gantt-core JAR via Java's ResourceBundle.
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
GanttMessages_fr.properties automatically
based on the active locale. No additional framework configuration
is required.
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.
The most common cause is missing @Persist annotations
on startDate, columnDuration,
numColumns, and ganttColumnDuration.
Tapestry resets non-persisted fields to null on every
request. Ensure all four fields are annotated with
@Persist on your page class.
Confirm that AppModule.java contains the
contributeComponentClassResolver contribution mapping
the gantt library prefix to
com.optomus.gantt.tapestry.components (Step 4).
Without this, Tapestry cannot locate GanttChart or
GanttFilterPane.
Confirm that GanttChart.css and
GanttFilterPane.css have been copied to
src/main/webapp/css/ (Step 3) and that your layout
template includes both stylesheet links. Tapestry serves CSS from
the web application context, not directly from JAR files.
Confirm that filter-icon.png has been copied to
src/main/webapp/static/ (Step 3). The
GanttFilterPane component references it at
context:static/filter-icon.png.
Confirm that your page class has an onFilter() method
and that it calls
ajaxResponseRenderer.addRender(ganttZone).
Also confirm that @InjectComponent("ganttFilterPane")
matches the component id in your template. If the component id
does not match, Tapestry cannot inject the reference and
getResolvedColumnDuration() will not be available.
This typically means ganttFilterPane.getResolvedColumnDuration()
is returning null because validation failed silently.
Ensure the visitor has entered both a start date and a period before
clicking Apply Filter. The filter pane validates both fields and
displays an error message if either is blank — the
onFilter() event is only triggered after successful
validation.
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.