//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//!    Use Angular to create time bar and chart with font icons and tooltips.

// This demo uses webpack to manage dependencies and build

import KeyLines from "keylines";

import "core-js";
import "zone.js";

import { OnInit } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { BrowserModule } from "@angular/platform-browser";
import { NgModule, enableProdMode, Component } from "@angular/core";

import type {
  Chart,
  TimeBar,
  TimeBarOptions,
  ChartOptions,
  ChartAllEventProps,
  ChartPointerEventProps,
  SelectionOptions,
  LayoutOptions,
  TimeBarAllEventProps,
} from "keylines";

// import the KeyLines components
import {
  KlComponent,
  KlComponents,
  KlComponentsService,
} from "angular/angular-keylines";
import { LeftPanel, RightPanel } from "angular/angular-demo";

import { chartData, timebarData } from "./angularchartfilter-data";

// enable production mode
enableProdMode();

interface Events {
  name: string;
  args: ChartAllEventProps | TimeBarAllEventProps;
}

export class Tooltip {
  constructor(
    public start: string,
    public end: string,
    public left: string,
    public top: string,
    public tooltipClass: string,
    public arrowClass: string
  ) {}
}

@Component({
  selector: "kl-container",
  template: `
    <div class="chart-wrapper demo-cols">
      <kl-leftPanel class="flex1">
        <span
          #ngTooltip
          id="tooltip"
          class="{{ this.model.tooltipClass }}"
          [style.left]="this.model.left"
          [style.top]="this.model.top"
        >
          <div class="{{ this.model.arrowClass }}"></div>
          <tr>
            <td class="table-cell"><b>Appointed on: </b></td>
            <td>{{ this.model.start }}</td>
          </tr>
          <tr>
            <td class="table-cell"><b>Resigned on: </b></td>
            <td>{{ this.model.end }}</td>
          </tr>
        </span>
        <div id="lhsVisual" class="toggle-content is-visible">
          <kl-components (klReady)="klChartReady($event)">
            <kl-component
              class="flex1"
              klType="chart"
              klContainerClass="klchart klchart-timebar"
              [klOptions]="chartOptions"
              (klEvents)="klChartEvents($event)"
            >
            </kl-component>
            <kl-component
              klType="timebar"
              klContainerClass="kltimebar"
              [klOptions]="timebarOptions"
              (klEvents)="klTimebarEvents($event)"
            >
            </kl-component>
          </kl-components>
        </div>
        <div id="lhsSource" class="toggle-content">
          <iframe
            id="sourceView"
            style="width: 100%; height: 100%; border: none;"
            src="source-angularchartfilter.js.htm"
            scrolling="no"
          ></iframe>
        </div>
      </kl-leftPanel>
      <kl-rightPanel
        name="angularchartfilter"
        title="Angular Integration: Time"
        description="Use Angular and the time bar to filter chart items."
      >
        <fieldset>
          <legend>Interaction</legend>
          <ul>
            <li>Use the time bar controls to see changes over time</li>
            <li>
              Hover/click a link to to show the duration of an individual's
              directorship
            </li>
            <li>
              Multi-select up to three items to highlight their neighbours
            </li>
          </ul>
        </fieldset>
        <fieldset>
          <legend>Data Format</legend>
          <p>
            The time bar data format is similar to that used for the chart. It
            is possible to pass the same data object to both components.
          </p>
        </fieldset>
      </kl-rightPanel>
    </div>
  `,
  styleUrls: ["angularchartfilter.css"],
})
export class ChartFilterComponent implements OnInit {
  private chart?: Chart;
  private timebar?: TimeBar;
  // Define some options for the chart
  public chartOptions: ChartOptions = {
    logo: { u: "/images/Logo.png" },
    overview: { icon: false, shown: false },
    iconFontFamily: "Font Awesome 5 Free",
    handMode: true,
    hover: 0,
    selectedNode: {
      fbc: "rgba(0,0,0,0)",
    },
    selectedLink: {
      fbc: "rgba(0,0,0,0)",
    },
  };
  // Define some options for the timebar
  public timebarOptions: TimeBarOptions = {
    histogram: {
      colour: "#E64669",
      highlightColour: "#DD0031",
      markColour: "rgb(192, 192, 192)",
      markHiColour: "rgb(105, 105, 105)",
    },
  };

