Adding week numbers to Gantt Chart

Hey all,

I am working on a GANTT chart application which shows tasks planned in on employees. My question is if it is possible to add week numbers to the value axis labels. I found the setLabelFunction() which allows me to add Javascript code in a string in order to create custom labels but I am not sure how I should get started with this in order to add week numbers to the axis.

Do any of you have experience with using this function or know another way of adding weeknumbers/custom date labels to a chart? Any help to get me started would be appreciated!

(Blue scribbled out area contains the employees, I am trying to add week numbers in the red marked area. XML and Java code used is displayed below as well, but I think it will not be of much use)

image

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="msg://browseCaption"
        messagesPack="com.company.gantttest.web.employee" xmlns:chart="http://schemas.haulmont.com/charts/charts.xsd">
    <data readOnly="true">
        <collection id="employeesDc"
                    class="com.company.gantttest.entity.Employee"
                    view="employee-view">
            <loader id="employeesDl">
                <query><![CDATA[select e from gantttest_Employee e ORDER BY e.group.position, e.id]]></query>
            </loader>
        </collection>
    </data>
    <dialogMode height="600"
                width="800"/>
    <layout spacing="true">
        <vbox id="vbox" spacing="true">
            <buttonsPanel align="MIDDLE_CENTER">
                <button id="refreshButton" align="MIDDLE_CENTER" caption="Refresh" invoke="onRefreshButtonClick"
                        stylename="primary"/>
                <button id="resetButton" caption="Huidige week" invoke="onResetButtonClick" stylename="primary"
                        align="MIDDLE_CENTER"/>
                <button id="newTaskButton" caption="Nieuwe taak" invoke="onNewTaskButtonClick" stylename="friendly"
                        align="MIDDLE_CENTER"/>
                <button id="button_2" caption="Taak voor iedereen" invoke="onButton_2Click"/>
            </buttonsPanel>
            <buttonsPanel height="51px" id="buttonsPanel_1" align="MIDDLE_CENTER">
                <button id="button_1" caption="Vorige week" invoke="onBackClick" stylename="danger"
                        align="MIDDLE_CENTER"/>
                <dateField id="dateField" align="MIDDLE_CENTER" caption="Ga naar: " datatype="dateTime"/>
                <button id="button" caption="Volgende week" invoke="onNextClick" stylename="friendly"
                        align="MIDDLE_CENTER"/>
            </buttonsPanel>
            <chart:ganttChart id="ganttChart1"
                              brightnessStep="1"
                              balloonDateFormat="JJ:NN"
                              creditsPosition="BOTTOM_RIGHT"
                              panEventsEnabled="true"
                              additionalSegmentFields="captionCreated, captionUpdated, captionStart, captionEnd, orderId, task, order, category.description"
                              colorField="category.color"
                              categoryField="name"
                              gridAboveGraphs="false"
                              columnWidth="1"
                              dataContainer="employeesDc"
                              endDateField="end"
                              marginRight="10"
                              period="WEEKS"
                              rotate="true"
                              segmentsField="tasks"
                              startDateField="start"
                              theme="LIGHT"
                              width="100%" height="AUTO"
                              synchronizeGrid="true"
            >
            <chart:graph balloonText="&lt;strong&gt;[[orderId]] [[task]] &lt;/strong&gt; &lt;br&gt; [[category.description]] &lt;br&gt;  &lt;i&gt;[[order]] &lt;/i&gt; &lt;br&gt; &lt;br&gt; &lt;strong&gt;Start:&lt;/strong&gt; [[captionStart]] &lt;br&gt; &lt;strong&gt;Eind:&lt;/strong&gt; [[captionEnd]] &lt;br&gt;"
                         fontSize="11"
                         bullet="ROUND"
                         behindColumns="false"
                         fillAlphas="0.75"
                         labelText="[[task]]"
                         labelPosition="MIDDLE"
                         bulletSize="12"
                         bulletBorderAlpha="255"
                         bulletColor="BLACK"
                         bulletBorderThickness="6"
                         showBulletsAt="HIGH"
                         useLineColorForBulletBorder="true"

            />
            <chart:valueAxis id="axis1"
                             maximum="4"
                             minimum="1"
                             type="DATE"
                             position="TOP"
                             includeAllValues="true"
                             labelsEnabled="true"
                             labelRotation="90"
                             boldLabels="true"
            />
            <chart:chartCursor cursorAlpha="0.3"
                               cursorPosition="MIDDLE"
                               fullWidth="true"
                               valueLineAlpha="0.5"
                               valueBalloonsEnabled="false"
                               valueLineBalloonEnabled="true"
                               valueLineEnabled="true"
                               selectWithoutZooming="true"
                               valueZoomable="false"
            />
        </chart:ganttChart>
        </vbox>
    </layout>
