//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.

//     Sample Code
//!    Use Vue and the time bar to filter chart items.

import KeyLines from "keylines";

import { createApp } from "vue";
import Demo from "./vuechartfilter.vue";

createApp(Demo).mount("#kl-container");
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Vue.js 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">
    <link rel="stylesheet" type="text/css" href="/css/font-awesome.css">
    <link rel="stylesheet" type="text/css" href="../vuechartfilter.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols"><kl-container id="kl-container" class="chart-wrapper demo-cols">
</kl-container>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
      <script src="/vuechartfilter.js" crossorigin="use-credentials" type="module"></script>
    </div>
  </body>
</html>
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.   
//

<template>
  <div class="chart-wrapper demo-cols">
    <left-panel name="vuechartfilter">
      <span id="tooltip" :class="tooltip.tooltipClass"
        :style="{ left: tooltip.left, top: tooltip.top }">
        <div :class="tooltip.arrowClass"></div>
        <tr>
          <td class="table-cell"><b>Appointed on: </b></td>
          <td>{{ tooltip.start }}</td>
        </tr>
        <tr>
          <td class="table-cell"><b>Resigned on: </b></td>
          <td>{{ tooltip.end }}</td>
        </tr>
      </span>
      <kl-chart
        id="kl"
        containerClass="klchart klchart-timebar"
        :animateOnLoad="true"
        :data="data"
        :options="chartOptions"
        @kl-ready="onChartReady"
        @kl-selection-change="highlightSelections"
        @kl-hover="toggleTooltip"
        @kl-drag-move="toggleTooltip"
      />

      <kl-timebar
        id="timebar"
        containerClass="kltimebar"
        :animateOnLoad="true"
        :data="data"
        :options="timebarOptions"
        @kl-change="onTimebarChange"
        @kl-ready="onTimebarReady"
      />
    </left-panel>

    <right-panel
      title="Vue.js Integration"
      name="vuechartfilter"
      description="Use Vue 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 and chart data formats are similar, so you can pass the same data object to both components.
        </p>
      </fieldset>
    </right-panel>
  </div>
</template>

<script>
import KlChart from '../../../../vue/Chart.vue';
import KlTimebar from '../../../../vue/Timebar.vue';
import LeftPanel from '../../../../vue/LeftPanel.vue';
import RightPanel from '../../../../vue/RightPanel.vue';
import KeyLines from "keylines"
import data from '../vuechartfilter-data';
export default {
  name: 'demo',
  components: {
    LeftPanel,
    RightPanel,
    KlChart,
    KlTimebar,
  },
  data: () => ({
    chartOptions: {
      logo: { u: '/images/Logo.png' },
      handMode: true,
      iconFontFamily: 'Font Awesome 5 Free',
      selectionColour: '#35495E',
      hover: 0,
      selectedNode: {
        fbc: 'rgba(0,0,0,0)'
      },
      selectedLink: {
        fbc: 'rgba(0,0,0,0)'
      }
    },
    timebarOptions: {
      histogram: {
        colour: '#42B883',
        highlightColour: '#35495E',
        markColour: 'rgb(192, 192, 192)',
        markHiColour: 'rgb(105, 105, 105)',
      },
      playSpeed: 30,
    },
    greyColours: ['rgb(105, 105, 105)', 'rgb(192, 192, 192)'],
    // only style the first 3 selections
    selectionColours: [
      ['#9600d5', '#d0a3cb'],
      ['#0044ff', '#6b93ff'],
      ['#ff0000', '#ff9e9e'],
    ],
    marks: [
      {
        dt1: new Date('01 Jan 2018 01:00:00'),
        dt2: new Date('01 Jan 2050 01:00:00')
      }
    ],
    data,
    tooltip: {
      start: '',
      end: '',
      left: '',
      top: '',
      tooltipClass: 'hidden',
      arrowClass: 'arrow'
    }
  }),
  mounted() {
    document.fonts.load('24px "Font Awesome 5 Free"'),
    // set font icons for each node type
    this.data.items.forEach((item) => {
      if (item && item.type === 'node') {
        item.fi = {
          c: this.greyColours[0],
          t: item.d.type === 'person' ? 'fas fa-user' : 'fas fa-building',
        };
      }
    })
  },
  methods: {
    onChartReady(chart) {
      this.chart = chart;
    },
    onTimebarReady(timebar) {
      this.timebar = timebar;
      this.timebar.mark(this.marks);
    },
    onTimebarChange() {
      if (this.chart && this.timebar) {
        // filter the chart to show only items in the new range
        this.chart.filter(this.timebar.inRange, { animate: false, type: 'link' }).then(() => {
          this.chart.layout('organic', { time: 500, mode: 'adaptive', easing: 'linear' });
        });
      }
    },
    buildItemStyling(item, width, colour, 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's a link
      return { id: item.id, w: width || 4, c: colour };
    },
    highlightSelections() {
      if (!this.chart || !this.timebar) return;
      const timebarSelection = [];
      const chartProperties = {};
      // 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 = [];
          let links = [];
          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) => {
              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) => {
            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]));
      }
    },
    getDateFromTimestamp(timestamp) {
      return timestamp ? new Date(timestamp).toDateString().slice(4) : 'n/a';
    },
    toggleTooltip({ id, x, y }) {
      if (this.chart) {
        const item = id ? this.chart.getItem(id) : null;
        const viewWidth = this.chart.viewOptions().width;

        if (item && item.type === 'link') {
          const timebarItem = data.items.filter((tbItem) => {
            if (tbItem.id === item.id) {
              return tbItem;
            }
          })
          this.tooltip.start = this.getDateFromTimestamp(timebarItem[0].dt[0].dt1);
          this.tooltip.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.tooltip.top = `${y - 24}px`;
            this.tooltip.left = `${x + 22}px`;
            this.tooltip.tooltipClass = '';
          } else {
            this.tooltip.top = `${y - 24}px`;
            this.tooltip.left = `${x - 218}px`;
            this.tooltip.tooltipClass = 'right'; 
          } 
        } else {
          this.tooltip.tooltipClass = 'hidden';
        }
      }
    }
  },
};
</script>
//
//     Vue components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

