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

Filter
Sat, 25 AprSun, 26 AprMon, 27 AprTue, 28 AprWed, 29 AprThu, 30 AprFri, 1 MaySat, 2 MaySun, 3 MayMon, 4 MayTue, 5 MayWed, 6 MayThu, 7 May
Alice
  • Project Kickoff Project Kickoff | Project Kickoff | Alice | Completed
Bob
  • Development Sprint Development Sprint | Development Sprint | Bob | In progress
Carol
  • Testing & QA Testing & QA | Testing & QA | Carol | Upcoming

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 both JARs to your local Maven repository by running the following command once for each, 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-tapestry-1.0.0.jar (artifactId: gantt-tapestry). The remaining 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-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.

3
Copy CSS and icon to your web application

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:

Shell — extract from JAR
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:

Project layout
src/main/webapp/
  css/
    GanttChart.css
    GanttFilterPane.css
  static/
    filter-icon.png

Then add stylesheet links to your application's layout component:

Layout.tml
<link rel="stylesheet" href="${context:css/GanttChart.css}"/>
<link rel="stylesheet" href="${context:css/GanttFilterPane.css}"/>
Why manual copying? Tapestry serves static assets from the web application context (src/main/webapp/), not directly from JAR files. This one-time copy is all that is needed — no build plugin configuration required.
4
Register the component library

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:

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

Namespace in templates: add the gantt namespace declaration to any template that uses OptoGantt components: xmlns:gantt="tapestry-library:gantt". You can then use <gantt:ganttchart .../> and <gantt:ganttfilterpane .../>.
5
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).

YourPage.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.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
));
Row order: rows appear in the chart in the order they are first encountered in the bar list, unless you supply rowNames explicitly in Step 6.
6
Add the chart component to your page

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.

YourPage.java
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");
    }
}
YourPage.tml
<!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.
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.

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.

7
Add filter state fields and event handlers to your page

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.

YourPage.java
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);
    }
}
No JSON endpoints needed: Tapestry's zone mechanism handles all AJAX communication internally. The ajaxResponseRenderer.addRender(zone) call tells Tapestry which zone to re-render and stream back to the browser automatically.
8
Add the filter pane and chart zone to your template

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.

YourPage.tml
<!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>
Two duration fields: 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.
9
Inject the filter pane component and wire onFilter()

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.

YourPage.java — complete onFilter() wiring
@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();
}
Your chart is now fully interactive with Tapestry's native AJAX zone 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.

10
Customise chart colours Optional

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.

YourPage.java
@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");
YourPage.tml
<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.

11
Customise row height and name column width Optional
YourPage.java
@Property private String  nameColumnWidth   = "180px"; // any valid CSS width
@Property private Integer rowHeightInPixels = 40;
YourPage.tml
<gantt:ganttchart
    ...
    nameColumnWidth="nameColumnWidth"
    rowHeightInPixels="rowHeightInPixels"/>
12
Internationalise labels via a properties file Optional

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

GanttMessages.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: Tapestry picks up locale-specific files such as GanttMessages_fr.properties automatically based on the active locale. No additional framework configuration is required.
13
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.

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

!
Chart always renders with default parameters

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.

!
Component not found — ClassNotFoundException or unknown component

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.

!
CSS not loading

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.

!
Filter icon not displaying

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.

!
Apply Filter does not update the chart

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.

!
NullPointerException in onFilter()

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

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.