  model = new Tooltip("", "", "", "", "hidden", "arrow");
  marks = [
    {
      dt1: new Date("01 Jan 2018 01:00:00"),
      dt2: new Date("01 Jan 2050 01:00:00"),
    },
  ];
  greyColours = ["rgb(105, 105, 105)", "rgb(192, 192, 192)"];

  // only style the first 3 selections
  selectionColours = [
    ["#9600d5", "#d0a3cb"],
    ["rgb(0, 191, 255)", "#8dd8f8"],
    ["rgb(50,205,50)", "#afdaae"],
  ];

  ngOnInit() {
    document.fonts.load('24px "Font Awesome 5 Free"');
  }

  klChartReady([chart, timebar]: [Chart, TimeBar]) {
    this.chart = chart;
    this.timebar = timebar;

    chartData.items.forEach((element) => {
      if (element.type === "node") {
        element.fi = {
          c: this.greyColours[0],
          t: element.d.type === "person" ? "fas fa-user" : "fas fa-building",
        };
      }
    });

    this.chart.load(chartData);
    this.timebar.load(timebarData);
    this.timebar.mark(this.marks);

    this.chart.zoom("fit");
    this.timebar.zoom("fit");
  }

  // will receive events for the timebar
  klTimebarEvents({ name }: Events) {
    if (name === "change") {
      this.onTimebarChange();
    }
  }

  // will receive events for the chart
  klChartEvents({ name, args }: Events) {
    if (name === "selection-change" && this.chart?.selection()) {
      this.highlightSelections();
    } else if (
      name === "hover" ||
      name === "pointer-down" ||
      name === "drag-move"
    ) {
      this.toggleTooltip(args as ChartPointerEventProps);
    }
  }

  getDateFromTimestamp(timestamp: number) {
    return timestamp ? new Date(timestamp).toDateString().slice(4) : "n/a";
  }

  toggleTooltip({ id, x, y }: ChartPointerEventProps) {
    if (this.chart) {
      const item = id ? this.chart.getItem(id) : null;
      const viewWidth = this.chart.viewOptions().width;

      if (item && item.type === "link") {
        const timebarItem = timebarData.items.filter(
          (tbItem) => tbItem.id === item.id
        ) as { dt: { dt1: number; dt2: number }[] }[];
        this.model.start = this.getDateFromTimestamp(timebarItem[0].dt[0].dt1);
        this.model.end = this.getDateFromTimestamp(timebarItem[0].dt[0].dt2);

        // style tooltip to left of cursor if it's x position + width exceeds
        // the remaining chart space.
        if (viewWidth && viewWidth > x + 250) {
          this.model.top = `${y - 24}px`;
          this.model.left = `${x + 22}px`;
          this.model.tooltipClass = "";
        } else {
          this.model.top = `${y - 24}px`;
          this.model.left = `${x - 218}px`;
          this.model.tooltipClass = "right";
        }
      } else {
        this.model.tooltipClass = "hidden";
      }
    }
  }

  buildItemStyling(item: any, width: number, colour: string, border = false) {
    if (item.type === "node") {
      return {
        id: item.id,
        fi: {
          c: colour,
          t: item.d.type === "person" ? "fas fa-user" : "fas fa-building",
        },
        b: border ? colour : null,
      };
    }
    // else it is a link
    return { id: item.id, w: width || 4, c: colour };
  }

