Incident Timeline

by kelvinau

HTML

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://demo.kelvinau.net/vis/vis.js"></script>
<div>
  <b>From 2019-02-06 09:00 to 2019-02-16 12:00</b>
</div>
<div id="visualization"></div>

SCSS

$blue : #5dabfb;
$dot-radius: 16px;
$dot-inner-white-circle: 24px;
$dot-inner-blue-circle: 20px;
$tick-size: 20px;

.header {
  font-weight: bold;
}

div.vis-tooltip {
  background-color: white;
}
.vis-timeline {
  overflow: visible;
}

.custom-item {
  &.vis-box {
    border: none;
    background-color: transparent;
    transform: translateY(5px);
    color: #1580ef;
    font-weight: bold;
    // for showing tooltip on dot
/*     height: 170px */;
  }
  &.vis-line {
    display: none;
  }
  &.vis-dot {
    border-width: $dot-radius;
    border-radius: $dot-radius;
    background-color: $blue;
    border-color: $blue;
    &::before {
      content: '';
      position: absolute;
      background: white;
      border-color: white;
      border-width: $dot-inner-white-circle;
      border-radius: $dot-inner-white-circle;
      width: $dot-inner-white-circle;
      height: $dot-inner-white-circle;
      transform: translate(-50%, -50%);
    }
    &::after {
      // Text
      content: '\2714'; // Can change it to \f2c9 after merged
      color: white;
      font-size: $tick-size;    
      line-height: $tick-size;
      text-align: center;
      // Circle
      position: absolute;
      background: $blue;
      border-color: white;
      border-width: $dot-inner-blue-circle;
      border-radius: $dot-inner-blue-circle;
      width: $dot-inner-blue-circle;
      height: $dot-inner-blue-circle;
      transform: translate(-50%, -50%);
    } 
  }
}

JavaScript

const INCIDENT = {
  id: 'IN00000160',
  // Assumption: the cycle is in chronological order
  cycle: [{
      key: 'new',
      content: 'New',
      time: '2019-02-06 09:00',
      PIC: 'mthompson',
      // TODO: does cycle mean accumulation of time?
      cycle: '3',
    },
    {
      key: 'analysis',
      content: 'Analysis',
      time: '2019-02-08 13:00',
      PIC: 'bsabetghadam',
      cycle: '55',
    },
    {
      key: 'response',
      content: 'Response',
      time: '2019-02-11 10:00',
      PIC: 'kau',
      cycle: '100',
    },
    {
      key: 'closed',
      content: 'Closed',
      time: '2019-02-16 12:00',
      PIC: 'scao',
      cycle: '10',
    },
  ]
};

class IncidentLifeCycle {
  constructor(dom, incident, option = {}) {
    this.dom = dom;
    this.incident = incident;
    this.tFactory = new TooltipFactory(incident);
    this.userOption = option;
    this.defaultOption = {
      showCurrentTime: false,
    	tooltip: {
      	followMouse: true,
			},
    };
  }
  init() {
    if (!this.incident.cycle.length) throw 'there are no items in cycle!';

    const startTime = this.incident.cycle[0].time;
    const endTime = this.incident.cycle[this.incident.cycle.length - 1].time;

    const items = this.incident.cycle.map(({
      key,
      content,
      ...item
    }, i) => ({
      id: i,
      content,
      start: item.time,
      title: this.tFactory[key](item),
      className: 'custom-item',
    }));

    const option = Object.assign(this.defaultOption, {
      min: moment(startTime).subtract(1, 'day'),
      max: moment(endTime).add(1, 'day'),
    }, this.userOption);

    const dataSet = new vis.DataSet(items);
    /* 		console.log(option); */
    this.timeline = new vis.Timeline(this.dom, dataSet, option);

  }
  destroy() {
    this.timeline.destroy();
    this.dom = null;
  }
}

class TooltipFactory {
  constructor(incident) {
    this.incident = incident;
    this.header = `<div class="header">${incident.id}</div>`;
  }
 ...