This example demonstrate nested groups that are 3 levels deep.
<style>
    div.vis-item-content {
        padding: 4px;
        border-radius: 2px;
        -moz-border-radius: 2px;
    }
    div.vis-item.vis-item-range {
        border-width: 0;
    }
    #overlappedOrders {
        margin-top: 20px;
        width: 100%;
    }
    #overlappedOrders .ui-chkbox {
        vertical-align: middle;
        margin: 3px 5px;
    }
</style>
<div class="card">
    <h:form id="form">
        <p:growl id="growl" showSummary="true" showDetail="false">
            <p:autoUpdate/>
        </p:growl>
        <p:timeline id="timeline" value="#{nestedGroupingTimelineView.model}" var="order" varGroup="truck"
                    editable="true" eventMargin="0" eventMarginAxis="0" stackEvents="false"
                    orientationAxis="top" widgetVar="timelineWdgt">
            <p:ajax event="changed" update="@none" listener="#{nestedGroupingTimelineView.onChange}"/>
            <p:ajax event="delete" update="@none" listener="#{nestedGroupingTimelineView.onDelete}"/>
            <p:ajax event="add" update="@none" onstart="PF('timelineWdgt').cancelAdd()"/>
            <f:facet name="group">
                <h:graphicImage library="demo" name="images/timeline/truck.png" style="vertical-align:middle;"
                                alt="Truck"/>
                <h:outputText value="#{truck}" style="font-weight:bold;"/>
            </f:facet>
            <h:graphicImage library="demo" name="#{order.imagePath}" rendered="#{not empty order.imagePath}"
                            style="display:inline; vertical-align:middle;" alt="Order"/>
            <h:outputText value="Order #{order.number}"/>
        </p:timeline>
        <!-- Dialog with overlapped timeline events -->
        <p:dialog id="overlapEventsDlg" header="Overlapped Orders" widgetVar="overlapEventsWdgt"
                  showEffect="clip" hideEffect="clip">
            <h:panelGroup id="overlappedOrdersInner" layout="block" style="padding:10px;">
                <strong>
                    Please choose Orders you want to merge with the Order #{nestedGroupingTimelineView.selectedOrder}
                </strong>
                <p/>
                <p:selectManyMenu id="overlappedOrders" value="#{nestedGroupingTimelineView.ordersToMerge}"
                                  showCheckbox="true">
                    <f:selectItems value="#{nestedGroupingTimelineView.overlappedOrders}" var="order"
                                   itemLabel=" Order #{order.data.number}" itemValue="#{order}"/>
                    <sc:convertOrder events="#{nestedGroupingTimelineView.model.events}"/>
                </p:selectManyMenu>
            </h:panelGroup>
            <f:facet name="footer">
                <h:panelGroup layout="block" style="text-align:right; padding:2px; white-space:nowrap;">
                    <p:commandButton value="Merge" process="overlapEventsDlg" update="@none"
                                     action="#{nestedGroupingTimelineView.merge}"
                                     oncomplete="PF('overlapEventsWdgt').hide()"/>
                    <p:commandButton type="button" value="Close" onclick="PF('overlapEventsWdgt').hide()"/>
                </h:panelGroup>
            </f:facet>
        </p:dialog>
    </h:form>
