<NME> svg-chart.ts
<BEF> import * as svg from "../helpers/svg";
import { requestTypeToCssClass } from "../transformers/styling-converters";
import { Context, ContextCore } from "../typing/context";
import { ChartRenderOption } from "../typing/options";
import { RectData } from "../typing/rect-data";
import { HoverEvtListeners } from "../typing/svg-alignment-helpers";
import {HoverEvtListeners} from "../typing/svg-alignment-helpers";
import {Mark} from "../typing/waterfall";
import {WaterfallData, WaterfallEntry} from "../typing/waterfall";
import PubSub from "./details-overlay/overlay-changes-pub-sub";
import OverlayManager from "./details-overlay/svg-details-overlay-manager";
import * as indicators from "./row/svg-indicators";
import * as row from "./row/svg-row";
import * as alignmentHelper from  "./sub-components/svg-alignment-helper";
import * as svgMarks from "./sub-components/svg-marks";

/**
 * Get a string that's as wide, or wider than any number from 0-n.
 * @param {number} n the highest number that should fit within the returned string's width.
 * @returns {string}
 */
function getWidestDigitString(n: number): string {
  const numDigits = Math.floor((Math.log(n) / Math.LN10)) + 1;
  let s = "";
  for (let i = 0; i < numDigits; i++) {
    // No number should take more horizontal space than "0" does.
    s += "0";
  }
  return s;
}

/**
 * Calculate the height of the SVG chart in px
 * @param {Mark[]}       marks      [description]
 * @param  {number} diagramHeight
 * @returns Number
 */
function getSvgHeight(marks: Mark[], diagramHeight: number): number {
  const maxMarkTextLength = marks.reduce((currMax: number, currValue: Mark) => {
    const attributes = { x: 0, y: 0 };
    return Math.max(currMax, svg.getNodeTextWidth(svg.newTextEl(currValue.name, attributes), true));
  }, 0);

  return Math.floor(diagramHeight + maxMarkTextLength + 35);
}

/**
 * Intitializes the context object
 * @param {WaterfallData} data - Object containing the setup parameter
 * @param {ChartOptions} options - Chart config/customization options
 * @param {WaterfallEntry[]} entriesToShow - Filtered array of entries that will be rendered
 * @return {Context} Context object
 */
function createContext(data: WaterfallData, options: ChartRenderOption,
                       entriesToShow: WaterfallEntry[]): Context {
  const unit = data.durationMs / 100;
  const diagramHeight = (entriesToShow.length + 1) * options.rowHeight;
  const context: ContextCore = {
    diagramHeight,
    options,
    pubSub: new PubSub(),
    unit,
  };
  // `overlayManager` needs the `context` reference, so it's attached later
  return {
    ...context,
    overlayManager: new OverlayManager(context),
  };
}

/**
 * Entry point to start rendering the full waterfall SVG
 * @param {WaterfallData} data - Object containing the setup parameter
 * @param {ChartOptions} options - Chart config/customization options
 * @return {SVGSVGElement} - SVG Element ready to render
 */