<template>
  <div ref="container" :class="containerClass" :style="styleObject"></div>
</template>

<script>
import KeyLines from 'keylines';

export default {
  name: "KlComponent",
  props: {
    id: {
      type: String,
      required: true
    },
    container: Object,
    containerClass: String,
    styleObject: Object,
    options: Object,
    data: Object,
    animateOnLoad: {
      type: Boolean,
      default: false
    },
    selection: {
      type: Array,
      default: () => []
    }
  },
  mounted() {
    KeyLines.create({
      id: this.id,
      options: this.options,
      container: this.$refs ? this.$refs.container : null,
      type: this.type
    }).then(component => {
      this.klcreate(component);
    }).catch(console.err);
  },
  beforeUnmount() {
    if (this.component) {
      this.component.destroy();
    }
  },
  methods: {
    onEvent(props) {
      const name = 'kl-' + props.name;
      this.$emit('kl-all', props);
      this.$emit(name, props.event);
    },
    klcreate(component) {
      this.component = component;
      this.component.on('all', this.onEvent);
      this.component.load(this.data)
        .then(() => this.onLoad({ animate: !!this.animateOnLoad }))
        .then(() => {
          component.selection(this.selection);
          this.$emit('kl-ready', component);
        });
    }
  }
};
</script>
//
//     Vue components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

<script>
import KlComponent from './Keylines.vue';
export default {
  name: 'KlChart',
  extends: KlComponent,
  data: () => ({
    type: 'chart'
  }),
  methods: {
    onLoad(options) {
      return this.component.layout('organic', options);
    }
  }
};
</script>
//
//     Vue components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

<script>
import KlComponent from './Keylines.vue';
export default {
  name: 'KlTimebar',
  extends: KlComponent,
  data: () => ({
    type: 'timebar'
  }),
  methods: {
    onLoad() {
      return this.component.zoom('fit');
    }
  }
};
</script>
//
//     Vue demo components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

<script>
const leftPanelTemplate = `
  <div class="flex1" id="lhs">
    <div id="lhsVisual" style="height: 99%;" class="toggle-content is-visible">
      <slot></slot>
    </div>
    <div id="lhsSource" class="toggle-content"/>
  </div>
`;
export default {
  name: 'LeftPanel',
  props: {
    name: String
  },
  mounted() {
    const sourceCodeElement = document.getElementById('lhsSource');
    const sourceCode = `<iframe id="sourceView" style="width: 100%; height: 100%; border: none;" src="source-${this.name}.js.htm" scrolling="no"></iframe>`;
    sourceCodeElement.innerHTML = sourceCode;
  },
  template: leftPanelTemplate
};
</script>
//
//     Vue demo components KeyLines v8.6.0-11643712880
//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//

<script>
const rightPanelTemplate = `
  <div class="rhs citext flex flex-column">
    <div class="title-bar">
      <h3 class="flex1">${document.title.split('|')[0]}</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 vue-logo" :src="image.src" :alt="image.alt" />
          <slot></slot>
        </div>
      </div>
      <div ref="aboutTab" class="toggle-content tabcontent" id="aboutTab"></div>
      <div ref="sourceTab" class="toggle-content tabcontent" id="sourceTab"></div>
    </div>
  </div>
`;
export default {
  name: 'RightPanel',
  props: {
    title: String,
    name: String,
    description: String,
    isFullscreenAvailable: {
      type: Boolean,
      default: () => typeof doFullScreen === 'function'
    }
  },
  mounted() {
    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.$refs.sourceTab.appendChild(sourcePanel);
      for(const child of Array.from(sourcePanel.children)) {
        this.$refs.sourceTab.appendChild(child);
      }
      const aboutTemplate = aboutPanel.innerHTML;
      this.$refs.aboutTab.innerHTML = aboutTemplate;
    }
  },
  computed: {
    image() {
      return {
        src: `images/${this.name}/vuejs.png`,
        alt: 'Vue.js logo'
      };
    }
  },
  template: rightPanelTemplate
};
</script>
Loading source