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.originis 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:
- aggregateDataEntry - a function called for each child data entry, meant to aggregate the entry values to the corresponding parent entry. The config references the aggregateAllocationEntry method.
- initAggregatedDataEntry - a function that returns a target parent entry to put aggregated values in. The config references the initAggregatedAllocationEntry method.
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:
sumoradd(default) - sum of group member valuesmin- minimum of group member valuesmax- maximum of group member valuescount- 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:
- showBarTip a boolean flag to toggle showing the tooltip on/off
- barTooltipTemplate a function implementing the tooltip template
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
maxEffortline 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;
},
...
});