  highlightSelections() {
    if (!this.chart || !this.timebar) return;

    this.timebar.selection([]);
    const timebarSelection: SelectionOptions[] = [];
    const chartProperties: Record<
      string,
      ReturnType<typeof this.buildItemStyling>
    > = {};
    // set default colouring
    this.chart.each({ type: "all" }, (item) => {
      chartProperties[item.id] = this.buildItemStyling(
        item,
        3,
        item.type === "node" ? this.greyColours[0] : this.greyColours[1]
      );
    });

    let selectedIds = this.chart.selection();

    if (selectedIds) {
      if (selectedIds.length > this.selectionColours.length) {
        selectedIds = selectedIds.slice(0, this.selectionColours.length);
        this.chart.selection(selectedIds);
      }

      selectedIds.forEach((id, index) => {
        let neighbouringNodes: any = [];
        let links: string[] = [];
        const item = this.chart?.getItem(id);
        const colour =
          index < this.selectionColours.length
            ? this.selectionColours[index][0]
            : this.greyColours[1];
        const linkColour = this.selectionColours[index][1];

        if (item && item.type === "node") {
          // all: true - applies styles to hidden nodes + links not in current time range
          const neighbours = this.chart?.graph().neighbours(id, { all: true });
          if (neighbours && neighbours.links) {
            links = neighbours.links;
            neighbouringNodes = neighbours.nodes;
          }

          // colour node
          chartProperties[id] = this.buildItemStyling(item, 3, colour, true);
          links.forEach((link: string) => {
            const linkItem = this.chart?.getItem(link);
            chartProperties[link] = this.buildItemStyling(
              linkItem,
              5,
              linkColour
            );
          });
        } else if (item && item.type === "link") {
          links.push(id);
          neighbouringNodes.push(item.id1, item.id2);

          // colour link
          chartProperties[id] = this.buildItemStyling(item, 5, colour);
        }
        // colour neighbour nodes
        neighbouringNodes.forEach((neighbourId: string) => {
          if (
            chartProperties[neighbourId].fi.c === this.greyColours[0] &&
            index < this.selectionColours.length
          ) {
            chartProperties[neighbourId] = this.buildItemStyling(
              this.chart?.getItem(neighbourId),
              3,
              this.selectionColours[index][1]
            );
          }
        });
        // and select in the timebar
        timebarSelection.push({
          id: links,
          index,
          c: colour,
        });
      });

      this.timebar.selection(timebarSelection);
      this.chart.setProperties(
        Object.keys(chartProperties).map((key) => chartProperties[key])
      );
    }
  }

  doLayout() {
    if (!this.chart) return;
    const opts: LayoutOptions = {
      time: 500,
      mode: "adaptive",
      easing: "linear",
      straighten: false,
    };
    this.chart.layout("organic", opts);
  }

  onTimebarChange() {
    if (!this.chart || !this.timebar) return;
    // filter the chart to show only items in the new range
    this.chart.filter(this.timebar.inRange, { animate: false, type: "link" });
    // and then adjust the chart's layout
    this.doLayout();
  }
}

@NgModule({
  imports: [BrowserModule],
  declarations: [
    ChartFilterComponent,
    KlComponent,
    KlComponents,
    LeftPanel,
    RightPanel,
  ],
  providers: [KlComponentsService],
  bootstrap: [ChartFilterComponent],
})
export class AppModule {}

platformBrowserDynamic().bootstrapModule(AppModule);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Angular Integration</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/vendor/jquery-ui.js" defer type="text/javascript"></script>
    <script src="/vendor/Reflect.js" defer type="text/javascript"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols"><kl-container id="kl-container" class="flex1 flex">
</kl-container>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
      <!-- This is a bundled JavaScript file.-->
      <!-- Angular conventions are to include scripts after the target DOM node has loaded.-->
      <script src="angularchartfilter.js" crossorigin="use-credentials" defer type="module"></script>
    </div>
  </body>
</html>
#tooltip {
    position: absolute;
    padding: 5px;
    display: block;
    color: black;
    float: left;
    border: 1px solid #ccc;
    background-color: #fff;
    z-index: 1000;
    width: 208px;
}

.arrow {
    position: absolute;
    top: 20px;
    left: -9px;
    border-width: 0px 0px 1px 1px;
    border-style: solid;
    border-color: #ccc;
    background-color: #fff;
    transform: rotateZ(45deg);
    height: 15px;
    width: 15px;
}
  
.right .arrow {
    top: 20px;
    left: 199px;
    transform: rotateZ(225deg);
}

.table-cell {
    text-align: right;
    padding: 5px;
}
//
//     Angular components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