</window>

Java code:

@UiController("gantttest_Employee.browsePlanning")
@UiDescriptor("planning.xml")
@LookupComponent("employeesTable")
@LoadDataBeforeShow
public class Planning extends StandardLookup<Employee> {

    @Inject
    private GanttChart ganttChart1;
    @Inject
    private DateField<Date> dateField;

    @Inject
    private Screens screens;
    @Inject
    private DataManager dataManager;
    @Inject
    private Notifications notifications;

    @Inject
    private CollectionLoader<Employee> employeesDl;
    @Inject
    private CollectionContainer<Employee> employeesDc;
    @Inject
    private Dialogs dialogs;

//******INITIATIE VAN SCHERMEN******//
//Deze code wordt uitgevoerd bij initialisering van het scherm
    @Subscribe
    private void onInit(InitEvent event) {

        //Listeners die bij wijzigingen van het dateField, de closest maandag berekenen en de grafiek aanpassen.
        dateField.addValueChangeListener(dateValueChangeEvent -> ganttChart1.setStartDate(calculateStartDate(dateField.getValue())));
        dateField.addValueChangeListener(dateValueChangeEvent -> ganttChart1.repaint());

        //Listeners die luisteren naar klik acties in de grafiek. Een voor linkermuisknop (taak openen en aanpassen) en een voor rechtermuisknop (taak verwijderen)
        ganttChart1.addGraphItemClickListener(graphItemClickEvent -> showScreen(graphItemClickEvent.getEntity()));
        ganttChart1.addGraphItemRightClickListener(graphItemClickEvent -> deleteEntity(graphItemClickEvent.getEntity()));
        
    }

//Voer deze code uit op het moment dat alle onderdelen van het scherm geladen zijn.
    @Subscribe
    private void onAfterShow(AfterShowEvent event) {
        //Zet datum op datum van vandaag
        Date date = new Date();
        //Calculeer de dichtbijzijnste maandag
        ganttChart1.setStartDate(calculateStartDate(date));

        //Tel het aantal medewerkers en calculeer de hoogte van de GANTT op basis van dit
        setGanttLength();

        //Notificatie met instructies
        notifications.create(Notifications.NotificationType.TRAY).withCaption("Klik met linkermuisknop op taken om deze te openen. Gebruik rechtermuisknop om taken te verwijderen!").show();

    }

//******CALCULATIES******//
//Functie die de closest maandag opzoekt
    private Date calculateStartDate(Date date) {
        //Als er geen datum aangereikt wordt, gebruik de datum van vandaag
        if (date == null) {
            date = new Date();
        }

        //Zet de datum om naar een instant (lang getal) en haal er precies 1 dag aan seconden af (dit is omdat de grafiek altijd een week naar voren toe stond)
        Instant instant = date.toInstant().minusSeconds(604800);
        //Zet de instant weer om naar een datum
        Date dateConverted = Date.from(instant);
        //Haal de dag van de week op via de funcie dayOfWeek()
        Integer day = dayOfWeek(date);

        Calendar calendar = Calendar.getInstance();
        calendar.setTime(dateConverted);
        calendar.set(Calendar.MILLISECOND, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.HOUR_OF_DAY, 0);

        Date datec = calendar.getTime();

        //Zoek aan de hand van if/else statements op welke dag van de week de datum valt. En stel aan de hand van seconden bij zodat de begindatum van de GANTT chart altijd op maandag is.
        if (day == Calendar.MONDAY) {
            return dateConverted;
        }
        else if (day == Calendar.TUESDAY) {
            Instant i = datec.toInstant().minusSeconds(86400);
            return Date.from(i);
        }
        else if (day == Calendar.WEDNESDAY) {
            Instant i = datec.toInstant().minusSeconds(172800);
            return Date.from(i);
        }
        else if (day == Calendar.THURSDAY) {
            Instant i = datec.toInstant().minusSeconds(259200);
            return Date.from(i);
        }
        else if (day == Calendar.FRIDAY) {
            Instant i = datec.toInstant().minusSeconds(345600);
            return Date.from(i);
        }
        else if (day == Calendar.SATURDAY) {
            Instant i = datec.toInstant().minusSeconds(432000);
            return Date.from(i);
        }
        else if (day == Calendar.SUNDAY) {
            Instant i = datec.toInstant().minusSeconds(518400);
            return Date.from(i);
        }
        //Indien geen van de condities klopt, geef dan een foutmelding
        throw new IllegalArgumentException("No day of the week!");
    }

//Function die de dag van de week opzoekt.
    private Integer dayOfWeek(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal.get (Calendar.DAY_OF_WEEK);
    }

//Wanneer er met linkermuisknop op een scherm gedrukt wordt, dan wordt deze actie aangeroepen. Aan de hand van de entityID wordt het desbetreffende scherm van de taak opgezocht.
    private void showScreen(Entity id) {
        //Maak een nieuwe instance van het task-edit scherm
        EditorScreen screen = screens.create(TaskEdit.class);
        //Zet de waardes van de instance op de waardes die vanuit de klikactie komt
        screen.setEntityToEdit(id);
        //Voeg een evenement toe die ervoor zorgt dat de GANTT chart opnieuw geladen wordt op het moment dat het edit scherm gesloten wordt.
        ((TaskEdit) screen).addAfterCloseListener(afterCloseEvent -> reloadData());
        //Laat het scherm zien
        ((TaskEdit) screen).show();
    }

//Wanneer er met rechtermuisknop geklikt wordt, voer deze functie dan uit. Aan de hand van entityID wordt de task waar op gedrukt wordt, verwijderd.
    private void deleteEntity(Entity id) {
        //Maak een dialoog scherm en vraag om confirmatie van verwijderen
        dialogs.createOptionDialog()
                .withCaption("Verwijderen van taak!")
                .withMessage("Weet je zeker dat je deze taak wil verwijderen?")
                .withActions(
                        //Actie voor het verwijderen van het scherm
                        new DialogAction(DialogAction.Type.YES, Action.Status.PRIMARY).withHandler(e -> {
                           //Verwijder de task met overeenkomende id
                            dataManager.remove(id);
                            notifications.create().withCaption("Verwijderd!").withDescription("De taak is verwijderd.").show();
                            //Laad de datasource opnieuw zodat wijzigingen zichtbaar worden
                            employeesDl.load();
                        }),
                        //Actie voor het annuleren en niets doen
                        new DialogAction(DialogAction.Type.NO)
                )
                .show();
    }

//Bereken en stel de hoogte van de GANTT in.
    private void setGanttLength() {
        //Variabeles om op te tellen
        Integer c = 0;
        Integer cn = 0;

        //Loop door alle medewerkers in het systeem
        for (Employee employee : employeesDc.getItems()) {
            //Voor elke medewerker, tel 1 op
            cn = c + 1;
            c = cn;
        }

        //Doe het aantal medewerkers keer 25 (elke medewerker heeft 25 pixels nodig om weer te geven)
        Integer height = c * 25;
        String heightString = height.toString();

        //Stel de hoogte van de GANTT in
        ganttChart1.setHeight(heightString);
    }

//Functie die alle data herlaad
    private void reloadData() {
        employeesDl.load();
        ganttChart1.repaint();
    }

//******BUTTONS******//
//Functie die aangeroepen wordt bij het indrukken van de volgende week button
    public void onNextClick() {
        Date start = ganttChart1.getStartDate();
        Instant instant = start.toInstant().plusSeconds(604800);
        Date added = Date.from(instant);
        ganttChart1.setStartDate(added) ;
        ganttChart1.repaint();
    }

//Functie die aangeroepen wordt bij het indrukken van de vorige week button
    public void onBackClick() {
        Date start = ganttChart1.getStartDate();
        Instant instant = start.toInstant().minusSeconds(604800);
        Date added = Date.from(instant);
        ganttChart1.setStartDate(added) ;
        ganttChart1.repaint();
    }

//Functie die aangeroepen wordt bij het indrukken van de refresh button
    public void onRefreshButtonClick() {
        reloadData();
    }

//Functie die aangeroepen wordt bij het indrukken van de reset button
    public void onResetButtonClick() {
        dateField.clear();
        ganttChart1.setStartDate(calculateStartDate(dateField.getValue()));
        ganttChart1.repaint();
    }

//Functie die aangeroepen wordt bij het indrukken van de nieuwe taak button
    public void onNewTaskButtonClick() {
        //Zet variabele als een task-edit scherm.
        EditorScreen screen = screens.create(TaskEdit.class);
        //Zet de data in het scherm op een nieuwe instance van de taak
        screen.setEntityToEdit(new Task());

        ((TaskEdit) screen).addAfterCloseListener(afterCloseEvent -> reloadData());
        //Laat het scherm zien
        ((TaskEdit) screen).show();
    }
//Functie die aangeroepen wordt bij het indrukken van de taak voor iedereen button
    public void onButton_2Click() {
        //Zet variabele als een task-edit scherm.
        EditorScreen screen = screens.create(TaskEditForAll.class);
        //Zet de data in het scherm op een nieuwe instance van de taak
        screen.setEntityToEdit(new Task());

        ((TaskEditForAll) screen).addAfterCloseListener(afterCloseEvent -> reloadData());
        //Laat het scherm zien
        ((TaskEditForAll) screen).show();
    }
}

