Angular Timeline Resource Group Aggregate Summaries

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, onEventUpdated and 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. 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 →
Get started with this example

Demo

Loading the live demo

Preview in full screen

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

Code

import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import {
  formatDate,
  MbscCalendarEvent,
  MbscEventcalendar,
  MbscEventcalendarOptions,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscModule,
  MbscNewEventData,
  MbscPageLoadingEvent,
  MbscResource,
  setOptions ,
} from '@mobiscroll/angular';

setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
});

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  standalone: true,
  imports: [CommonModule, MbscModule],
})
export class AppComponent {
  calendarOptions: MbscEventcalendarOptions = {
    min: '2026-10-30T00:00',
    max: '2026-11-12T00:00',
    clickToCreate: true,
    dragToCreate: false,
    dragToMove: true,
    dragToResize: false,
    eventOverlap: false,
    view: {
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
        eventHeight: 'variable',
      },
    },
    extendDefaultEvent: (args: MbscNewEventData) => {
      const d = args.start;
      const start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 9);
      const 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,
      };
    },
  };

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

  eventsWithSummaries: MbscCalendarEvent[] = this.myEvents;

  myResources: MbscResource[] = [
    {
      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.
  ];

  firstViewDay: Date = new Date();
  lastViewDay: Date = new Date();

  getAggregateEvents(dayEvents: MbscCalendarEvent[]): MbscCalendarEvent[] {
    const aggregateEvents: Record<string, MbscCalendarEvent> = {};
    this.myResources.forEach((resource) => {
      for (let date = new Date(this.firstViewDay); date <= this.lastViewDay; date.setDate(date.getDate() + 1)) {
        aggregateEvents[(resource.id as string) + +date] = {
          id: (resource.id as string) + +date,
          isSummary: true,
          allDay: true,
          start: new Date(date),
          end: new Date(date),
          consumption: 0,
          distance: 0,
          resource: resource.id,
        };
      }
    });
    dayEvents.forEach((event) => {
      if (!event['isSummary']) {
        const parentResource = (event.resource as string).split('_')[0];
        const dayStart = new Date(event.start as Date).setHours(0, 0, 0, 0); // Check type
        if (aggregateEvents[parentResource + dayStart]) {
          aggregateEvents[parentResource + dayStart]['consumption'] += event['consumption'];
          aggregateEvents[parentResource + dayStart]['distance'] += event['distance'];
        }
      }
    });
    return Object.values(aggregateEvents);
  }

  updateCalendarEvents(inst: MbscEventcalendar): void {
    const dailyEvents = inst.getEvents();
    const updatedSummaries = this.getAggregateEvents(dailyEvents);
    this.eventsWithSummaries = [...this.myEvents, ...updatedSummaries];
  }

  handlePageLoading(args: MbscPageLoadingEvent): void {
    this.firstViewDay = new Date(args.firstDay);
    this.lastViewDay = new Date(args.lastDay);
    this.updateCalendarEvents(args.inst! as MbscEventcalendar);
  }

  handleEventUpdated(args: MbscEventUpdatedEvent): void {
    this.updateCalendarEvents(args.inst! as MbscEventcalendar);
  }

  handleEventCreated(args: MbscEventCreatedEvent): void {
    const newEvent = args.event;
    this.myEvents.push(newEvent);
    this.updateCalendarEvents(args.inst! as MbscEventcalendar);
  }

  handleEventDeleted(args: MbscEventDeletedEvent): void {
    const deletedEvent = args.event;
    const index = this.myEvents.indexOf(deletedEvent);
    this.myEvents.splice(index, 1);
    this.updateCalendarEvents(args.inst! as MbscEventcalendar);
  }

  roundCost(cost: number): number {
    return Math.round(cost);
  }

  formatMyDate(date: Date): string {
    return formatDate('hh:mm A', new Date(date));
  }
}

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.