export function createWaterfallSvg(data: WaterfallData, options: ChartRenderOption): SVGSVGElement {
  // constants
  const entriesToShow = data.entries
    .filter((entry) => (typeof entry.start === "number" && typeof entry.total === "number"))
    .sort((a, b) => (a.start || 0) - (b.start || 0));

  /** Holder of request-details overlay */
  const overlayHolder = svg.newG("overlays");

  /** Holds all rows */
  const rowHolder = svg.newG("rows-holder");

  const context = createContext(data, options, entriesToShow);

  /** full height of the SVG chart in px */
  const chartHolderHeight = getSvgHeight(data.marks, context.diagramHeight);

  /** Main SVG Element that holds all data */
  const timeLineHolder = svg.newSvg("water-fall-chart", {
    height: chartHolderHeight,
  });

  /** Holder for scale, event and marks */
  const scaleAndMarksHolder = svg.newSvg("scale-and-marks-holder", {
    width: `${100 - options.leftColumnWidth}%`,
    x: `${options.leftColumnWidth}%`,
  });

  /** Holder for on-hover vertical comparison bars */
  let hoverOverlayHolder: SVGGElement | undefined;
  let mouseListeners: HoverEvtListeners;
  if (options.showAlignmentHelpers) {
    hoverOverlayHolder = svg.newG("hover-overlays");
    const hoverEl = svgAlignmentHelper.createAlignmentLines(context.diagramHeight);
    hoverOverlayHolder.appendChild(hoverEl.startline);
    hoverOverlayHolder.appendChild(hoverEl.endline);
    mouseListeners = svgAlignmentHelper.makeHoverEvtListeners(hoverEl);
  }

  // Start appending SVG elements to the holder element (timeLineHolder)

  scaleAndMarksHolder.appendChild(svgGeneralComponents.createTimeScale(context, data.durationMs));
  scaleAndMarksHolder.appendChild(svgMarks.createMarks(context, data.marks));

  // This assumes all icons (mime and indicators) have the same width
  const perIconWidth = entriesToShow[0].responseDetails.icon.width;

  let maxIcons = 0;

  if (options.showMimeTypeIcon) {
    maxIcons += 1;
  }

  if (options.showIndicatorIcons) {
    const iconsPerBlock = entriesToShow.map((entry: WaterfallEntry) =>
      entry.responseDetails.indicators.filter((i) => i.displayType === "icon").length > 0 ? 1 : 0);
    maxIcons += Math.max.apply(null, iconsPerBlock);
  }

  const maxIconsWidth = maxIcons * perIconWidth;

  const widestRequestNumber = getWidestDigitString(entriesToShow.length);
  const maxNumberWidth = svg.getNodeTextWidth(svg.newTextEl(`${widestRequestNumber}`), true);

  const rowItems: SVGAElement[] = [];

  function getChartHeight(): number {
    return chartHolderHeight + context.overlayManager.getCombinedOverlayHeight();
  }

  context.pubSub.subscribeToOverlayChanges(() => {
    const newHeight = getChartHeight();
    timeLineHolder.classList.toggle("closing", newHeight < timeLineHolder.clientHeight);
    timeLineHolder.style.height = `${newHeight}px`;
  });

  /** Renders single row and hooks up behaviour */
  function renderRow(entry: WaterfallEntry, i: number) {
    const entryWidth = entry.total || 1;
    const y = options.rowHeight * i;
    const x = (entry.start || 0.001);
    const detailsHeight = 450;
    const rectData = {
      cssClass: requestTypeToCssClass(entry.responseDetails.requestType),
      height: options.rowHeight,
      hideOverlay: options.showAlignmentHelpers ? mouseListeners.onMouseLeavePartial : undefined,
      label: `<strong>${entry.url}</strong><br/>` +
        `${Math.round(entry.start)}ms - ${Math.round(entry.end)}ms<br/>` +
        `total: ${isNaN(entry.total) ? "n/a " : Math.round(entry.total)}ms`,
      showOverlay: options.showAlignmentHelpers ? mouseListeners.onMouseEnterPartial : undefined,
      unit: context.unit,
      width: entryWidth,
      x,
      y,
    } as RectData;

    const showDetailsOverlay = () => {
    } as RectData;

    let showDetailsOverlay = () => {
      context.overlayManager.openOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);
    };

    let rowItem = row.createRow(context, i, maxIconsWidth, maxNumberWidth, rectData, entry, showDetailsOverlay);
    rowHolder.appendChild(svg.newG("row-overlay-holder"));
  }

  // Main loop to render rows with blocks
  entriesToShow.forEach(renderRow);

  if (options.showAlignmentHelpers && hoverOverlayHolder !== undefined) {
    scaleAndMarksHolder.appendChild(hoverOverlayHolder);
  }
  timeLineHolder.appendChild(scaleAndMarksHolder);
  timeLineHolder.appendChild(rowHolder);
  timeLineHolder.appendChild(overlayHolder);
  timeLineHolder.appendChild(makeTooltip(options));

  return timeLineHolder;
}

<MSG> #88 allow to close details-view by clicking on bar (#140)


<DFF> @@ -7,8 +7,8 @@ import {RectData} from "../typing/rect-data";
 import {HoverEvtListeners} from "../typing/svg-alignment-helpers";
 import {Mark} from "../typing/waterfall";
 import {WaterfallData, WaterfallEntry} from "../typing/waterfall";