</div>
package org.primefaces.showcase.view.data.timeline;
import org.primefaces.PrimeFaces;
import org.primefaces.component.timeline.TimelineUpdater;
import org.primefaces.event.timeline.TimelineModificationEvent;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.model.timeline.TimelineGroup;
import org.primefaces.model.timeline.TimelineModel;
import org.primefaces.showcase.domain.Order;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
@Named("nestedGroupingTimelineView")
@ViewScoped
public class NestedGroupingTimelineView implements Serializable {
    private TimelineModel<Order, String> model;
    private TimelineEvent<Order> event; // current changed event
    private List<TimelineEvent<Order>> overlappedOrders; // all overlapped orders (events) to the changed order (event)
    private List<TimelineEvent<Order>> ordersToMerge; // selected orders (events) in the dialog which should be merged
    @PostConstruct
    protected void initialize() {
        model = new TimelineModel<>();
        // create nested groups
        TimelineGroup<String> group1 = new TimelineGroup<>("groupId1", "Truck Group Level 1", "groupId1", 1,
                Arrays.asList("groupId2", "id1", "id2", "id5", "id6"));
        TimelineGroup<String> group2 = new TimelineGroup<>("groupId2", "Truck Group Level 2", "groupId2", 2,
                Arrays.asList("id3", "id4"));
        TimelineGroup<String> group3 = new TimelineGroup<>("id1", "Truck 1", 2);
        TimelineGroup<String> group4 = new TimelineGroup<>("id2", "Truck 2", 2);
        TimelineGroup<String> group5 = new TimelineGroup<>("id3", "Truck 3", 3);
        TimelineGroup<String> group6 = new TimelineGroup<>("id4", "Truck 4", 3);
        TimelineGroup<String> group7 = new TimelineGroup<>("id5", "Truck 5", 2);
        TimelineGroup<String> group8 = new TimelineGroup<>("id6", "Truck 6", 2);
        TimelineGroup<String> group9 = new TimelineGroup<>("groupId3", "Truck Group Level 1", "groupId3", 1,
                Arrays.asList("id7", "id8", "id9"));
        TimelineGroup<String> group10 = new TimelineGroup<>("id7", "Truck 7", 2);
        TimelineGroup<String> group11 = new TimelineGroup<>("id8", "Truck 8", 2);
        TimelineGroup<String> group12 = new TimelineGroup<>("id9", "Truck 9", 2);
        // add nested groups to the model
        model.addGroup(group1);
        model.addGroup(group2);
        model.addGroup(group3);
        model.addGroup(group4);
        model.addGroup(group5);
        model.addGroup(group6);
        model.addGroup(group7);
        model.addGroup(group8);
        model.addGroup(group9);
        model.addGroup(group10);
        model.addGroup(group11);
        model.addGroup(group12);
        int orderNumber = 1;
        // iterate over groups
        for (int j = 1; j <= 12; j++) {
            LocalDateTime referenceDate = LocalDateTime.of(2015, Month.DECEMBER, 14, 8, 0);
            // iterate over events in the same group
            for (int i = 0; i < 6; i++) {
                LocalDateTime startDate = referenceDate.plusHours(3 * (Math.random() < 0.2 ? 1 : 0));
                LocalDateTime endDate = startDate.plusHours(2 + (int) Math.floor(Math.random() * 3));
                String imagePath = null;
                if (Math.random() < 0.25) {
                    imagePath = "images/timeline/box.png";
                }
                Order order = new Order(orderNumber, imagePath);
                model.add(TimelineEvent.<Order>builder()
                        .data(order)
                        .startDate(startDate)
                        .endDate(endDate)
                        .editable(true)
                        .group("id" + j)
                        .build());
                orderNumber++;
                referenceDate = endDate;
            }
        }
    }
    public TimelineModel<Order, String> getModel() {
        return model;
    }
    public void onChange(TimelineModificationEvent<Order> e) {
        // get changed event and update the model
        event = e.getTimelineEvent();
        model.update(event);
        // get overlapped events of the same group as for the changed event
        Set<TimelineEvent<Order>> overlappedEvents = model.getOverlappedEvents(event);
        if (overlappedEvents == null) {
            // nothing to merge
            return;
        }
        // list of orders which can be merged in the dialog
        overlappedOrders = new ArrayList<>(overlappedEvents);
        // no pre-selection
        ordersToMerge = null;
        // update the dialog's content and show the dialog
        PrimeFaces primefaces = PrimeFaces.current();
        primefaces.ajax().update("form:overlappedOrdersInner");
        primefaces.executeScript("PF('overlapEventsWdgt').show()");
    }
    public void onDelete(TimelineModificationEvent<Order> e) {
        // keep the model up-to-date
        model.delete(e.getTimelineEvent());
    }
    public void merge() {
        // merge orders and update UI if the user selected some orders to be merged
        if (ordersToMerge != null && !ordersToMerge.isEmpty()) {
            model.merge(event, ordersToMerge, TimelineUpdater.getCurrentInstance(":form:timeline"));
        }
        else {
            FacesMessage msg
                    = new FacesMessage(FacesMessage.SEVERITY_INFO, "Nothing to merge, please choose orders to be merged", null);
            FacesContext.getCurrentInstance().addMessage(null, msg);
        }
        overlappedOrders = null;
        ordersToMerge = null;
    }
    public int getSelectedOrder() {
        if (event == null) {
            return 0;
        }
        return event.getData().getNumber();
    }
    public List<TimelineEvent<Order>> getOverlappedOrders() {
        return overlappedOrders;
    }
    public List<TimelineEvent<Order>> getOrdersToMerge() {
        return ordersToMerge;
    }
    public void setOrdersToMerge(List<TimelineEvent<Order>> ordersToMerge) {
        this.ordersToMerge = ordersToMerge;
    }
}
package org.primefaces.showcase.domain;
public class Order implements java.io.Serializable {
    private final int number;
    private final String imagePath;
    public Order(int number, String imagePath) {
        this.number = number;
        this.imagePath = imagePath;
    }
    public int getNumber() {
        return number;
    }
    public String getImagePath() {
        return imagePath;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Order order = (Order) o;
        return number == order.number;
    }
    @Override
    public int hashCode() {
        return number;
    }
}
package org.primefaces.showcase.convert;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.showcase.domain.Order;
import java.io.Serializable;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.faces.component.UIComponent;
import jakarta.faces.context.FacesContext;
import jakarta.faces.convert.Converter;
import jakarta.faces.convert.FacesConverter;
import jakarta.inject.Named;
@Named
@ApplicationScoped
@FacesConverter("org.primefaces.showcase.converter.OrderConverter")
public class OrderConverter implements Converter<TimelineEvent<Order>>, Serializable {
    private List<TimelineEvent<Order>> events;
    public OrderConverter() {
    }
    @Override
    public TimelineEvent<Order> getAsObject(FacesContext context, UIComponent component, String value) {
        if (value == null || value.isEmpty() || events == null || events.isEmpty()) {
            return null;
        }
        for (TimelineEvent<Order> event : events) {
            if (event.getData().getNumber() == Integer.valueOf(value)) {
                return event;
            }
        }
        return null;
    }
    @Override
    public String getAsString(FacesContext context, UIComponent component, TimelineEvent<Order> value) {
        if (value == null) {
            return null;
        }
        return String.valueOf(value.getData().getNumber());
    }
    public List<TimelineEvent<Order>> getEvents() {
        return events;
    }
    public void setEvents(List<TimelineEvent<Order>> events) {
        this.events = events;
    }
}