import {
  Component, ElementRef, Input, Output, EventEmitter, Injectable, AfterViewInit, OnDestroy,
  OnChanges, SimpleChange, ContentChildren, ViewChild, NgZone
} from '@angular/core';

import type * as kl from 'keylines';
import KeyLines from 'keylines';

@Injectable()
export class KlComponentsService {
  constructor(private ngZone: NgZone) {}
  create(
    componentDefinitions: kl.Component[],
    pathToImages: string
  ): Promise<(kl.Chart | kl.TimeBar)[]> {
    // KeyLines paths configuration
    KeyLines.paths({ images: pathToImages });
    // KeyLines create components, running the KeyLines create call outside Angular
    // to prevent call of requestAnimationFrame from KeyLines triggering Angular change detection.
    return this.ngZone.runOutsideAngular(() => {
      return KeyLines.create(componentDefinitions);
    });
  }
}

@Component({
  selector: 'kl-component',
  template: '<div #container [ngClass]="containerClass" [ngStyle]="style"></div>'
})
export class KlComponent implements OnChanges, OnDestroy {
  @Input() id: string = ""; //optional

  @Input('ngStyle') style: any; //optional

  @Input('klType') type: "chart" | "timebar" = "chart"; // optional
  @Input('klOptions') options: kl.ChartOptions | kl.TimeBarOptions = {}; // optional

  @Input('klContainerClass') containerClass: string = ""; // optional

  @Output('klReady') klReady = new EventEmitter(); // optional
  @Output('klEvents') klEvents = new EventEmitter(); // optional

  // Save the reference of the container element: see #container in the template
  @ViewChild('container', { static: false })
  private containerElement?: ElementRef;
  // The KeyLines component
  private component?: kl.Chart | kl.TimeBar;

  isChart(component: kl.Chart | kl.TimeBar): component is kl.Chart {
    return this.type === "chart";
  }

  // lifecycle hooks
  ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
    const { options } = changes;
    // Refresh the options when necessary
    if (options && !options.isFirstChange()) {
      this.refreshOptions(options.currentValue);
    }
  }
  ngOnDestroy() {
    if (this.component) {
      // ensure the component cleans up its resources
      this.component.destroy();
    }
  }

  // Kl instructions
  getHeader(): kl.Component {
    return { 
      container: this.containerElement ? this.containerElement.nativeElement : undefined,
      type: this.type,
      options: this.options 
    };
  }

  setUpComponent(component: kl.Chart | kl.TimeBar) {
    // save the reference of the component
    this.component = component;
    // trigger a klReady event with the component reference
    this.klReady.emit(component);
    // attach the component events
    this.registerEvent();
  }

  registerEvent() {
    function emitEvent(this: KlComponent, props: any): void {
      const klEvent = { name: props.name, args: props.event, preventDefault: false };
      // dispatch the event to the parent
      this.klEvents.emit(klEvent);
      if (klEvent.preventDefault && props.event && props.event.preventDefault) {
        props.event.preventDefault();
      }
    }
    if (this.component) {
      if (this.isChart(this.component)) {
        this.component.on('all', emitEvent.bind(this));
      } else {
        this.component.on('all', emitEvent.bind(this));
      }
    }
  }

  refreshOptions(options: kl.ChartOptions | kl.TimeBarOptions) {
    if (this.component) {
      // Use type guard to allow TypeScript to infer type and prevent errors
      if (this.isChart(this.component)) {
        this.component.options(options);
      }
      else {
        this.component.options(options);
      }
    }
  }
}

@Component({
  selector: 'kl-components',
  template: '<ng-content></ng-content>'
})
export class KlComponents implements AfterViewInit {
  @Input('klImagesPath') pathToImages = ''; // optional
  @Output('klReady') klReady = new EventEmitter(); // optional

  // save the KeyLines service
  private KlComponentsService: KlComponentsService;
  // get the list of the children components
  // http://blog.thoughtram.io/angular/2015/09/03/forward-references-in-angular-2.html
  @ContentChildren(KlComponent)
  private components?: KlComponent[];