-import PubSub from "./details-overlay/overlay-changes-pub-sub";
-import OverlayManager from "./details-overlay/svg-details-overlay-manager";
+import OverlayManager from "./details-overlay/overlay-manager";
+import PubSub from "./details-overlay/pub-sub";
 import * as indicators from "./row/svg-indicators";
 import * as row from "./row/svg-row";
 import * as alignmentHelper from  "./sub-components/svg-alignment-helper";
@@ -174,7 +174,7 @@ export function createWaterfallSvg(data: WaterfallData, options: ChartOptions):
     } as RectData;
 
     let showDetailsOverlay = () => {
-      context.overlayManager.openOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);
+      context.overlayManager.toggleOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);
     };
 
     let rowItem = row.createRow(context, i, maxIconsWidth, maxNumberWidth, rectData, entry, showDetailsOverlay);

{'commit': 'diff --git a/src/ts/typing/context.ts b/src/ts/typing/context.ts\nindex f2d0940..9fec69a 100644\n--- a/src/ts/typing/context.ts\n+++ b/src/ts/typing/context.ts\n@@ -33,6 +33,9 @@ export interface OverlayManagerClass {\n   /** Opens an overlay - rerenders others  */\n   openOverlay: (index: number, y: number, accordionHeight: number, entry: WaterfallEntry,\n                               barEls: SVGGElement[]) => void;\n+  /** toggles an overlay - rerenders others  */\n+  toggleOverlay: (index: number, y: number, accordionHeight: number, entry: WaterfallEntry,\n+                              barEls: SVGGElement[]) => void;\n \n   /** closes on overlay - rerenders others internally */\n   closeOverlay: (index: number, accordionHeight: number, barEls: SVGGElement[]) => void;\ndiff --git a/src/ts/waterfall/details-overlay/svg-details-overlay-manager.ts b/src/ts/waterfall/details-overlay/overlay-manager.ts\nsimilarity index 83%\nrename from src/ts/waterfall/details-overlay/svg-details-overlay-manager.ts\nrename to src/ts/waterfall/details-overlay/overlay-manager.ts\nindex 931eafd..9607436 100644\n--- a/src/ts/waterfall/details-overlay/svg-details-overlay-manager.ts\n+++ b/src/ts/waterfall/details-overlay/overlay-manager.ts\n@@ -1,8 +1,8 @@\n import { removeChildren } from "../../helpers/dom";\n-import {Context, OverlayManagerClass} from "../../typing/context";\n-import {OpenOverlay, OverlayChangeEvent} from "../../typing/open-overlay";\n-import {WaterfallEntry} from "../../typing/waterfall";\n-import {createRowInfoOverlay} from "./svg-details-overlay";\n+import { Context, OverlayManagerClass } from "../../typing/context";\n+import { OpenOverlay, OverlayChangeEvent } from "../../typing/open-overlay";\n+import { WaterfallEntry } from "../../typing/waterfall";\n+import { createRowInfoOverlay } from "./svg-details-overlay";\n \n /** Overlay (popup) instance manager */\n export default class OverlayManager implements OverlayManagerClass {\n@@ -42,11 +42,23 @@ export default class OverlayManager implements OverlayManagerClass {\n     this.context.pubSub.publishToOverlayChanges({\n       "combinedOverlayHeight": self.getCombinedOverlayHeight(),\n       "openOverlays": self.openOverlays,\n-      "type" : "open",\n+      "type": "open",\n     } as OverlayChangeEvent);\n     this.realignBars(barEls);\n   }\n \n+  /**\n+   * Toggles an overlay - rerenders others\n+   */\n+  public toggleOverlay(index: number, y: number, accordionHeight: number,\n+                       entry: WaterfallEntry, barEls: SVGGElement[]) {\n+    if (this.openOverlays.some((o) => o.index === index)) {\n+      this.closeOverlay(index, accordionHeight, barEls);\n+    } else {\n+      this.openOverlay(index, y, accordionHeight, entry, barEls);\n+    }\n+  }\n+\n   /**\n    * closes on overlay - rerenders others internally\n    */\n@@ -60,7 +72,7 @@ export default class OverlayManager implements OverlayManagerClass {\n     this.context.pubSub.publishToOverlayChanges({\n       "combinedOverlayHeight": self.getCombinedOverlayHeight(),\n       "openOverlays": self.openOverlays,\n-      "type" : "closed",\n+      "type": "closed",\n     } as OverlayChangeEvent);\n     this.realignBars(barEls);\n   }\ndiff --git a/src/ts/waterfall/details-overlay/overlay-changes-pub-sub.ts b/src/ts/waterfall/details-overlay/pub-sub.ts\nsimilarity index 100%\nrename from src/ts/waterfall/details-overlay/overlay-changes-pub-sub.ts\nrename to src/ts/waterfall/details-overlay/pub-sub.ts\ndiff --git a/src/ts/waterfall/svg-chart.ts b/src/ts/waterfall/svg-chart.ts\nindex 1c47790..97307ba 100644\n--- a/src/ts/waterfall/svg-chart.ts\n+++ b/src/ts/waterfall/svg-chart.ts\n@@ -7,8 +7,8 @@ import {RectData} from "../typing/rect-data";\n import {HoverEvtListeners} from "../typing/svg-alignment-helpers";\n import {Mark} from "../typing/waterfall";\n import {WaterfallData, WaterfallEntry} from "../typing/waterfall";\n-import PubSub from "./details-overlay/overlay-changes-pub-sub";\n-import OverlayManager from "./details-overlay/svg-details-overlay-manager";\n+import OverlayManager from "./details-overlay/overlay-manager";\n+import PubSub from "./details-overlay/pub-sub";\n import * as indicators from "./row/svg-indicators";\n import * as row from "./row/svg-row";\n import * as alignmentHelper from  "./sub-components/svg-alignment-helper";\n@@ -174,7 +174,7 @@ export function createWaterfallSvg(data: WaterfallData, options: ChartOptions):\n     } as RectData;\n \n     let showDetailsOverlay = () => {\n-      context.overlayManager.openOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);\n+      context.overlayManager.toggleOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);\n     };\n \n     let rowItem = row.createRow(context, i, maxIconsWidth, maxNumberWidth, rectData, entry, showDetailsOverlay);\n', 'commit_subject': '#88 allow to close details-view by clicking on bar (#140)', 'repo_name': 'micmro/PerfCascade', 'committer': {'name': 'Tobias Lidskog', 'email': '059b8b880f8441509ec8a65b50b4c6ae74ebea76@lidskog.se', 'time_sec': '1486416653', 'tz_offset': '60', 'date': {'seconds': '1486416653'}}, 'commit_tree': '23e15ee296c565bbdc42fdec56c1aa80d4ade5e6', 'commit_message': '#88 allow to close details-view by clicking on bar (#140)\n\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -7,8 +7,8 @@ import {RectData} from "../typing/rect-data";\n import {HoverEvtListeners} from "../typing/svg-alignment-helpers";\n import {Mark} from "../typing/waterfall";\n import {WaterfallData, WaterfallEntry} from "../typing/waterfall";\n-import PubSub from "./details-overlay/overlay-changes-pub-sub";\n-import OverlayManager from "./details-overlay/svg-details-overlay-manager";\n+import OverlayManager from "./details-overlay/overlay-manager";\n+import PubSub from "./details-overlay/pub-sub";\n import * as indicators from "./row/svg-indicators";\n import * as row from "./row/svg-row";\n import * as alignmentHelper from  "./sub-components/svg-alignment-helper";\n@@ -174,7 +174,7 @@ export function createWaterfallSvg(data: WaterfallData, options: ChartOptions):\n     } as RectData;\n \n     let showDetailsOverlay = () => {\n-      context.overlayManager.openOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);\n+      context.overlayManager.toggleOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);\n     };\n \n     let rowItem = row.createRow(context, i, maxIconsWidth, maxNumberWidth, rectData, entry, showDetailsOverlay);\n', 'addition_count': 3, 'deletion_count': 3, 'src_file': 'a/src/ts/waterfall/svg-chart.ts', 'tgt_file': 'b/src/ts/waterfall/svg-chart.ts', 'file_extension': '.ts', 'before_file': 'import * as svg from "../helpers/svg";\nimport { requestTypeToCssClass } from "../transformers/styling-converters";\nimport { Context, ContextCore } from "../typing/context";\nimport { ChartRenderOption } from "../typing/options";\nimport { RectData } from "../typing/rect-data";\nimport { HoverEvtListeners } from "../typing/svg-alignment-helpers";\nimport {HoverEvtListeners} from "../typing/svg-alignment-helpers";\nimport {Mark} from "../typing/waterfall";\nimport {WaterfallData, WaterfallEntry} from "../typing/waterfall";\nimport PubSub from "./details-overlay/overlay-changes-pub-sub";\nimport OverlayManager from "./details-overlay/svg-details-overlay-manager";\nimport * as indicators from "./row/svg-indicators";\nimport * as row from "./row/svg-row";\nimport * as alignmentHelper from  "./sub-components/svg-alignment-helper";\nimport * as svgMarks from "./sub-components/svg-marks";\n\n/**\n * Get a string that\'s as wide, or wider than any number from 0-n.\n * @param {number} n the highest number that should fit within the returned string\'s width.\n * @returns {string}\n */\nfunction getWidestDigitString(n: number): string {\n  const numDigits = Math.floor((Math.log(n) / Math.LN10)) + 1;\n  let s = "";\n  for (let i = 0; i < numDigits; i++) {\n    // No number should take more horizontal space than "0" does.\n    s += "0";\n  }\n  return s;\n}\n\n/**\n * Calculate the height of the SVG chart in px\n * @param {Mark[]}       marks      [description]\n * @param  {number} diagramHeight\n * @returns Number\n */\nfunction getSvgHeight(marks: Mark[], diagramHeight: number): number {\n  const maxMarkTextLength = marks.reduce((currMax: number, currValue: Mark) => {\n    const attributes = { x: 0, y: 0 };\n    return Math.max(currMax, svg.getNodeTextWidth(svg.newTextEl(currValue.name, attributes), true));\n  }, 0);\n\n  return Math.floor(diagramHeight + maxMarkTextLength + 35);\n}\n\n/**\n * Intitializes the context object\n * @param {WaterfallData} data - Object containing the setup parameter\n * @param {ChartOptions} options - Chart config/customization options\n * @param {WaterfallEntry[]} entriesToShow - Filtered array of entries that will be rendered\n * @return {Context} Context object\n */\nfunction createContext(data: WaterfallData, options: ChartRenderOption,\n                       entriesToShow: WaterfallEntry[]): Context {\n  const unit = data.durationMs / 100;\n  const diagramHeight = (entriesToShow.length + 1) * options.rowHeight;\n  const context: ContextCore = {\n    diagramHeight,\n    options,\n    pubSub: new PubSub(),\n    unit,\n  };\n  // `overlayManager` needs the `context` reference, so it\'s attached later\n  return {\n    ...context,\n    overlayManager: new OverlayManager(context),\n  };\n}\n\n/**\n * Entry point to start rendering the full waterfall SVG\n * @param {WaterfallData} data - Object containing the setup parameter\n * @param {ChartOptions} options - Chart config/customization options\n * @return {SVGSVGElement} - SVG Element ready to render\n */\nexport function createWaterfallSvg(data: WaterfallData, options: ChartRenderOption): SVGSVGElement {\n  // constants\n  const entriesToShow = data.entries\n    .filter((entry) => (typeof entry.start === "number" && typeof entry.total === "number"))\n    .sort((a, b) => (a.start || 0) - (b.start || 0));\n\n  /** Holder of request-details overlay */\n  const overlayHolder = svg.newG("overlays");\n\n  /** Holds all rows */\n  const rowHolder = svg.newG("rows-holder");\n\n  const context = createContext(data, options, entriesToShow);\n\n  /** full height of the SVG chart in px */\n  const chartHolderHeight = getSvgHeight(data.marks, context.diagramHeight);\n\n  /** Main SVG Element that holds all data */\n  const timeLineHolder = svg.newSvg("water-fall-chart", {\n    height: chartHolderHeight,\n  });\n\n  /** Holder for scale, event and marks */\n  const scaleAndMarksHolder = svg.newSvg("scale-and-marks-holder", {\n    width: `${100 - options.leftColumnWidth}%`,\n    x: `${options.leftColumnWidth}%`,\n  });\n\n  /** Holder for on-hover vertical comparison bars */\n  let hoverOverlayHolder: SVGGElement | undefined;\n  let mouseListeners: HoverEvtListeners;\n  if (options.showAlignmentHelpers) {\n    hoverOverlayHolder = svg.newG("hover-overlays");\n    const hoverEl = svgAlignmentHelper.createAlignmentLines(context.diagramHeight);\n    hoverOverlayHolder.appendChild(hoverEl.startline);\n    hoverOverlayHolder.appendChild(hoverEl.endline);\n    mouseListeners = svgAlignmentHelper.makeHoverEvtListeners(hoverEl);\n  }\n\n  // Start appending SVG elements to the holder element (timeLineHolder)\n\n  scaleAndMarksHolder.appendChild(svgGeneralComponents.createTimeScale(context, data.durationMs));\n  scaleAndMarksHolder.appendChild(svgMarks.createMarks(context, data.marks));\n\n  // This assumes all icons (mime and indicators) have the same width\n  const perIconWidth = entriesToShow[0].responseDetails.icon.width;\n\n  let maxIcons = 0;\n\n  if (options.showMimeTypeIcon) {\n    maxIcons += 1;\n  }\n\n  if (options.showIndicatorIcons) {\n    const iconsPerBlock = entriesToShow.map((entry: WaterfallEntry) =>\n      entry.responseDetails.indicators.filter((i) => i.displayType === "icon").length > 0 ? 1 : 0);\n    maxIcons += Math.max.apply(null, iconsPerBlock);\n  }\n\n  const maxIconsWidth = maxIcons * perIconWidth;\n\n  const widestRequestNumber = getWidestDigitString(entriesToShow.length);\n  const maxNumberWidth = svg.getNodeTextWidth(svg.newTextEl(`${widestRequestNumber}`), true);\n\n  const rowItems: SVGAElement[] = [];\n\n  function getChartHeight(): number {\n    return chartHolderHeight + context.overlayManager.getCombinedOverlayHeight();\n  }\n\n  context.pubSub.subscribeToOverlayChanges(() => {\n    const newHeight = getChartHeight();\n    timeLineHolder.classList.toggle("closing", newHeight < timeLineHolder.clientHeight);\n    timeLineHolder.style.height = `${newHeight}px`;\n  });\n\n  /** Renders single row and hooks up behaviour */\n  function renderRow(entry: WaterfallEntry, i: number) {\n    const entryWidth = entry.total || 1;\n    const y = options.rowHeight * i;\n    const x = (entry.start || 0.001);\n    const detailsHeight = 450;\n    const rectData = {\n      cssClass: requestTypeToCssClass(entry.responseDetails.requestType),\n      height: options.rowHeight,\n      hideOverlay: options.showAlignmentHelpers ? mouseListeners.onMouseLeavePartial : undefined,\n      label: `<strong>${entry.url}</strong><br/>` +\n        `${Math.round(entry.start)}ms - ${Math.round(entry.end)}ms<br/>` +\n        `total: ${isNaN(entry.total) ? "n/a " : Math.round(entry.total)}ms`,\n      showOverlay: options.showAlignmentHelpers ? mouseListeners.onMouseEnterPartial : undefined,\n      unit: context.unit,\n      width: entryWidth,\n      x,\n      y,\n    } as RectData;\n\n    const showDetailsOverlay = () => {\n    } as RectData;\n\n    let showDetailsOverlay = () => {\n      context.overlayManager.openOverlay(i, y + options.rowHeight, accordionHeight, entry, barEls);\n    };\n\n    let rowItem = row.createRow(context, i, maxIconsWidth, maxNumberWidth, rectData, entry, showDetailsOverlay);\n    rowHolder.appendChild(svg.newG("row-overlay-holder"));\n  }\n\n  // Main loop to render rows with blocks\n  entriesToShow.forEach(renderRow);\n\n  if (options.showAlignmentHelpers && hoverOverlayHolder !== undefined) {\n    scaleAndMarksHolder.appendChild(hoverOverlayHolder);\n  }\n  timeLineHolder.appendChild(scaleAndMarksHolder);\n  timeLineHolder.appendChild(rowHolder);\n  timeLineHolder.appendChild(overlayHolder);\n  timeLineHolder.appendChild(makeTooltip(options));\n\n  return timeLineHolder;\n}\n', 'message': '#88 allow to close details-view by clicking on bar (#140)\n\n', 'author_name': 'Tobias Lidskog', 'lang': 'ts'}