v7.3.0
SupportExamplesFree Trial

Resource utilization

ResourceUtilization is a view showing the utilization levels of the project resources. By default the resources are displayed in a summary list where each row can be expanded to show the events assigned for the resource.

const resourceUtilization = new ResourceUtilization({ project : { transport : { load : { url : 'data/SchedulerPro/examples/view/ResourceUtilization.json' } }, autoLoad : true }, columns : [ { type : 'tree', text : 'Name', field : 'name', width : 150 } ], startDate : new Date(2020, 3, 26), endDate : new Date(2020, 4, 15), appendTo : targetElement, rowHeight : 40, tickSize : 40, minHeight : '20em', // display tooltip showBarTip : true });

The view data

Values series

The component subclasses resource histogram, but unlike the histogram which visualizes two series effort (resource working time spent) and maxEffort (maximum time the resource can work), this view visualizes effort series only.

A resource row and tick cell displays the resource working time spent text and highlights background based on to the resource allocation level: green if it's fully occupied, light-green if it's underallocated and red if it's overallocated. The values of the series are calculated based on the resource assignments.

The view store and model

The view store is an instance of the ResourceUtilizationStore class. The component does not need a store to be provided. It builds it automatically and just needs a project to be provided.

new ResourceUtilization({
    project : new ProjectModel({
        loadUrl : 'https://some.cool.url'
    }),
    ...
});

The store implements a two level hierarchy, having resources on the root level with resource assignments as child records. Yet it does not contain real resource and assignment records, but creates new records wrapping actual resources and assignments. ResourceUtilizationModel is the class implementing that "wrapping". So all the store records are instances of that class.

In order to access actual resources and assignments each record has an origin property:

// get view store record
const record = resourceUtilization.store.getById(123);
// get the actual record it wraps
const actualRecord = record.origin;

Another way to access an "actual" record is using the resolveRecordToOrigin method:

// get view store record
const record = resourceUtilization.store.getById(123);
// get actual record
const actualRecord = resourceUtilization.resolveRecordToOrigin(record);

The benefit of the method is it also resolves links which are used when the store is grouped with the TreeGroup feature.

Data calculations under the hood

The component uses the Engine to calculate resources allocation data. It creates ResourceAllocationInfo class instances for all displayed resources. The class has a few input properties: resource, ticks, includeInactiveEvents and an output allocation property that provides the allocation calculated for the provided input values. The allocation has a total property referencing an array of the resource allocation values and byAssignments which is a Map with the allocation categorized for individual assignments. This view renders allocation.total values for rows representing resources and allocation.total values for assignment rows.

The class instances are added into the Engine graph and they get calculated automatically as soon as any related values change (like a calendar, assignment, event or any other parameter involved in the resource allocation calculation). After the change happens the component updates the involved resources histogram data which in turn causes the resource rows to refresh.

Series values and zooming

The displayed values are calculated for ticks of the time axis and thus naturally depend on the view zoom level. So if the view ticks represent days the values are collected for days and if it shows quarters they are collected for quarters respectively. That means the component automatically recalculates the displayed values after zooming in/out or changing the visible timespan.

Grouping support

The component uses a fixed two level hierarchy of resources in the root with nested assignments by default, but it also supports the TreeGroup and the Group features, and implements automatic rendering of utilization values for group records. It's done by summing up group members data to their parents.

targetElement.innerHTML = '<p>ResourceUtilization view grouping its assignment records by resource city and resource</p>'; const resourceUtilization = new ResourceUtilization({ project : { resources : [ { id : 1, name : 'Mike', city : 'Stockholm' }, { id : 2, name : 'Dan', city : 'Stockholm' }, { id : 3, name : 'Robert', city : 'Tokyo' } ], events : [ { id : 1, startDate : '2023-03-20', duration : 5, durationUnit : 'd', name : 'Event 1' }, { id : 2, startDate : '2023-03-24', duration : 5, durationUnit : 'd', name : 'Event 2' } ], assignments : [ { event : 1, resource : 1 }, { event : 2, resource : 2 }, { event : 1, resource : 3 }, { event : 2, resource : 1 } ] }, features : { treeGroup : { levels : [ // by city ({ origin }) => origin.isResourceModel ? origin.city : origin.resource.city, // by resource ..if that's an unassigned resource just stop grouping ({ origin }) => origin.isResourceModel ? Store.StopBranch : origin.resource ] } }, columns : [ { type : 'tree', field : 'name', width : 150, renderer({ value }) { // if value has resource model (it's a group row) display the resource name if (value.isResourceModel) { return value.name; } return value; } } ], startDate : new Date(2023, 2, 20), endDate : new Date(2023, 2, 30), appendTo : targetElement, rowHeight : 40, tickSize : 40, minHeight : '20em', // display tooltip showBarTip : true });

Tree grouping complexities

There are a couple of things that add complexity when using TreeGroup feature for this view:

  • All records are just wrappers so need to reach the original record to be able to group by its fields. Here for example we first group by resource city and then by resource. Since the feature groups only leaf records we assume record.origin is an assignment (since the view store has assignments on the second level):
new ResourceUtilization({
    features : {
        treeGroup : {
            levels : [
                // 1st level of grouping is resource city
                ({ origin }) => origin.resource.city,
                // 2nd level is resource
                ({ origin }) => origin.resource
            ]
        }
    },
    ...
});
  • The view store has two level structure with resources in the root and assignments on the second level. But in case a resource has no assignments it won't have any child records and thus will also be passed for grouping. So the above code should take into account such possibility too. Then the code will get more complex:
new ResourceUtilization({
    features : {
        treeGroup : {
            levels : [
                // 1st level of grouping is resource city
                ({ origin }) => {
                    // If record is a resource means it has no assignments ..since this function is called for leaves only.
                    // So further grouping makes no sense for this record - stop it.
                    if (origin.isResourceModel) {
                        return Store.StopBranch;
                    }

                    return origin.resource;
                },
                // 2nd level is assignment resource
                ({ origin }) => origin.resource;
            ]
        }
    },
    ...
});

Disabling automatic aggregation

That automatic aggregation can be turned off by setting the aggregateHistogramDataForGroups config to false:

const histogram = new ResourceUtilization({
    // do not show histograms for groups
    aggregateHistogramDataForGroups : false,
    ...
});

In that case group rows will not be served automatically, and thus won't display any values unless you provide data for them with the approach described in "Customizing series data at runtime" chapter or with help of the dataModelField. The getRecordData config is another way to customize data providing and it's actually already used by the component for that purpose. The component has the config referencing getRecordAllocationData method so you can simply override the method.

Customizing data aggregation

The component uses the following hooks to implement resource allocation data aggregation to parents:

There is also a way to use some other aggregate functions instead of summing effort values. It can be changed by providing the aggregate property to the corresponding series:

new ResourceUtilization({
    series : {
        effort : {
            // display average children effort
            aggregate : 'avg'
        }
    },
    ...
});

Currently the aggregate config supports the following values/operations:

  • sum or add (default) - sum of group member values
  • min - minimum of group member values
  • max - maximum of group member values
  • count - count of group member values (effectively count of the group child records)
  • avg - average of group member values

Changing displayed text

To change the displayed texts, supply a getBarText function. Here for example the provided function displays resources time left instead of allocated time

new ResourceUtilization({
    getBarText(datum) {
        const view = this.owner;

        // get default bar text
        let result = view.getBarTextDefault();

        // For resource records we will display the time left for allocation
        if (result && datum.resource) {

            const unit = view.getBarTextEffortUnit();

            // display the resource available time
            result = view.getEffortText(datum.maxEffort - datum.effort, unit);
        }

        return result;
    },
    ...
});

Please note that the function will be injected into the underlying Histogram component that is used under the hood to render actual bars. So this will refer to the Histogram instance, not this class instance. That's why in the above example this.owner is used to the class instance.

If you want to have full control of the displayed TEXT tag attributes you can use getBarTextDOMConfig function:

new ResourceUtilization({

    getBarTextDOMConfig(domConfig, datum, index, _series, renderData) {
        domConfig.y = '10%';

        return domConfig;
    },

    ...
});

The functions are called as part of a cell rendering and have renderData argument providing the cell render data which allows getting the record being rendered (and some other data like cell element, row etc):

new ResourceUtilization({
    getBarText(datum, index, series, renderData) {
        // do not display texts for TreeGroup built parents
        if (!renderData.record.generatedParent) {
            // default bar text
            return this.owner.getBarTextDefault(...arguments);
        }

        return '';
    },
    ...
});

Configuring bar tooltips

The view has a couple of configs that enables displaying a tooltip when hovering histogram bars:

Here is an example of the tooltip configuration:

new ResourceUtilization({
    // enable bar tooltips showing
    showBarTip : true,

    barTooltipTemplate(tooltipContext) {
        const { datum, record } = tooltipContext;

        // do not display tooltip for record #321
        if (record.id === 321) {
            return '';
        }

        return `<div class="my-tooltip">${datum.effort}</div>`;
    },
    ...
});

Customizing series data at runtime

The view triggers the histogramDataCacheSet event after a resource allocating is calculated. So event listeners can be used for modifying the calculated data or injecting additional series data.

new ResourceUtilization({
    series : {
        // provide an extra series
        someValue : {
            type : 'outline'
        }
    },
    ...
    listeners : {
        histogramDataCacheSet({ data }) {
            // add our extra series value for each entry
            data.allocation.total.forEach(entry => {
                // someValue will have a random [0 .. effort] value
                entry.someValue = Math.floor(Math.random() * entry.effort);
            });
        },
    }
});

Customizing series data and performance

The approach we used in the "Customizing series data at runtime" chapter is the most straightforward, but it might have a major disadvantage - performance. Series data collection code iterates all the component time span intervals, which can be quite expensive. And if you want to change collected tick values you would have to iterate the ticks again. If your calculations are quite complex that could be time consuming and hit the view performance badly.

If that's the case then the only way you have is overriding ResourceAllocationInfo class code. The class has calculateAllocation generator method that is used to calculate allocation property value. So the method code can be overridden and to avoid the above mentioned double iterating it's not enough to call super.calculateAllocation and do some changes after but instead the existing calculateAllocation code should be copied to your class and modified according to your needs.

class MyResourceAllocationInfo extends ResourceAllocationInfo {

    * calculateAllocation() {
        const
            total = [],
            ticksCalendar = yield this.ticks,
            resource = yield this.$.resource,
            includeInactiveEvents = yield this.$.includeInactiveEvents,
            assignments = yield resource.$.assigned,
            calendar = yield resource.$.effectiveCalendar,
            assignmentsByCalendar = new Map(),
            eventRanges = [],
            assignmentTicksData = new Map(),
            byAssignments = new Map();

        ....
    }

}

The overridden class then should be provided to the project resourceAllocationInfoClass config:

const project = new ProjectModel({
    resourceAllocationInfoClass : MyResourceAllocationInfo,
    ...
});

new ResourceUtilization({
    project,
    ....
});

Affecting individual rows rendering

Individual rows allocation rendering can be adjusted at runtime. There is a beforeRenderHistogramRow event fired before a row is rendered. The event data includes a histogramConfig configuration object that will be applied to the underlying Histogram widget used for rendering bars. The event allows changing the config in order to make changes to the displayed data, add or hide some series:

new ResourceUtilization({
    ...
    listeners : {
        beforeRenderHistogramRow(renderData) {
            const { record, histogramConfig } = renderData;

            // hide allocation for record #1
            if (record.id == 1) {
                histogramConfig.series.effort = false;
            }
        }
    }
});

Back to the histogram look and feel

The component subclasses resource histogram and adjusts some of its configs to represent the data in a bit different way.

Here is some view changes it applies (besides of the above mentioned using a different auto-generated store):

  • It stretches effort bars to take the whole row height since it relies on text representation of effort values.
  • It disables scale column automatic adding which makes no sense if we have the bars stretched.
  • it also disables resource maxEffort line showing which also makes no sense if bar heights do not express values.

Switching back to a histogram alike look and feel is possible with little help of the scaleColumn, showMaxEffort and series configs:

new ResourceUtilization({
    // add the scale column back
    scaleColumn   : {},
    // display max working time line
    showMaxEffort : true,
    series        : {
        effort : {
            // do not stretch bars ..let them represents values
            stretch : false
        }
    },
    ...
});

The above snippet will make it almost work, but displaying the max working time line for assignment rows will throw exceptions since maxEffort values are not collected for them. This can be fixed with the approach shown in the "Affecting individual rows rendering" chapter. We just need to disable maxEffort series showing for assignment rows:

new ResourceUtilization({
    // add the scale column back
    scaleColumn   : {},
    // display max working time line
    showMaxEffort : true,
    series        : {
        effort : {
            // do not stretch bars ..let them represent values
            stretch : false
        }
    },

    listeners : {
        beforeRenderHistogramRow(renderData) {
            const { source, record, histogramConfig } = renderData;

            // do not show max working time line for assignment rows
            if (source.resolveRecordToOrigin(record).isAssignmentModel) {
                histogramConfig.series.maxEffort = false;
            }
        }
    },
    ...
});

Styling bars

Since the component subclasses resource histogram row bars are rendered with SVG tags. Each bar is rendered as a separate RECT element.

By default bar elements are decorated with b-series-* CSS-class where * matches the identifier of the series. For this view it means RECT elements are decorated with b-series-effort class. Additionally the elements are indicated with b-underallocated and b-overallocated CSS classes in case the corresponding resource is underallocated or overallocated respectively.

Then changing the component default styling can be done with the following basic CSS:

/* normally allocated bar color */
.b-resource-utilization rect.b-series-effort {
    fill: #0f0;
}
/* overallocated mouse hovered bar color */
.b-resource-utilization rect.b-series-effort:hover {
    fill: #9f9;
}
/* underallocated bar color */
.b-resource-utilization rect.b-series-effort.b-underallocated {
    fill: #ff0;
}
/* overallocated mouse hovered bar color */
.b-resource-utilization rect.b-series-effort.b-underallocated:hover {
    fill: #ff9;
}
/* overallocated bar color */
.b-resource-utilization rect.b-series-effort.b-overallocated {
    fill: #f00;
}
/* overallocated mouse hovered bar color */
.b-resource-utilization rect.b-series-effort.b-overallocated:hover {
    fill: #f99;
}

Since the view rows out of the box represent either an assignment or a resource then it decorates rows with b-resource-row and b-assignment-row class respectively to allow styling only certain row types:

/* override assignment row styling */
.b-resource-utilization .b-grid-row.b-assignment-row rect.b-series-effort {
    fill: purple;
}

And if those CSS classes are not enough there is the getBarClass function that can be used to return CSS classes for a bar. For example in the following snippet we decorate effort bars having 8 hours working time allocated with eight-hours-effort CSS class:

new ResourceUtilization({
    getBarClass(series, _rectConfig, datum) {
        // indicate bars entries having 8 hrs effort with "eight-hours-effort" CSS class
        // values are expressed in milliseconds so we convert 8hrs to milliseconds here
        if (datum.effort === 8*3600000) {
            return 'eight-hours-effort';
        }

        return '';
    },
    ...
})

Or if you want to have full control of the displayed RECT element attributes you can use getBarDOMConfig function. Here is for example we add horizontal margins to the displayed bars by adjusting RECT elements width and x attributes:

new ResourceUtilization({

    getBarDOMConfig(series, domConfig, datum, index) {
        // let's add a 10% of width margin to the left & right of the cell
        const xMargin = 0.1 * domConfig.width;

        // reduce bar width respectively
        domConfig.width -= xMargin;

        // adjust x-coordinate by the margin size
        domConfig.x += xMargin;

        // return the edited domConfig (it's important)
        return domConfig;
    },

    ...
});

Contents