You can implement this using labelFunction of valueAxis:

    <chart:valueAxis type="DATE">
        <chart:labelFunction>
            <![CDATA[
                function(valueText, value, valueAxis) {
                  return valueText + AmCharts.formatDate(value, ' (W)');
                }
            ]]>
        </chart:labelFunction>
    </chart:valueAxis>

Complete Gantt chart definition:

<chart:ganttChart id="ganttChart"
                  caption="Gantt Chart"
                  additionalSegmentFields="task"
                  balloonDateFormat="JJ:NN"
                  brightnessStep="7"
                  categoryField="category"
                  colorField="color"
                  columnWidth="0.5"
                  dataContainer="taskSpansDc"
                  endDateField="end"
                  height="100%"
                  marginRight="70"
                  period="DAYS"
                  rotate="true"
                  segmentsField="segments"
                  startDateField="start"
                  width="100%">

    <chart:graph balloonText="&lt;strong&gt;[[task]]&lt;/strong&gt;: [[open]] - [[value]]"
                 fillAlphas="1"
                 lineAlpha="1"
                 lineColor="WHITE"/>
    <chart:valueAxis type="DATE">
        <chart:labelFunction>
            <![CDATA[
                function(valueText, value, valueAxis) {
                  return valueText + AmCharts.formatDate(value, ' (W)');
                }
            ]]>
        </chart:labelFunction>
    </chart:valueAxis>
    <chart:valueScrollbar autoGridCount="true"
                          color="BLACK"/>
    <chart:chartCursor cursorAlpha="0"
                       cursorColor="#55bb76"
                       fullWidth="true"
                       valueLineAlpha="0.5"
                       valueLineEnabled="true"
                       valueZoomable="true"
                       zoomable="false"/>
</chart:ganttChart>

Code inside of labelFunction is JavaScript function that will be executed in web browser. There we can use valueText and format date using AmCharts.formatDate utility method.

Result will be:

See the complete demo on Github:

2 Likes

I’ll be trying this out right away. Thanks a lot @artamonov for the incredible response time. :smile:

By the way, your code could be simplified.

Instead of:

        //Zet variabele als een task-edit scherm.
        EditorScreen screen = screens.create(TaskEditForAll.class);
        //Zet de data in het scherm op een nieuwe instance van de taak
        screen.setEntityToEdit(new Task());

        ((TaskEditForAll) screen).addAfterCloseListener(afterCloseEvent -> reloadData());
        //Laat het scherm zien
        ((TaskEditForAll) screen).show();

Use this:

@Inject
private ScreenBuilders screenBuilders;
...
screenBuilders.editor(Colour.class, this)
        .newEntity() // or pass metadata.create(Colour.class) if needed
        .withScreenClass(ColourEditor.class)
        .withAfterCloseListener(e -> {
            // reload data
        })
        .show();

No casts, safe and fluent API. See also: Opening Screens - CUBA Platform. Developer’s Manual