  // constructor
  constructor(KlComponentsService: KlComponentsService) {
    this.KlComponentsService = KlComponentsService;
  }

  // lifecycle hooks
  ngAfterViewInit() {
    if (!this.components) throw 'Could not find kl-component declaration';
    // iterate over the list of children components to create the KeyLines definition of components
    const componentDefinitions = this.components.map((component) => component.getHeader());
    this.createComponents(componentDefinitions);
  }

  // KL instructions
  createComponents(componentDefinitions: kl.Component[]) {
    // use the KeyLines service to create the components
    this.KlComponentsService.create(componentDefinitions, this.pathToImages)
      .then((components) => this.notifyComponents(components))
      .catch((error: any) => error);
  }

  notifyComponents(components: (kl.Chart | kl.TimeBar)[] | kl.Chart | kl.TimeBar) {
    // ensure that we have an array of components
    if (!Array.isArray(components)) {
      components = [components];
    }
    this.klReady.emit(components);
    // finalise the set up of registered components
    if (this.components){
      this.components.forEach((component, index) => { 
        component.setUpComponent((components as (kl.Chart | kl.TimeBar)[])[index])
      });
    }
  }
}
//
//     Angular demo components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

import { Component, Input, ViewChild, ElementRef } from '@angular/core';

@Component({
  selector: 'kl-leftPanel',
  template: `
    <ng-content></ng-content>`,
})
export class LeftPanel {}
@Component({
  selector: 'kl-rightPanel',
  template: `
    <div class="rhs citext flex flex-column">
      <div class="title-bar">
        <h3 class="flex1">Angular Integration</h3>
        <div class="buttons flex" id="titlebarbuttons">
          <a class="btn btn-label disabled fshide" href="#" data-toggle="tooltip" id="fullscreenDisabled">
            <i class="fa fa-compress"></i>
          </a>
          <a class="btn btn-label" href="#" data-toggle="tooltip" id="fullscreenButton">
            <i class="fa fa-expand"></i>
          </a>
        </div>
      </div>
      <div class="tabsdiv tab" data-tab-group="rhs">
        <a class="btn btn-spaced fshide rhs-toggle fancy-button" id="closeRHS" href="#" data-toggle="tooltip">
          <i class="fa fa-chevron-right"></i>
        </a>
        <button class="tablinks active" name="controlsTab" type="button">Controls</button>
        <button class="tablinks" name="aboutTab" type="button">About</button>
        <button class="tablinks" name="sourceTab" type="button">Source</button>
      </div>
      <div class="tab-content-panel flex1" data-tab-group="rhs">
        <div class="toggle-content is-visible tabcontent" id="controlsTab">
          <div class="cicontent">
            <img class='kl-center' src='images/{{ name }}/angular.png' width='40%' />
            <ng-content></ng-content>
          </div>
        </div>
        <div id="aboutTab" #aboutTab class="toggle-content tabcontent"></div>
        <div id="sourceTab" #sourceTab class="toggle-content tabcontent"></div>
      </div>
    </div>
    `,
})
export class RightPanel {
  @Input() name: string = '';
  @Input() title: string = '';
  @Input() description: string = '';
  @ViewChild('sourceTab', { static: false }) sourceTabEl?: ElementRef;
  @ViewChild('aboutTab', { static: false }) aboutTabEl?: ElementRef;

  ngAfterViewInit() {
    const sourcePanel = document.getElementById('sourcePanel');
    const aboutPanel = document.getElementById('aboutPanel');
    if (sourcePanel && aboutPanel) {
      // re-assign parent of child elements, rather than copy HTML so as to not remove any event handlers
      this.sourceTabEl!.nativeElement.appendChild(sourcePanel);
      for (const child of Array.from(sourcePanel.children)) {
        this.sourceTabEl!.nativeElement.appendChild(child);
      }
      const aboutTemplate = aboutPanel.innerHTML;
      this.aboutTabEl!.nativeElement.innerHTML = aboutTemplate;
      // @ts-ignore
      if (window.setupTabPanel) window.setupTabPanel();
    }
  }
}
Loading source