Step 7: Gantt Chart

To analyze the functioning of the system as a whole, it is useful to have a visual representation of the equipment’s state on the time axis. One of the popular ways of doing this is a Gantt Chart. To implement it, we need to save the history of the forklift states. The simplest approach is to store such history in the Forklift object itself.

1. Simulation

Let’s make a class that represents a period of time in one specific state.

EquipmentStatsSlot.java
package com.company.warehouse.simulation.equipment;

import com.amalgamasimulation.core.scheduling.Slot;

public class EquipmentStatsSlot extends Slot {

    private final Forklift forklift;
    private final IEquipmentState state;
    private boolean closed = false;

    public EquipmentStatsSlot(Forklift forklift, IEquipmentState state) {
        // initially assign current time as beginTime & endTime
        super(forklift.time(), forklift.time());
        this.forklift = forklift;
        this.state = state;
    }

    /**
     * @return saved endTime if closed, current time otherwise
     */
    @Override
    public double endTime() {
        return closed ? super.endTime() : forklift.time();
    }

    /**
     * Saves current time as endTime
     */
    public void close() {
        max = forklift.time();
        closed = true;
    }

    public IEquipmentState getState() {
        return state;
    }

    public Forklift getForklift() {
        return forklift;
    }

}

Add the ability to save state slots in the Forklift.

Forklift.java
...
    /**
     * The history of states in time. To be used in the Gantt Chart.
     */
    private List<EquipmentStatsSlot> statsSlots = new ArrayList<>();
    public List<EquipmentStatsSlot> getStatsSlots() {
        return statsSlots;
    }
...
    /**
     * Save a slot for the state at current simulation time.
     * @param state  the state to be saved
     */
    public void putStatsSlot(IEquipmentState state) {
        var statsSlots = getStatsSlots();
        var newSlot = new EquipmentStatsSlot(this, state);
        if(!statsSlots.isEmpty()) {
            var lastSlot = statsSlots.get(statsSlots.size() - 1);
            if(lastSlot.duration() > 0) {
                lastSlot.close();
                statsSlots.add(newSlot);
            } else {
                statsSlots.set(statsSlots.size() - 1, newSlot);
            }
        } else {
            statsSlots.add(newSlot);
        }
    }
...

The putStatsSlot method closes the last slot before adding a new one. If the last slot has zero duration, it’s just replaced with a new one.

We’ll perform state saving from MovePalletTask and IdlingTask. To the constructors of these classes, add the following line:

MovePalletTask.java & IdlingTask.java
...
.addEnterAction((state, message) -> forklift.putStatsSlot(state));
...

The State Machine is very handy for such kinds of things. Now we can be sure that every state change will be saved.

That’s all with the modeling. Let’s make it visible in the UI.

2. Visual representation

For better distinguishing of the states, we’ll display them in different colors. In order to separate the representation from the model, we’ll create a special class in the application bundle.

EquipmentStateUI.java
package com.company.warehouse.application;

import java.awt.Color;
import java.util.Map;

import com.amalgamasimulation.utils.Colors;
import com.company.warehouse.simulation.equipment.IEquipmentState;
import com.company.warehouse.simulation.tasks.IdlingTask;
import com.company.warehouse.simulation.tasks.MovePalletTask;

public class EquipmentStateUI {

    public static String nameOf(IEquipmentState state) {
        return names.get(state);
    }

    public static Color colorOf(IEquipmentState state) {
        return colors.get(state);
    }

    private static Map<IEquipmentState, String> names = Map.of(
            MovePalletTask.State.PENDING, "-",
            MovePalletTask.State.MOVE_TO_LOADING, "Move to loading",
            MovePalletTask.State.LOADING, "Loading",
            MovePalletTask.State.MOVE_TO_UNLOADING, "Move to unloading",
            MovePalletTask.State.UNLOADING, "Unloading",
            MovePalletTask.State.FINISHED, "-",

            IdlingTask.State.PENDING, "-",
            IdlingTask.State.MOVE_TO_BASE, "Moving",
            IdlingTask.State.IDLING, "Idle",
            IdlingTask.State.CANCELLED, "Cancelled"
            );

    private static Map<IEquipmentState, Color> colors = Map.of(
            MovePalletTask.State.PENDING, Colors.gray,
            MovePalletTask.State.MOVE_TO_LOADING, Colors.orange,
            MovePalletTask.State.LOADING, Colors.blue,
            MovePalletTask.State.MOVE_TO_UNLOADING, Colors.orangeRed,
            MovePalletTask.State.UNLOADING, Colors.green,
            MovePalletTask.State.FINISHED, Colors.gray,

            IdlingTask.State.PENDING, Color.gray,
            IdlingTask.State.MOVE_TO_BASE, Colors.paleGoldenRod,
            IdlingTask.State.IDLING, Colors.gray,
            IdlingTask.State.CANCELLED, Colors.black
            );

}

Here we provide colors and display labels for all the states that a forklift can be in.

The Gantt Chart display in the UI is handled by the GanttChartPart. Let’s update its updateContent method:

GanttChartPart.java
    private void updateContent(Model model) {
        ganttChart.getVisualSetContainer().clear();
        if (model != null) {
            ganttChart.getXAxis().setTimeStyle(AxisTimeStyle.getDefault(model.timeToDate(0), model.timeUnit()))
                .setDisplayedRange(0,  model.minute() * 30);
            for (var forklift : model.getForklifts()) {
                var visualSet = new GanttVisualSet<>(forklift.getName(), () -> forklift.getStatsSlots(), t -> t.beginTime(), t -> t.endTime())
                        .setBackgroundColor(s -> EquipmentStateUI.colorOf(s.getState()))
                        .setLabelText(LabelSide.MIDDLE_CENTER, s -> EquipmentStateUI.nameOf(s.getState()), s -> 12.0, s -> Colors.white)
                        .setLabelText(LabelSide.TOP_LEFT, this::getBeginTimeText, s -> 9.0, s -> Colors.white);
                ganttChart.getVisualSetContainer().addVisualSet(visualSet);
            }
        }
        ganttChart.redraw();
    }
    
    private String getBeginTimeText(EquipmentStatsSlot slot) {
        return Formats.getDefaultFormats().dayMonthLongYearHoursMinutes(timeToDate(slot.beginTime()));
    }

Note:

  • We set the initial chart display range to a 30-minute interval in order to see our forklifts' activity immediately without zooming.

  • The Formats class provides plenty of methods to represent different data types as strings.

3. What did we get?

Relaunch the app and open the Gantt Chart tab.

Gantt chart

Use the mouse wheel to zoom and dragging to pan along the time axis.