---
title: "Angular Timeline Dynamic Resource Sort Example | Mobiscroll"
component: "Timeline"
framework: "Angular"
library_version: "6.1.5"
url: "https://demo.mobiscroll.com/angular/timeline/dynamic-calculation-resource-sort"
---

# Dynamic Resource Sort - Angular Timeline demo

Sort Angular Timeline resources by metrics — header popup ranks trucks by standby time, payload efficiency, or deadhead, with instant reorder on event changes. For Angular 22 and Ionic 8.

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

## Description

For advanced resource sorting and ordering calculations, sorting just by resource properties might not be enough. In those situations you can provide a modal or popup that is invoked either from outside of the calendar or from within the calendar header itself and you can get as sophisticated as you have to.

In this example trucks are listed as resources along with the scheduled tours as events. The sort popup that can be invoked from the header [through templating](https://mobiscroll.com/docs/angular/eventcalendar/api#template-headerTemplate) allows you to perform calculations based on truck parameters and tour events. Once the calculation options are selected, the resulting metrics are displayed in the resources column, dynamically updating to reflect the results using the [resourceTemplate](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) templating option.

Since the tours affect the resource order, on new event creation or reassignment a [snackbar](https://demo.mobiscroll.com/angular/forms/notifications) is shown with a 3 second timer and a "Sort now" button for an instant order update.

## What this demo shows

- A desktop timeline that displays one week at a time, with days arranged horizontally and trucks listed as resources on the left.
- **Header navigation** The month and year label opens date navigation, while the blue previous and next arrows move between weeks.
- **Week view** The timeline shows the selected week from Sunday to Saturday, with the current date highlighted.
- **Resources** Trucks are displayed as separate resource rows, each with model, capacity, and the currently active calculated metric.
- **Sorting panel** In the header the Sort Trucks button opens a custom sort popup that lets users choose which metric controls the resource order.
- **Sort metrics** Resources can be sorted by Standby Time, Payload Efficiency, or Deadhead Time.
- **Sort direction** Users can choose ascending or descending order before applying the new sort.
- **Staged sorting** Sort options are staged until Apply is clicked, while Cancel leaves the current resource order unchanged.
- **Automatic reordering** Resources are reordered from calculated business metrics rather than a fixed resource order.
- **Metric display** The calculated metric is shown directly in each resource row, with a progress bar that reflects the active value.
- **Event cards** Tour events show the tour title and payload value, or an empty-run label when the payload is zero.
- **Date positioning** Events are positioned by their assigned truck and exact date range.
- **Event interaction** Events highlight on hover and show drag and resize handles for moving tours or changing their duration.
- **Event selection** Clicking an event selects and highlights it.
- **Event creation** New events can be created by double-clicking the timeline or by clicking and dragging across a time range.
- **Event changes** When a tour is created, deleted, updated, or reassigned, a snackbar appears with a 3-second timer and a "Sort now" action.
- **Delayed sorting** The snackbar lets users apply the new order immediately or wait for the timed update.
- **Scrolling** The timeline supports horizontal and vertical scrolling for navigating through days and truck rows.

## Best for

- **Fleet dispatching** Scheduling tours across trucks while keeping the resource order aligned with current operating metrics.
- **Logistics planning** Comparing vehicles by standby time, deadhead time, or payload efficiency in a weekly timeline.
- **Transportation operations** Identifying underused, overused, or inefficient trucks without manually scanning every assignment.
- **Asset management** Organizing resources automatically based on calculated performance data.
- **Operational dashboards** Combining scheduled work with resource-level metrics in the same timeline view.
- **Custom resource ordering** Building scheduler interfaces where resource order depends on event data, calculated values, or user-selected KPIs.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/angular/popup/api#opt-cssClass) - option
- [`getEvents`](https://mobiscroll.com/docs/angular/eventcalendar/api#method-getEvents) - method
- [`cssClass`](https://mobiscroll.com/docs/angular/notifications/snackbar#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/angular/notifications/snackbar#opt-display) - option
- [`onClose`](https://mobiscroll.com/docs/angular/notifications/snackbar#event-onClose) - event
- [`navigateToEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#method-navigateToEvent) - method
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`eventOverlap`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-eventOverlap) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`resources`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-resources) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`headerTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-headerTemplate) - template
- [`resourceHeaderTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceHeaderTemplate) - template
- [`resourceTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) - template
- [`timelineEventContentTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-timelineEventContentTemplate) - template
- [`onPageLoading`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onPageLoading) - event
- [`onEventCreated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDeleted) - event
- [`onEventUpdated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventUpdated) - event
- [`display`](https://mobiscroll.com/docs/angular/popup/api#opt-display) - option
- [`anchor`](https://mobiscroll.com/docs/angular/popup/api#opt-anchor) - option
- [`buttons`](https://mobiscroll.com/docs/angular/popup/api#opt-buttons) - option
- [`focusOnClose`](https://mobiscroll.com/docs/angular/popup/api#opt-focusOnClose) - option
- [`showOverlay`](https://mobiscroll.com/docs/angular/popup/api#opt-showOverlay) - option
- [`width`](https://mobiscroll.com/docs/angular/popup/api#opt-width) - option

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { Component, ElementRef, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  MbscCalendarEvent,
  MbscEventcalendar,
  MbscEventcalendarView,
  MbscEventCreatedEvent,
  MbscEventDeletedEvent,
  MbscEventUpdatedEvent,
  MbscModule,
  MbscPageLoadingEvent,
  MbscPopup,
  MbscPopupButton,
  MbscResource,
  Notifications,
  setOptions,
} from '@mobiscroll/angular';


setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

interface MyEvent extends MbscCalendarEvent {
  payload?: number;
}

interface MyResource extends MbscResource {
  standby?: number;
  deadhead?: number;
  payload?: number;
  capacity: number;
  model: string;
  location: string;
}

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  standalone: true,
  imports: [CommonModule, FormsModule, MbscModule],
})
export class AppComponent {
  constructor(private notify: Notifications) {}

  myEvents: MyEvent[] = [
    {
      start: '2026-10-21T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #013 - Miami to Seattle',
      resource: 1,
      color: '#FF9933',
      payload: 25,
      overlap: false,
    },
    {
      start: '2026-10-23T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #014 - Denver to Boston',
      resource: 2,
      color: '#33FFA6',
      payload: 18,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-25T00:00',
      title: 'Tour #015 - Orlando to Austin',
      resource: 3,
      color: '#9933FF',
      payload: 20,
      overlap: false,
    },
    {
      start: '2026-10-23T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #016 - Detroit to Baltimore',
      resource: 4,
      color: '#33A6FF',
      payload: 0,
      overlap: false,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-27T00:00',
      title: 'Tour #017 - Las Vegas to Portland',
      resource: 5,
      color: '#FF5733',
      payload: 20,
      overlap: false,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-27T00:00',
      title: 'Tour #018 - Atlanta to Kansas City',
      resource: 6,
      color: '#33FF99',
      payload: 0,
      overlap: false,
    },
    {
      start: '2026-10-18T11:00',
      end: '2026-10-22T00:00',
      title: 'Tour #018 - Dallas to Atlanta',
      resource: 6,
      color: '#33FF99',
      payload: 14,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-26T00:00',
      title: 'Tour #019 - Charlotte to Indianapolis',
      resource: 7,
      color: '#FF5733',
      payload: 22,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #005 - Dallas to San Francisco',
      resource: 7,
      color: '#FF5733',
      payload: 18,
      overlap: false,
    },
    {
      start: '2026-10-26T00:00',
      end: '2026-10-28T00:00',
      title: 'Tour #001 - New York to Los Angeles',
      resource: 7,
      color: '#FF5733',
      payload: 20,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #009 - San Diego to Dallas',
      resource: 7,
      color: '#FF5733',
      payload: 16,
      overlap: false,
    },
    {
      start: '2026-10-26T00:00',
      end: '2026-10-28T00:00',
      title: 'Tour #006 - Los Angeles to Chicago',
      resource: 8,
      color: '#FF33A6',
      payload: 10,
      overlap: false,
    },
    {
      start: '2026-10-20T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #010 - San Francisco to Los Angeles',
      resource: 8,
      color: '#FF33A6',
      payload: 0,
      overlap: false,
    },
    {
      start: '2026-10-25T00:00',
      end: '2026-10-28T00:00',
      title: 'Tour #007 - Houston to New York',
      resource: 9,
      color: '#33FF57',
      payload: 0,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #003 - Philadelphia to Phoenix',
      resource: 9,
      color: '#33FF57',
      payload: 0,
      overlap: false,
    },
    {
      start: '2026-10-18T00:00',
      end: '2026-10-21T00:00',
      title: 'Tour #028 - Trenton to Philadelphia',
      resource: 9,
      color: '#33FF57',
      payload: 11,
      overlap: false,
    },
    {
      start: '2026-10-18T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #004 - San Antonio to San Diego',
      resource: 10,
      color: '#3357FF',
      payload: 15,
      overlap: false,
    },
    {
      start: '2026-10-25T00:00',
      end: '2026-10-28T00:00',
      title: 'Tour #022 - Cleveland to Cincinnati',
      resource: 10,
      color: '#3357FF',
      payload: 17,
      overlap: false,
    },
    {
      start: '2026-10-18T00:00',
      end: '2026-10-23T00:00',
      title: 'Tour #023 - Boston to Philadelphia',
      resource: 11,
      color: '#FF9933',
      payload: 19,
      overlap: false,
    },
    {
      start: '2026-10-22T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #024 - Las Vegas to San Diego',
      resource: 12,
      color: '#33FF57',
      payload: 28,
      overlap: false,
    },
    {
      start: '2026-10-19T00:00',
      end: '2026-10-22T00:00',
      title: 'Tour #025 - Miami to Charlotte',
      resource: 13,
      color: '#9933FF',
      payload: 26,
      overlap: false,
    },
    {
      start: '2026-10-24T00:00',
      end: '2026-10-27T00:00',
      title: 'Tour #026 - Seattle to Portland',
      resource: 14,
      color: '#33A6FF',
      payload: 12,
      overlap: false,
    },
    {
      start: '2026-10-21T00:00',
      end: '2026-10-24T00:00',
      title: 'Tour #027 - Atlanta to Orlando',
      resource: 15,
      color: '#FF5733',
      payload: 17,
      overlap: false,
    },
  ];

  myResources: MyResource[] = [
    { id: 1, name: 'NY-TRK-1200', capacity: 25, location: 'New York', model: 'Renault Magnum' },
    { id: 2, name: 'LA-TRK-0090', capacity: 18, location: 'Los Angeles', model: 'Mercedes Actros' },
    { id: 3, name: 'CH-TRK-0700', capacity: 22, location: 'Phoenix', model: 'Scania R500' },
    { id: 4, name: 'HO-TRK-0850', capacity: 28, location: 'Houston', model: 'Volvo FH16' },
    { id: 5, name: 'PH-TRK-0900', capacity: 24, location: 'Chicago', model: 'MAN TGX' },
    { id: 6, name: 'PA-TRK-0300', capacity: 15, location: 'Philadelphia', model: 'Renault T High' },
    { id: 8, name: 'SD-TRK-0250', capacity: 21, location: 'San Francisco', model: 'Mercedes Arocs' },
    { id: 9, name: 'DA-TRK-0400', capacity: 20, location: 'Dallas', model: 'DAF XF' },
    { id: 10, name: 'SF-TRK-0550', capacity: 17, location: 'San Diego', model: 'Iveco Stralis' },
    { id: 11, name: 'BO-TRK-1100', capacity: 23, location: 'Boston', model: 'Kenworth T680' },
    { id: 12, name: 'LV-TRK-2200', capacity: 30, location: 'Las Vegas', model: 'Volvo FH16' },
    { id: 13, name: 'MI-TRK-3300', capacity: 26, location: 'Miami', model: 'Peterbilt 579' },
    { id: 14, name: 'SE-TRK-4400', capacity: 16, location: 'Seattle', model: 'Mack Anthem' },
    { id: 15, name: 'AT-TRK-5500', capacity: 19, location: 'Atlanta', model: 'Renault Magnum' },
  ];

  @ViewChild('calendar', { static: false })
  calendar!: MbscEventcalendar;

  @ViewChild('popup', { static: false })
  popup!: MbscPopup;

  @ViewChild('sortButton')
  anchorBtn!: ElementRef;

  popupAnchor!: HTMLButtonElement;
  sortColumn: 'standby' | 'payload' | 'deadhead' | 'name' = 'standby';
  sortDirection: 'asc' | 'desc' = 'asc';
  sortRequests = 0;
  sortedResources: MyResource[] = this.myResources;
  tempEvent!: MyEvent;
  tempSortColumn: 'standby' | 'payload' | 'deadhead' | 'name' = 'standby';
  tempSortDirection: 'asc' | 'desc' = 'asc';
  weekStart!: Date;
  weekEnd!: Date;

  myView: MbscEventcalendarView = {
    timeline: {
      type: 'week',
      resolutionHorizontal: 'day',
    },
  };

  sortColumnLabels: Record<string, string> = {
    standby: 'Standby Time',
    payload: 'Payload Efficiency',
    deadhead: 'Deadhead Time',
  };

  popupButtons: (MbscPopupButton | 'cancel')[] = [
    'cancel',
    {
      text: 'Apply',
      keyCode: 'enter',
      cssClass: 'mbsc-popup-button-primary',
      handler: () => {
        this.popup.close();
        this.notify.toast({ message: 'Resources sorted' });
        this.sortColumn = this.tempSortColumn;
        this.sortDirection = this.tempSortDirection;
        this.sortResources();
      },
    },
  ];

  calcMetrics() {
    const loadedEvents: MyEvent[] = this.calendar ? this.calendar.getEvents() : [];

    this.myResources.forEach((resource) => {
      let standby = 168;
      let deadhead = 0;
      let payload = 0;
      let numberOfTours = 0;

      const resourceEvents = loadedEvents.filter((event) => event.resource === resource.id);

      resourceEvents.forEach((event) => {
        const eventStart = new Date(event.start as Date);
        const eventEnd = new Date(event.end as Date);
        const effectiveStart = eventStart < this.weekStart ? this.weekStart : eventStart;
        const effectiveEnd = eventEnd > this.weekEnd ? this.weekEnd : eventEnd;

        if (effectiveStart < effectiveEnd) {
          standby -= (effectiveEnd.getTime() - effectiveStart.getTime()) / (1000 * 60 * 60);
        }

        if (effectiveStart < effectiveEnd && (!event.payload || event.payload <= 0)) {
          deadhead += (effectiveEnd.getTime() - effectiveStart.getTime()) / (1000 * 60 * 60);
        }

        if (eventEnd > this.weekStart && eventStart < this.weekEnd) {
          numberOfTours++;
          payload += event.payload || 0;
        }
      });

      resource.standby = standby;
      resource.deadhead = deadhead;
      resource.payload = numberOfTours && resource.capacity ? Math.round((payload / numberOfTours / resource.capacity) * 100) : 0;
    });
  }

  sortResources() {
    this.sortedResources = [...this.myResources].sort((resource1, resource2) => {
      let col = this.sortColumn;
      if (resource1[col] === resource2[col]) {
        col = 'name';
      }
      return this.sortDirection === 'asc' ? (resource1[col]! > resource2[col]! ? 1 : -1) : resource1[col]! < resource2[col]! ? 1 : -1;
    });
  }

  delayedSort(event: MbscCalendarEvent) {
    this.sortRequests++;
    this.tempEvent = event;

    this.notify.snackbar({
      animation: 'pop',
      button: { text: 'Sort now' },
      cssClass: 'mds-popup-sort-snackbar',
      display: 'center',
      duration: 3000,
      onClose: () => {
        this.sortRequests--;
        if (this.sortRequests === 0) {
          const event = this.tempEvent;
          const resource = this.myResources.find((r) => r.id === event.resource)!;
          const eventStart = new Date(event.start as Date);
          const navStart = eventStart < this.weekStart ? this.weekStart : eventStart;
          this.sortResources();
          this.calendar.navigateToEvent({ start: navStart, resource: event.resource });
          resource.cssClass = 'mbsc-timeline-parent-hover';
          setTimeout(() => {
            resource.cssClass = '';
            this.myResources = [...this.myResources];
          }, 1000);
        }
      },
    });
  }

  addEventPayload() {
    return {
      payload: Math.floor(Math.random() * (17 - 5 + 1)) + 5,
    };
  }

  handlePopupOpen() {
    this.tempSortColumn = this.sortColumn;
    this.tempSortDirection = this.sortDirection;
    this.popupAnchor = this.anchorBtn.nativeElement;
    this.popup.open();
  }

  handlePageLoading(args: MbscPageLoadingEvent) {
    this.weekStart = args.firstDay;
    this.weekEnd = args.lastDay;
    setTimeout(() => {
      this.calcMetrics();
      this.sortResources();
    });
  }

  handleEventCreated(args: MbscEventCreatedEvent) {
    setTimeout(() => {
      this.calcMetrics();
      this.delayedSort(args.event);
    });
  }

  handleEventDeleted(args: MbscEventDeletedEvent) {
    setTimeout(() => {
      this.calcMetrics();
      this.delayedSort(args.event);
    });
  }

  handleEventUpdated(args: MbscEventUpdatedEvent): void {
    const oldEventStart = new Date(args.oldEvent!.start as string);
    const oldEventEnd = new Date(args.oldEvent!.end as string);
    if (
      +oldEventStart !== +(args.event.start as Date) &&
      +oldEventEnd !== +(args.event.end as Date) &&
      oldEventStart >= this.weekStart &&
      oldEventEnd <= this.weekEnd &&
      (args.event.start as Date) >= this.weekStart &&
      (args.event.end as Date) <= this.weekEnd &&
      args.oldEvent!.resource === args.event.resource
    ) {
      return;
    }
    setTimeout(() => {
      this.calcMetrics();
      this.delayedSort(args.event);
    });
  }

  getBarColor(resource: MbscResource): string {
    const barValue = this.getBarValue(resource);
    return barValue <= 33 ? 'green' : barValue <= 66 ? 'yellow' : 'red';
  }

  getBarValue(resource: MbscResource): number {
    const metricValue = resource[this.sortColumn];
    return this.sortColumn === 'payload' ? metricValue : (metricValue / 168) * 100;
  }
}
```

### Template

```html
<mbsc-eventcalendar
  #calendar
  class="mds-popup-sort"
  [clickToCreate]="true"
  [data]="myEvents"
  [dragToCreate]="true"
  [dragToMove]="true"
  [dragToResize]="true"
  [eventOverlap]="false"
  [extendDefaultEvent]="addEventPayload"
  [resources]="sortedResources"
  [view]="myView"
  [headerTemplate]="headerTemplate"
  [resourceHeaderTemplate]="resourceHeaderTemplate"
  [resourceTemplate]="resourceTemplate"
  [timelineEventContentTemplate]="scheduleEventContentTemplate"
  (onPageLoading)="handlePageLoading($event)"
  (onEventCreated)="handleEventCreated($event)"
  (onEventDeleted)="handleEventDeleted($event)"
  (onEventUpdated)="handleEventUpdated($event)"
>
  <ng-template #headerTemplate>
    <mbsc-calendar-prev></mbsc-calendar-prev>
    <mbsc-calendar-next></mbsc-calendar-next>
    <mbsc-calendar-nav></mbsc-calendar-nav>
    <div class="mbsc-flex mbsc-flex-1-1 mbsc-justify-content-end">
      <mbsc-button #sortButton startIcon="bars" variant="flat" (click)="handlePopupOpen()">Sort Trucks </mbsc-button>
    </div>
  </ng-template>

  <ng-template #resourceHeaderTemplate>
    <div class="mds-popup-sort-resource-header">Trucks</div>
  </ng-template>

  <ng-template #resourceTemplate let-resource>
    <div class="mds-popup-sort-resource-cell">
      <strong>{{ resource.name }}</strong>
      <div class="mds-popup-sort-resource-attr">Model: {{ resource.model }}</div>
      <div class="mds-popup-sort-resource-attr">Capacity: {{ resource.capacity }}T</div>
      <div class="mds-popup-sort-resource-attr">
        {{ sortColumnLabels[sortColumn] }}: {{ resource[sortColumn] }}{{ sortColumn === 'payload' ? '%' : 'h' }}
      </div>
      <div class="mds-popup-sort-bar-track">
        <div class="mds-popup-sort-bar mds-popup-sort-bar-{{ getBarColor(resource) }}" [style.width.%]="getBarValue(resource)"></div>
      </div>
    </div>
  </ng-template>

  <ng-template #scheduleEventContentTemplate let-event>
    <div>{{ event.title }}</div>
    <div class="mds-popup-sort-event-attr">Payload: {{ event.original.payload ? event.original.payload + ' T' : 'empty' }}</div>
  </ng-template>
</mbsc-eventcalendar>

<mbsc-popup
  #popup
  display="anchored"
  [anchor]="popupAnchor"
  [buttons]="popupButtons"
  [contentPadding]="false"
  [focusOnClose]="false"
  [focusOnOpen]="false"
  [showOverlay]="false"
  [width]="400"
>
  <div class="mbsc-form-group">
    <div class="mbsc-form-group-title">Metric to calculate and sort by</div>
    <mbsc-radio-group [(ngModel)]="tempSortColumn">
      <mbsc-radio label="Standby Time" description="Time the truck is driven without cargo." value="standby"></mbsc-radio>
      <mbsc-radio
        label="Payload Efficiency"
        description="Truck capacity divided by the average cargo on tours."
        value="payload"
      ></mbsc-radio>
      <mbsc-radio label="Deadhead Time" description="Time when the truck is not on a tour." value="deadhead"></mbsc-radio>
    </mbsc-radio-group>
  </div>
  <div class="mbsc-form-group">
    <div class="mbsc-form-group-title">Sort direction</div>
    <mbsc-segmented-group [(ngModel)]="tempSortDirection">
      <mbsc-segmented value="asc">Ascending</mbsc-segmented>
      <mbsc-segmented value="desc">Descending</mbsc-segmented>
    </mbsc-segmented-group>
  </div>
</mbsc-popup>
```

### CSS

```css
/* Overrides */

.mds-popup-sort .mbsc-timeline-resource-header,
.mds-popup-sort .mbsc-timeline-resource-title {
  padding: 0;
}

.mds-popup-sort .mbsc-timeline-resource-col {
  width: 170px;
}

.mds-popup-sort .mbsc-timeline-row {
  height: 110px;
}

.mds-popup-sort .mbsc-timeline-events {
  top: 20px;
}

/* Resource grid */

.mds-popup-sort-resource-header {
  padding: 0 5px;
  line-height: 25px;
}

.mds-popup-sort-resource-cell {
  padding: 5px;
  line-height: 20px;
}

.mds-popup-sort-resource-attr {
  font-size: 12px;
  font-weight: 400;
}

/* Event template */

.mds-popup-sort-event-attr {
  font-size: 11px;
}

/* Metric bar */

.mds-popup-sort-bar-track {
  background-color: #f0f0f0;
  border-radius: 5px;
  height: 10px;
  width: 150px;
  margin-top: 5px;
  overflow: hidden;
}

.mds-popup-sort-bar {
  height: 100%;
}

.mds-popup-sort-bar-green {
  background-color: #4caf50;
}

.mds-popup-sort-bar-yellow {
  background-color: #ffeb3b;
}

.mds-popup-sort-bar-red {
  background-color: #f44336;
}

.mds-popup-sort-bar-animation {
  animation: mds-popup-sort-fill-bar 1s ease-in-out forwards;
}

/* Snackbar countdown */

.mds-popup-sort-snackbar .mbsc-snackbar-cont {
  border-radius: 4px;
  position: relative;
  overflow: hidden;
}

.mds-popup-sort-snackbar .mbsc-snackbar-cont::before {
  content: '';
  position: absolute;
  left: 0;
  height: 100%;
  width: 100%;
  background-color: rgba(0, 0, 0, 0.5);
  animation: mds-popup-sort-fill-bar 3s linear forwards;
}

.mds-popup-sort-snackbar .mbsc-snackbar-message::after {
  content: 'Sorting in 1 .';
  position: relative;
  z-index: 1;
  animation: mds-popup-sort-message 3s steps(3) forwards;
}

/* Animations */

@keyframes mds-popup-sort-fill-bar {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}

@keyframes mds-popup-sort-message {
  0% {
    content: 'Sorting in 3 ...';
  }
  43% {
    content: 'Sorting in 2 ..';
  }
  76% {
    content: 'Sorting in 1 .';
  }
}
```

## Implementation notes

- Use `timeline: { type: 'week', resolutionHorizontal: 'day' }` - a week view with a column per day.
- Define 14 truck resources, each with `id`, `name`, `capacity` (tons), `location`, and `model`. Resources also carry computed fields (`standby`, `deadhead`, `payload`) that are recalculated after every event change.
- Add 23 tour events with `dyndatetime` offsets around today. Each event carries a `payload` property (tons carried; `0` = empty/deadhead run) and a `color`. Enable all interactions: `clickToCreate: true`, `dragToCreate: true`, `dragToMove: true`, `dragToResize: true`, `eventOverlap: false`. When a new event is created, assign it a random payload (5-17 tons) in `onEventCreated`.
- **Three metrics**, all calculated per resource against the currently visible week (`weekStart` / `weekEnd` captured from `onPageLoading`'s `args.firstDay` / `args.lastDay`):
  - **Standby time** (hours): `168` minus the total hours the truck is on tour within the week. Clamp event start/end to week boundaries before computing overlap.
  - **Deadhead time** (hours): hours within the week where the truck is on a tour with `payload === 0` (empty runs).
  - **Payload efficiency** (%): `Math.round((sum of tour payloads / number of tours / truck capacity) * 100)`. Returns 0 if no tours.
- **Sorting**: Sort the resource array by the active `sortColumn` (`'standby'`, `'payload'`, or `'deadhead'`) in `sortDirection` (`'asc'` or `'desc'`). Ties are broken by `name`. Default: standby time ascending.
- **Sort popup** (`Popup`, display `'anchored'`, anchored to the "Sort Trucks" button): Contains a `RadioGroup` with three metric options and a `SegmentedGroup` for ascending/descending. Changes are staged in temporary state until "Apply" is clicked - Apply commits the selection, closes the popup, and shows a `Toast` "Resources sorted". Cancel discards changes.
- **Snackbar on event changes**: After any event created/deleted/updated that affects the metric, open a `Snackbar` (3-second auto-dismiss, "Sort now" button, `display: 'center'`). When the snackbar closes (either by timer or the button), sort the resources, navigate the calendar to the affected event using `calRef.current.navigateToEvent(...)`, and briefly apply `cssClass: 'mbsc-timeline-parent-hover'` to the resource row for a highlight animation (clear after 1 second). Increment a `snackbarKey` on each new event change to restart the snackbar timer from zero.
- **`onPageLoading`**: Capture `firstDay`/`lastDay`, then recalculate metrics and sort in a `setTimeout`.
- **Custom resource column header** (`renderResourceHeader` / `resourceHeaderTemplate` / `resourceHeader` slot): Static "Trucks" label.
- **Custom resource row** (`renderResource` / `resourceTemplate` / `resource` slot): Shows truck name (bold), model, capacity in tons, then the currently active metric label + value + unit (`h` for time-based metrics, `%` for payload). Below that, render a colored progress bar: green if ≤ 33%, yellow if ≤ 66%, red if > 66%. For payload efficiency the bar value equals the metric percentage directly; for standby/deadhead, compute `(metricValue / 168) * 100`. Apply a brief CSS animation class on the bar immediately after a sort.
- **Custom event content** (`renderTimelineEventContent`, Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent`): Shows the tour title and a payload line - `"X T"` if payload > 0, `"empty"` otherwise.
- **Custom header** (`renderHeader` / `headerTemplate` / `header` slot): `CalendarPrev`, `CalendarNext`, `CalendarNav`, then a "Sort Trucks" `Button` with a bars icon aligned to the right.

## 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-angular.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 angular
   ```

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

   ```
   npm install
   ```

6. Run the project:

   ```
   ng serve
   ```

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

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

## Related demos

- Previous: [Resource Drag-and-Drop Reorder](https://demo.mobiscroll.com/angular/timeline/resource-drag-drop-reorder.md) - Reorder resources on a Angular Timeline by drag and drop — each row shows a drag handle, and an update callback runs on drop.
- Next: [Resource Properties](https://demo.mobiscroll.com/angular/timeline/resource-data-structure.md) - Define resource properties on the Angular Timeline — set up nested groups, fixed rows, and per-resource drag, resize, and overlap controls.
