---
title: "Timeline Resource Group Aggregate Summaries Example | Mobiscroll"
component: "Timeline"
framework: "JavaScript"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/timeline/show-summaries-aggregates-for-resource-groups"
---

# Resource Group Aggregate Summaries - JavaScript Timeline demo

Show aggregate totals in parent resource rows on a JavaScript Timeline — summary values recalculate as bookings are added, changed, or removed.

- [Live demo](https://demo.mobiscroll.com/timeline/show-summaries-aggregates-for-resource-groups)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-javascript/tree/main/src/demos/eventcalendar/timeline/show-summaries-aggregates-for-resource-groups)
- This demo in other frameworks: [jQuery](https://demo.mobiscroll.com/jquery/timeline/show-summaries-aggregates-for-resource-groups.md), [Angular](https://demo.mobiscroll.com/angular/timeline/show-summaries-aggregates-for-resource-groups.md), [React](https://demo.mobiscroll.com/react/timeline/show-summaries-aggregates-for-resource-groups.md), [Vue](https://demo.mobiscroll.com/vue/timeline/show-summaries-aggregates-for-resource-groups.md)

## Description

When scheduling across multiple groups of resources it sometimes is helpful to provide summaries and aggregate calculations on a group level. These dynamically calculated values can be presented as custom templated events in resource parent rows. Whenever new bookings are made, updated or deleted, use the [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated), [`onEventUpdated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdated) and [`onEventDeleted`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDeleted) lifecycle events to update the aggregates for their parent resource. As we are working with variable event heights, make sure to set `eventHeight: 'variable'` which can be configured under the [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view). This is necessary if the actual booked events have a different height than the summary events (for parent resources).

**Variable event height is an experimental feature.** [Learn more about it in the documentation →](https://mobiscroll.com/docs/javascript/eventcalendar/timeline#variable-event-height)

## What this demo shows

- A desktop timeline that combines a weekly date range with hierarchical resources and group-level summary events.
- **Header navigation** The month and year label opens date navigation, the previous and next arrows move between weeks, and the Today button returns to the current date.
- **Week view** The date strip shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Resource hierarchy** The resource column shows expandable parent groups with nested vehicle resources grouped by fleet or location.
- **Resource details** Child resource rows display vehicle metadata such as model, license plate, and vehicle type.
- **Group summaries** Parent resource rows show daily aggregate metrics for all scheduled trips in that group.
- **Summary metrics** Each group summary displays total estimated distance, total fuel consumption, and estimated operating cost.
- **Event cards** Trip events show booking details such as destination or appointment, assigned driver, estimated distance, estimated fuel consumption, and pick-up and drop-off times.
- **Dynamic aggregates** Summary values are recalculated from the scheduled trips in each resource group when events are created, updated, or deleted.
- **Date positioning** Events are positioned by their assigned resource and exact date range.
- **Event interaction** Events highlight on hover.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created with double click on timeline cells.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through time and resources.

## Best for

- **Fleet management** Scheduling vehicles while tracking daily distance, fuel consumption, and estimated operating cost by fleet or location.
- **Transportation planning** Comparing trip assignments across vehicles and resource groups in a timeline layout.
- **Vehicle reservations** Managing bookings for individual vehicles while keeping parent rows available for group-level utilization summaries.
- **Logistics operations** Reviewing scheduled trips alongside aggregate travel metrics for each resource group.
- **Field service scheduling** Assigning vehicles or crews to appointments where travel estimates and operating costs need to stay visible with the schedule.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-themeVariant) - option
- [`getEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-getEvents) - method
- [`setEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setEvents) - method
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-extendDefaultEvent) - option
- [`clickToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-clickToCreate) - option
- [`dragToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToResize) - option
- [`eventOverlap`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-eventOverlap) - option
- [`onPageLoading`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onPageLoading) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventUpdated) - event
- [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventDeleted) - event
- [`min`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-min) - option
- [`max`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-max) - option
- [`renderResource`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`resources`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-resources) - option
- [`data`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-data) - option

## Code

### JS

```js
mobiscroll.setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

function getAggregateEvents(dayEvents) {
  var aggregateEvents = {};
  myResources.forEach(function (resource) {
    for (var date = new Date(firstViewDay); date < lastViewDay; date.setDate(date.getDate() + 1)) {
      aggregateEvents[resource.id + +date] = {
        id: resource.id + +date,
        isSummary: true,
        allDay: true,
        start: new Date(date),
        end: new Date(date),
        consumption: 0,
        distance: 0,
        resource: resource.id,
      };
    }
  });
  dayEvents.forEach(function (event) {
    if (!event.isSummary) {
      var parentResource = event.resource.split('_')[0];
      var dayStart = new Date(event.start).setHours(0, 0, 0, 0);
      if (aggregateEvents[parentResource + dayStart]) {
        aggregateEvents[parentResource + dayStart].consumption += event.consumption;
        aggregateEvents[parentResource + dayStart].distance += event.distance;
      }
    }
  });
  return Object.values(aggregateEvents);
}

function updateCalendarEvents(inst) {
  var dailyEvents = inst.getEvents();
  var updatedSummaries = getAggregateEvents(dailyEvents);
  var updatedEvents = myEvents.concat(updatedSummaries);
  setTimeout(function () {
    inst.setEvents(updatedEvents);
  });
}

var myResources = [
  {
    id: 'orlando',
    name: 'CNL Tower II',
    address: '420 S Orange Ave, Orlando, FL 32801',
    eventCreation: false,
    eventDragBetweenResources: false,
    eventDragInTime: false,
    eventResize: false,
    color: 'transparent',
    children: [
      {
        id: 'orlando_1',
        car: 'Lincoln Continental',
        plate: '51-5375',
        type: 'Sedan',
      },
      {
        id: 'orlando_2',
        car: 'Porsche Panamera',
        plate: '72-2590',
        type: 'Sports sedan',
      },
      {
        id: 'orlando_3',
        car: 'Range Rover Velar',
        plate: '41-7701',
        type: 'Compact SUV',
      },
    ],
  },
  // Showing partial data. Download full source.
];

var myEvents = [
  {
    id: 1,
    allDay: true,
    start: '2026-10-28T06:00',
    end: '2026-10-28T19:00',
    resource: 'orlando_1',
    title: 'Sales meeting with JLL',
    name: 'John C. Johnson',
    distance: 120,
    consumption: 35,
  },
  {
    id: 2,
    allDay: true,
    start: '2026-10-29T03:00',
    end: '2026-10-29T20:00',
    resource: 'orlando_1',
    title: 'Client consultation',
    name: 'Mary K. Adams',
    distance: 95,
    consumption: 30,
  },
  {
    id: 3,
    allDay: true,
    start: '2026-10-31T10:00',
    end: '2026-10-31T17:00',
    resource: 'orlando_1',
    title: 'Project review at XYZ',
    name: 'Robert B. Brown',
    distance: 150,
    consumption: 40,
  },
  {
    id: 4,
    allDay: true,
    start: '2026-11-02T09:00',
    end: '2026-11-02T16:00',
    resource: 'orlando_1',
    title: 'Team building workshop',
    name: 'Susan E. Smith',
    distance: 60,
    consumption: 20,
  },
  // Showing partial data. Download full source.
];

var firstViewDay = null;
var lastViewDay = null;

mobiscroll.eventcalendar('#demo-aggregates', {
  view: {
    timeline: {
      type: 'week',
      eventDisplay: 'fill',
      eventHeight: 'variable',
    },
  },
  extendDefaultEvent: function (args) {
    var d = args.start;
    var start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 9);
    var end = new Date(start.getFullYear(), start.getMonth(), start.getDate(), 18);
    return {
      allDay: true,
      start: start,
      end: end,
      title: 'New event',
      name: 'John Doe',
      distance: 42,
      consumption: 6,
    };
  },
  clickToCreate: true,
  dragToCreate: false,
  dragToMove: true,
  dragToResize: false,
  eventOverlap: false,
  onPageLoading: function (args, inst) {
    firstViewDay = new Date(args.firstDay);
    lastViewDay = new Date(args.lastDay);
    updateCalendarEvents(inst);
  },
  onEventUpdated: function (args, inst) {
    updateCalendarEvents(inst);
  },
  onEventCreated: function (args, inst) {
    var newEvent = args.event;
    myEvents.push(newEvent);
    updateCalendarEvents(inst);
  },
  onEventDeleted: function (args, inst) {
    var deletedEvent = args.event;
    var index = myEvents.indexOf(deletedEvent);
    myEvents.splice(index, 1);
    updateCalendarEvents(inst);
  },
  min: '2026-10-28T00:00',
  max: '2026-11-10T00:00',
  renderResource: function (res) {
    if (res.isParent) {
      return '<div>' + res.name + '</div><div class="mds-aggregates-address">' + res.address + '</div>';
    }
    return (
      '<div>' +
      res.car +
      '</div><div class="mds-aggregates-plate">Plate ' +
      res.plate +
      '</div><div class="mds-aggregates-cartype">' +
      res.type +
      '</div>'
    );
  },
  renderTimelineEventContent: function (event) {
    if (event.currentResource.isParent) {
      return (
        '<div class="mds-aggregates-summary-event mbsc-flex">' +
        '<div class="mds-aggregates-summary-details mbsc-flex mbsc-flex-1-1"><div class="mds-aggregates-label">Total dist. <div class="mds-aggregates-summary-value">' +
        event.original.distance +
        ' mi</div></div><div class="mds-aggregates-label">Total cons. <div class="mds-aggregates-summary-value">' +
        event.original.consumption +
        ' gal</div></div><div class="mds-aggregates-label">Est. cost <div class="mds-aggregates-summary-value">$' +
        Math.round(event.original.consumption * 3.706 || 0) +
        '</div></div></div></div>'
      );
    }
    return (
      '<div class="mds-aggregates-event">' +
      '<div class="mds-aggregates-title">' +
      event.title +
      '</div><div class="mds-aggregates-name">' +
      event.original.name +
      '</div><div class="mds-aggregates-details"><div class="mds-aggregates-label">Est. distance: <span class="mds-aggregates-value">' +
      event.original.distance +
      ' mi</span></div><div class="mds-aggregates-label">Est. consumption: <span class="mds-aggregates-value">' +
      event.original.consumption +
      ' gal</span></div></div><div class="mds-aggregates-label mbsc-flex"><div class="mbsc-flex-1-1">Pick up: <span class="mds-aggregates-value"> ' +
      mobiscroll.formatDate('hh:mm A', new Date(event.original.start)) +
      '</span></div><div> Drop off: <span class="mds-aggregates-value">' +
      mobiscroll.formatDate('hh:mm A', new Date(event.original.end)) +
      '</span></div></div></div>'
    );
  },
  resources: myResources,
  data: myEvents,
});
```

### HTML

```html
<div id="demo-aggregates" class="mds-aggregates"></div>
```

### CSS

```css
.mds-aggregates .mbsc-timeline-column,
.mds-aggregates .mbsc-timeline-header-column {
  width: 14.5em;
}

.mds-aggregates .mbsc-timeline-row-gutter {
  height: 6px;
}

.mds-aggregates .mbsc-timeline-resource-title{
  font-size: 14px;
  font-weight: 600;
}

.mds-aggregates .mbsc-ios .mbsc-schedule-event:after {
  display: none;
}

.mds-aggregates .mbsc-schedule-event-inner {
  height: auto;
  width: 100%;
}

.mbsc-windows-dark .mds-aggregates-summary-event,
.mbsc-ios-dark .mds-aggregates-summary-event,
.mbsc-material-dark .mds-aggregates-summary-event {
  color: #fff;
}

.mds-aggregates-address {
  padding: 8px 0;
  font-size: 11px;
  line-height: 16px;
  font-weight: 600;
}

.mds-aggregates-plate {
  padding: 8px 0;
  font-weight: 600;
}

.mds-aggregates-cartype {
  font-size: 11px;
  font-weight: 600;
}

.mds-aggregates-summary-event {
  color: #000;
}

.mds-aggregates-summary-details {
  white-space: normal;
  justify-content: space-between;
  padding: 8px;
}

.mds-aggregates-summary-label {
  font-size: 11px;
  font-weight: 400;
}

.mds-aggregates-summary-value {
  font-weight: 600;
  font-size: 15px;
  line-height: 24px;
}

.mds-aggregates-event {
  padding: 4px;
}

.mds-aggregates-title {
  white-space: normal;
  line-height: 20px;
  font-size: 14px;
  font-weight: 600;
}

.mds-aggregates-name {
  padding: 4px 0;
  font-size: 12px;
  line-height: 16px;
  font-weight: 600;
}

.mds-aggregates-value {
  font-weight: 600;
}

.mds-aggregates-label {
  font-size: 11px;
  font-weight: 400;
}

.mds-aggregates-details {
  white-space: normal;
  justify-content: space-between;
  padding: 8px 0;
}
```

## Implementation notes

- Use `timeline: { type: 'week', eventDisplay: 'fill', eventHeight: 'variable' }`. `eventHeight: 'variable'` is required because summary events on parent rows and regular events on child rows have different heights.
- Restrict the calendar to a 2-week window centered on today: `min: dyndatetime('y,m,d-7')`, `max: dyndatetime('y,m,d+6')`.
- Define 3 parent resource groups (by city), each with 3 child vehicle resources. Use city-prefixed IDs to establish parent-child relationships (e.g. `orlando`, `orlando_1`, `orlando_2`, `orlando_3`). Parent resources carry `name`, `address`, `color: 'transparent'`, and `eventCreation: false`, `eventDragBetweenResources: false`, `eventDragInTime: false`, `eventResize: false` - they display summary data only and cannot be booked directly. Child resources carry `car`, `plate`, and `type` as custom properties.
- Add ~66 regular events (all `allDay: true`), each assigned to a child resource. Every event carries two custom numeric properties: `distance` (miles) and `consumption` (gallons). Use `dyndatetime` offsets relative to today for dates.
- Enable `clickToCreate: true`, `dragToMove: true`. Set `dragToCreate: false`, `dragToResize: false`, `eventOverlap: false`. Provide `extendDefaultEvent` to supply defaults for newly created events (`title: 'New event'`, `name: 'John Doe'`, `distance: 42`, `consumption: 6`, `allDay: true`).
- **Summary (aggregate) events** - generated programmatically, not from user input:
  - For each visible day and each parent resource, create one summary event object: `{ id: parentId + dayTimestamp, isSummary: true, allDay: true, start: day, end: day, resource: parentId, distance: 0, consumption: 0 }`.
  - Iterate over all non-summary events; for each, derive the parent resource ID by splitting the child resource ID on `_` (e.g. `'orlando_1'.split('_')[0]` → `'orlando'`). Add the event's `distance` and `consumption` to the matching (parent, day) summary object.
  - Estimated cost is computed in the render layer: `Math.round(consumption * 3.706)`.
  - Merge the original events and the generated summaries into a single array and pass it to the calendar's `data` prop.
- **Refresh cycle**: Store original events in a mutable ref (not state). On `onPageLoading`, capture `args.firstDay` / `args.lastDay`, then call the aggregate recalculation in a `setTimeout`. On `onEventCreated`, `onEventUpdated`, and `onEventDeleted`, update the events ref and call the same recalculation.
- **Custom resource column** (`renderResource` / `resourceTemplate` / `resource` slot): check `res.isParent` - if true, render `name` + `address`; otherwise render `car`, licence plate, and vehicle type.
- **Custom event content** (`renderTimelineEventContent` / `timelineEventContentTemplate` / `timeline-event-content` slot): check `event.currentResource.isParent` - if true (summary row), render "Total dist.", "Total cons.", "Est. cost" aggregates; otherwise render the booking title, driver name, estimated distance/consumption, and formatted pick-up / drop-off times using `formatDate('hh:mm A', ...)`.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-javascript.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config javascript
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config javascript --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/javascript/getting-started/installation

## Related demos

- Previous: [Resource Grouping & Hierarchy](https://demo.mobiscroll.com/timeline/resource-grouping-hierarchy.md) - Build a JavaScript resource timeline with collapsible nested groups, pinned resources, and per-group event creation controls.
- Next: [Resource Drag & Drop Reorder](https://demo.mobiscroll.com/timeline/resource-drag-drop-reorder.md) - Reorder resources on the JavaScript Timeline by drag and drop — each row shows a drag handle, and an update callback runs on drop.
