Grouping events can be useful for example when showing availability of multiple people, rooms, or other resources next to each other.
<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>
<h:form id="form">
    <p:growl id="growl" showSummary="true" showDetail="false">
        <p:autoUpdate/>
    </p:growl>
    <div class="card">
        <h5>Timeline Grouping</h5>
        <p:timeline id="timeline" value="#{groupingTimelineView.model}" var="order" varGroup="truck"
                    editable="true" eventMargin="0" eventMarginAxis="0" stackEvents="false"
                    orientationAxis="top" widgetVar="timelineWdgt">
            <p:ajax event="changed" update="@none" listener="#{groupingTimelineView.onChange}"/>
            <p:ajax event="delete" update="@none" listener="#{groupingTimelineView.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 #{truck.code}" 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>
    </div>
    <div class="card">
        <h5>Timeline Grouping With Vertical Scroll and zoomKey(ctrlKey) </h5>
        <p:timeline id="timelineVerticalScroll" value="#{groupingTimelineView.model2}" var="order"
                    varGroup="truck" zoomKey="ctrlKey"
                    editable="true" eventMargin="0" eventMarginAxis="0" stackEvents="false"
                    orientationAxis="top" orientationItem="top" maxHeight="400" horizontalScroll="false"
                    verticalScroll="true" widgetVar="timelineWdgt2">
            <f:facet name="group">
                <h:graphicImage library="demo" name="images/timeline/truck.png" style="vertical-align:middle;"
                                alt="Truck"/>
                <h:outputText value="Truck #{truck.code}" 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>
    </div>
    <!-- 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 #{groupingTimelineView.selectedOrder}
            </strong>
            <p/>
            <p:selectManyMenu id="overlappedOrders" value="#{groupingTimelineView.ordersToMerge}"
                              showCheckbox="true">
                <f:selectItems value="#{groupingTimelineView.overlappedOrders}" var="order"
                               itemLabel=" Order #{order.data.number}" itemValue="#{order}"/>
                <sc:convertOrder events="#{groupingTimelineView.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="#{groupingTimelineView.merge}"
                                 oncomplete="PF('overlapEventsWdgt').hide()"/>
                <p:commandButton type="button" value="Close" onclick="PF('overlapEventsWdgt').hide()"/>
            </h:panelGroup>
        </f:facet>
    </p:dialog>
</h:form>
package org.primefaces.showcase.view.data.timeline;
import jakarta.annotation.PostConstruct;
import jakarta.faces.application.FacesMessage;
import jakarta.faces.context.FacesContext;
import jakarta.faces.view.ViewScoped;
import jakarta.inject.Named;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import io.quarkus.runtime.annotations.RegisterForReflection;
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;
@Named("groupingTimelineView")
@ViewScoped
@RegisterForReflection(serialization = true)
public class GroupingTimelineView implements Serializable {
    @Serial
    private static final long serialVersionUID = 1L;
    private TimelineModel<Order, Truck> model;
    private TimelineModel<Order, Truck> model2;
    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() {
        // create timeline model
        model = newModelWithNumber(6);
        model2 = newModelWithNumber(30);
    }
    public TimelineModel<Order, Truck> newModelWithNumber(int n) {
        TimelineModel<Order, Truck> model = new TimelineModel<>();
        int orderNumber = 1;
        for (int j = 1; j <= n; j++) {
            model.addGroup(new TimelineGroup<>("id" + j, new Truck(String.valueOf(9 + j))));
            LocalDateTime referenceDate = LocalDateTime.of(2015, Month.DECEMBER, 14, 8, 0);
            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() * 4));
                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;
            }
        }
        return model;
    }
    public TimelineModel<Order, Truck> getModel() {
        return model;
    }
    public TimelineModel<Order, Truck> getModel2() {
        return model2;
    }
    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;
    }
    @RegisterForReflection
    public static class Truck implements java.io.Serializable {
        @Serial
        private static final long serialVersionUID = 1L;
        private final String code;
        public Truck(String code) {
            this.code = code;
        }
        public String getCode() {
            return code;
        }
    }
}
package org.primefaces.showcase.domain;
import io.quarkus.runtime.annotations.RegisterForReflection;
@RegisterForReflection
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 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;
import org.primefaces.model.timeline.TimelineEvent;
import org.primefaces.showcase.domain.Order;
@Named
@ApplicationScoped
@FacesConverter("org.primefaces.showcase.converter.OrderConverter")
public class OrderConverter implements Converter<TimelineEvent<Order>>, Serializable {
	private static final long serialVersionUID = 1L;
	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;
    }
}