function TimelineStoryteller()

in src/core/main.ts [55:4188]


function TimelineStoryteller(isServerless, showDemo, parentElement) {
  var instance = this;
  var timeline_vis = configurableTL(globals.unit_width, globals.padding);
  parentElement = parentElement || document.body;
  this.parentElement = parentElement;
  this._timeline_vis = timeline_vis;
  this._loaded = false;
  this.scale = 1;
  this._dispatch = d3.dispatch("stateChanged");
  this.on = this._dispatch.on.bind(this._dispatch);
  this.playback_mode = false;
  this._currentSceneIndex = -1;

  var timelineElement = document.createElement("div");
  timelineElement.className = "timeline_storyteller";
  parentElement.appendChild(timelineElement);

  this._colorPicker = colorPickerPopup(timelineElement);
  this._container =
    selectWithParent()
      .append("div")
      .attr("class", "timeline_storyteller-container");
  this._errorArea = this._container.append("div")
    .attr("class", "timeline_storyteller-error");

  this._component_width = parentElement.clientWidth;
  this._component_height = parentElement.clientHeight;
  this._render_width = this._component_width;
  this._render_height = this._component_height;

  this.options = clone(TimelineStoryteller.DEFAULT_OPTIONS);

  globals.serverless = isServerless;
  // if (typeof isServerless === "undefined" || isServerless === false) {
  //   globals.socket = require("socket.io")({ transports: ["websocket"] });
  // }

  // if (globals.socket) {
  //   globals.socket.on("hello_from_server", function (data) {
  //     log(data);
  //   });
  // }

  /**
   * Creates the import panel
   * @returns {void}
   */
  function createImportPanel() {
    var element = selectWithParent()
      .append("div")
      .attr("id", "import_div")
      .attr("class", "control_div")
      .style("top", "25%");

    var panel = {
      visible: true,
      element: element,
      show: function () {
        panel.visible = true;
        element.style("top", "25%").style("display", "block");
      },
      hide: function () {
        panel.visible = false;
        element.style("top", "-210px");
      }
    };
    return panel;
  }

  function showDemoData() {
    return (typeof showDemo === "undefined" || showDemo) && (<any>window).timeline_story_demo_data !== undefined;
  }

  function showDemoStory() {
    return (typeof showDemo === "undefined" || showDemo) && (<any>window).timeline_story_demo_story !== undefined;
  }

  instance._showDemoStory = showDemoStory;
  instance._showDemoData = showDemoData;

  function adjustSvgSize() {
    main_svg.transition()
      .duration(instance._getAnimationStepDuration())
      .attr("width", d3.max([globals.width, (instance._render_width - globals.margin.left - globals.margin.right - getScrollbarWidth())]))
      .attr("height", d3.max([globals.height, (instance._component_height - globals.margin.top - globals.margin.bottom - getScrollbarWidth())]));
  }

  instance._adjustSvgSize = adjustSvgSize;

  (<any>Date.prototype).stdTimezoneOffset = function () { // eslint-disable-line no-extend-native
    var jan = new Date(this.getFullYear(), 0, 1);
    var jul = new Date(this.getFullYear(), 6, 1);
    return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
  };

  (<any>Date.prototype).dst = function () { // eslint-disable-line no-extend-native
    return this.getTimezoneOffset() < this.stdTimezoneOffset();
  };

  // window.addEventListener("load", function () {
  //   logEvent("Initializing Timeline Storyteller");

  //   if (globals.socket) {
  //     globals.socket.emit("hello_from_client", { hello: "server" });
  //   }

  //   instance._onResized(false);
  // });

  instance._container.on("scroll", function () {
    var axis = instance._container.select(".timeline_axis");
    axis
      .select(".domain")
      .attr("transform", function () {
        return "translate(0," + instance._container.node().scrollTop + ")";
      });

    axis
      .selectAll(".tick text")
      .attr("y", instance._container.node().scrollTop - 6);
  });

  var legendDrag = d3.behavior.drag()
    .origin(function () {
      var t = d3.select(this);

      return {
        x: t.attr("x"),
        y: t.attr("y")
      };
    })
    .on("drag", function () {
      var x_pos = d3.event.x;
      var y_pos = d3.event.y;

      if (x_pos < 0) {
        x_pos = 0;
      } else if (x_pos > (globals.width - globals.margin.right)) {
        x_pos = globals.width - globals.margin.right;
      }

      if (y_pos < 0) {
        y_pos = 0;
      }

      d3.select(this)
        .attr("x", x_pos)
        .attr("y", y_pos);
    })
    .on("dragend", function () {
      globals.legend_x = d3.select(this).attr("x");
      globals.legend_y = d3.select(this).attr("y");

      //logEvent("legend moved to: " + globals.legend_x + ", " + globals.legend_y, "legend");
    });

  var filterDrag = d3.behavior.drag()
    .origin(function () {
      var t = selectWithParent("#filter_div");

      return {
        x: parseInt(t.style("left"), 10),
        y: parseInt(t.style("top"), 10)
      };
    })
    .on("drag", function () {
      var x_pos = d3.event.x;
      var y_pos = d3.event.y;

      if (x_pos < (10 + parseInt(selectWithParent("#menu_div").style("width"), 10) + 10)) {
        x_pos = (10 + parseInt(selectWithParent("#menu_div").style("width"), 10) + 10);
      } else if (x_pos >= globals.effective_filter_width) {
        x_pos = globals.effective_filter_width - 10;
      }

      if (y_pos < (180 + parseInt(selectWithParent("#option_div").style("height"), 10) + 20)) {
        y_pos = (180 + parseInt(selectWithParent("#option_div").style("height"), 10) + 20);
      } else if (y_pos >= globals.effective_filter_height + 155) {
        y_pos = globals.effective_filter_height + 155;
      }

      selectWithParent("#filter_div")
        .style("left", x_pos + "px")
        .style("top", y_pos + "px");
    })
    .on("dragend", function () {
      var filter_x = selectWithParent("#filter_div").style("left");
      var filter_y = selectWithParent("#filter_div").style("top");

      //logEvent("filter options moved to: " + filter_x + ", " + filter_y, "filter");
    });

  /**
  --------------------------------------------------------------------------------------
  KEY PRESS EVENTS
  --------------------------------------------------------------------------------------
  **/

  selectWithParent().on("keydown", function () {
    if (d3.event.keyCode === 76 && d3.event.altKey) {
      // recover legend
      selectWithParent(".legend")
        .transition()
        .duration(instance._getAnimationStepDuration())
        .attr("x", 0)
        .attr("y", 0);

      globals.legend_x = 0;
      globals.legend_y = 0;
    }
    if (d3.event.keyCode === 82 && d3.event.altKey) {
      // recover legend
      if (!instance.playback_mode) {
        instance._recordScene();
      }
    } else if (instance.playback_mode && d3.event.keyCode === 39) {
      goNextScene();
    } else if (instance.playback_mode && d3.event.keyCode === 37) {
      goPreviousScene();
    } else if (d3.event.keyCode === 80 && d3.event.altKey) {
      instance.setPlaybackMode(!instance.playback_mode);
    }
    // else if (d3.event.keyCode === 46 && selectWithParent("#caption_div").style("display") === "none" && instance._addImagePopup.hidden() && !instance.importPanel.visible) {
    //   globals.deleteScene();
    // }
  });

  function goNextScene() {
    if (globals.scenes.length < 2) {
      return;
    } else if (instance._currentSceneIndex < globals.scenes.length - 1) {
      instance._currentSceneIndex++;
    } else {
      instance._currentSceneIndex = 0;
    }
    //logEvent("scene: " + (instance._currentSceneIndex + 1) + " of " + globals.scenes.length, "playback");

    changeScene(instance._currentSceneIndex);
  }

  function goPreviousScene() {
    if (globals.scenes.length < 2) {
      return;
    }
    if (instance._currentSceneIndex > 0) {
      instance._currentSceneIndex--;
    } else {
      instance._currentSceneIndex = globals.scenes.length - 1;
    }
    //logEvent("scene: " + instance._currentSceneIndex + " of " + globals.scenes.length, "playback");

    changeScene(instance._currentSceneIndex);
  }

  // initialize main visualization containers
  var main_svg,
    export_div,
    menu_div,
    filter_div,
    navigation_div;

  gif.on("finished", function (blob) {
    var saveLink = document.createElement("a");
    var downloadSupported = "download" in saveLink;
    if (downloadSupported) {
      saveLink.download = "timeline_story.gif";
      saveLink.href = URL.createObjectURL(blob);
      saveLink.style.display = "none";
      document.querySelector(".timeline_storyteller").appendChild(saveLink);
      saveLink.click();
      document.querySelector(".timeline_storyteller").removeChild(saveLink);
    } else {
      window.open(URL.createObjectURL(blob), "_temp", "menubar=no,toolbar=no,status=no");
    }

    var reader = new window.FileReader();
    let base64data: any = "";
    reader.readAsDataURL(blob);
    reader.onloadend = function () {
      base64data = reader.result;
      var research_copy = {};
      if (!globals.opt_out) {
        research_copy = {
          "timeline_json_data": globals.timeline_json_data,
          "name": "timeline_story.gif",
          "usage_log": globals.usage_log,
          "image": base64data,
          "email_address": globals.email_address,
          "timestamp": new Date().valueOf()
        };
      } else {
        research_copy = {
          "usage_log": globals.usage_log,
          "email_address": globals.email_address,
          "timestamp": new Date().valueOf()
        };
      }
      var research_copy_json = JSON.stringify(research_copy);
      // var research_blob = new Blob([research_copy_json], { type: "application/json" });

      //log(research_copy);

      // if (globals.socket) {
      //   globals.socket.emit("export_event", research_copy_json); // raise an event on the server
      // }
    };

    gif.running = false;
  });

  this.onIntro = true;

  instance.importPanel = createImportPanel();

  export_div = selectWithParent()
    .append("div")
    .attr("id", "export_div")
    .attr("class", "control_div")
    .style("top", -185 + "px");

  menu_div = selectWithParent()
    .append("div")
    .attr("id", "menu_div")
    .attr("class", "control_div");

  var control_panel = instance._control_panel = menu_div.append("g");

  var menuItems = instance.options.menu;
  instance._initializeMenu(menuItems);

  /**
  ---------------------------------------------------------------------------------------
  EXPORT OPTIONS
  ---------------------------------------------------------------------------------------
  **/

  selectWithParent("#export_div").append("input")
    .attr({
      type: "image",
      name: "Hide export panel",
      id: "export_close_btn",
      class: "img_btn_enabled",
      src: imageUrls("close.png"),
      height: 15,
      width: 15,
      title: "Hide export panel"
    })
    .style("margin-top", "5px")
    .on("click", function () {
      selectWithParent("#export_div").style("top", -185 + "px");

      //logEvent("hide export panel", "export");
    });

  export_div.append("div")
    .attr("id", "export_boilerplate")
    .style("height", "120px")
    .html("<span class='boilerplate_title'>Export options</span><hr>" +
      "<span class='disclaimer_text'>By providing an email address you agree that <a title='Microsoft' href='http://microsoft.com'>Microsoft</a> may contact you to request feedback and for user research.<br>" +
      "You may withdraw this consent at any time.</span><hr>");

  var export_formats = export_div.append("div")
    .attr("id", "export_formats");

  export_formats.append("input")
    .attr({
      type: "text",
      placeholder: "email address",
      class: "text_input",
      id: "email_input"
    })
    .on("input", function () {
      var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
      if (re.test(selectWithParent("#email_input").property("value"))) {
        globals.email_address = selectWithParent("#email_input").property("value");
        export_formats.selectAll(".img_btn_disabled")
          .attr("class", "img_btn_enabled");

        //logEvent("valid email address: " + globals.email_address, "export");
      } else {
        export_formats.selectAll(".img_btn_enabled")
          .attr("class", "img_btn_disabled");
      }
    });

  export_formats.append("input")
    .attr({
      type: "image",
      name: "Export PNG",
      class: "img_btn_disabled export--image",
      src: imageUrls("png.png"),
      height: 30,
      width: 30,
      title: "Export PNG"
    })
    .on("click", function () {
      if (globals.opt_out || globals.email_address !== "") {
        selectAllWithParent("foreignObject").remove();

        //logEvent("exporting main_svg as PNG", "export");

        svgImageUtils.saveSvgAsPng(document.querySelector(".timeline_storyteller #main_svg"), "timeline_image.png", { backgroundColor: "white" });
      }
    });

  export_formats.append("input")
    .attr({
      type: "image",
      name: "Export SVG",
      class: "img_btn_disabled export--image",
      src: imageUrls("svg.png"),
      height: 30,
      width: 30,
      title: "Export SVG"
    })
    .on("click", function () {
      if (globals.opt_out || globals.email_address !== "") {
        selectAllWithParent("foreignObject").remove();

        //logEvent("exporting main_svg as SVG", "export");

        svgImageUtils.saveSvg(document.querySelector(".timeline_storyteller #main_svg"), "timeline_image.svg", { backgroundColor: "white" });
      }
    });

  export_formats.append("input")
    .attr({
      type: "image",
      name: "Export animated GIF",
      class: "img_btn_disabled export--image",
      src: imageUrls("gif.png"),
      height: 30,
      width: 30,
      title: "Export animated GIF"
    })
    .on("click", function () {
      if (globals.opt_out || globals.email_address !== "") {
        selectAllWithParent("foreignObject").remove();

        gif.frames = [];
        var gif_scenes = globals.scenes;
        if (gif_scenes.length > 0) {
          //logEvent("exporting story as animated GIF", "export");

          gif_scenes.sort(function (a, b) {
            return parseFloat(a.s_order) - parseFloat(b.s_order);
          });
          gif_scenes.forEach(function (d, i) {
            var img = document.createElement("img");
            img.style.display = "none";
            img.id = "gif_frame" + i;
            img.src = d.s_src;
            document.querySelector(".timeline_storyteller").appendChild(img);
            selectWithParent("#gif_frame" + i).attr("class", "gif_frame");
            setTimeout(function () {
              gif.addFrame(document.getElementById("gif_frame" + i), { delay: 1500 });
            }, 150);
          });
        } else {
          //logEvent("exporting main_svg as GIF", "export");

          svgImageUtils.svgAsPNG(document.querySelector(".timeline_storyteller #main_svg"), -1, { backgroundColor: "white" });

          setTimeout(function () {
            gif.addFrame(document.getElementById("gif_frame-1"));
          }, 150);
        }
        setTimeout(function () {
          gif.render();
          selectAllWithParent(".gif_frame").remove();
        }, 150 + 150 * gif.frames.length);
        gif_scenes = [];
      }
    });

  export_formats.append("input")
    .attr({
      type: "image",
      name: "Export story",
      class: "img_btn_disabled",
      src: imageUrls("story.png"),
      height: 30,
      width: 30,
      title: "Export story"
    })
    .on("click", function () {
      if (globals.opt_out || globals.email_address !== "") {
        selectAllWithParent("foreignObject").remove();

        //logEvent("exporting story as .cdc", "export");

        globals.timeline_story = instance.saveState();

        var story_json = JSON.stringify(globals.timeline_story);
        var blob = new Blob([story_json], { type: "application/json" });
        var url = URL.createObjectURL(blob);

        var a = document.createElement("a");
        a.download = "timeline_story.cdc";
        a.href = url;
        a.textContent = "Download timeline_story.cdc";
        document.querySelector(".timeline_storyteller").appendChild(a);
        a.click();
        document.querySelector(".timeline_storyteller").removeChild(a);

        if (globals.opt_out) {
          globals.timeline_story = {
            "usage_log": globals.usage_log,
            "author": globals.email_address,
            "timestamp": new Date().valueOf()
          };
        }

        story_json = JSON.stringify(globals.timeline_story);

        //log(story_json);

        // if (globals.socket) {
        //   globals.socket.emit("export_event", story_json); // raise an event on the server
        // }
      }
    });

  var out_out_cb = export_formats.append("div")
    .attr("id", "opt_out_div");

  out_out_cb.append("input")
    .attr({
      type: "checkbox",
      name: "opt_out_cb",
      value: globals.opt_out
    })
    .property("checked", false)
    .on("change", function () {
      if (!globals.opt_out) {
        //logEvent("opting out of sharing content", "export");

        globals.opt_out = true;
        export_formats.selectAll(".img_btn_disabled")
          .attr("class", "img_btn_enabled");
      } else {
        globals.opt_out = false;

        //logEvent("opting into sharing content", "export");

        export_formats.selectAll(".img_btn_enabled")
          .attr("class", "img_btn_disabled");
      }
    });

  out_out_cb.append("label")
    .attr("class", "menu_label")
    .attr("for", "opt_out_cb")
    .style("vertical-align", "text-top")
    .text(" Don't share content with Microsoft");


  /**
  ---------------------------------------------------------------------------------------
  OPTIONS DIV
  ---------------------------------------------------------------------------------------
  **/

  selectWithParent()
    .append("div")
    .attr("id", "option_div")
    .attr("class", "control_div");

  /**
  ---------------------------------------------------------------------------------------
  CAPTION OPTIONS
  ---------------------------------------------------------------------------------------
  **/

  selectWithParent()
    .append("div")
    .attr("id", "caption_div")
    .attr("class", "annotation_div control_div")
    .style("display", "none");

  /**
  ---------------------------------------------------------------------------------------
  IMAGE OPTIONS
  ---------------------------------------------------------------------------------------
  **/

  instance._addImagePopup = addImagePopup();
  selectWithParent().node().appendChild(instance._addImagePopup.element.node());
  instance._addImagePopup.on("imageSelected", instance._onAddImageSelected.bind(this));

  /**
  --------------------------------------------------------------------------------------
  DATASETS
  --------------------------------------------------------------------------------------
  **/

  selectWithParent().append("div")
    .attr("id", "logo_div")
    .html("<a href='https://microsoft.com'><img class='ms-logo' src='" + imageUrls("ms-logo.svg") + "'></a>");

  var footer = selectWithParent().append("div")
    .attr("id", "footer");

  footer.append("div")
    .attr("id", "footer_left")
    .html("<span class='footer_text_left'><a title=About & getting started' href='../../' target='_blank'>About & getting started</a></span> <span class='footer_text_left'><a title='Contact the project team' href='mailto:timelinestoryteller@microsoft.com' target='_top'>Contact the project team</a>");

  footer.append("div")
    .attr("id", "footer_right")
    .html("<span class='footer_text'><a title='Privacy & cookies' href='https://go.microsoft.com/fwlink/?LinkId=521839' target='_blank'>Privacy & cookies</a></span><span class='footer_text'><a title='Terms of use' href='https://go.microsoft.com/fwlink/?LinkID=760869' target='_blank'>Terms of use</a></span><span class='footer_text'><a title='Trademarks' href='http://go.microsoft.com/fwlink/?LinkId=506942' target='_blank'>Trademarks</a></span><span class='footer_text'><a title='About our ads' href='http://choice.microsoft.com/' target='_blank'>About our ads</a></span><span class='footer_text'>© 2017 Microsoft</span>");

  var boilerplate = instance.importPanel.element.append("div")
    .attr("id", "boilerplate")
    .html("<span class='boilerplate_title'>Timeline Storyteller (Alpha)</span>");

  boilerplate.append("input")
    .attr({
      type: "image",
      name: "Hide import panel",
      id: "import_close_btn",
      class: "img_btn_enabled",
      src: imageUrls("close.png"),
      height: 15,
      width: 15,
      title: "Hide import panel"
    })
    .style("margin-top", "5px")
    .on("click", function () {
      //logEvent("hiding import panel", "load");

      instance.importPanel.hide();

      selectWithParent("#gdocs_info").style("height", 0 + "px");
      selectAllWithParent(".gdocs_info_element").style("display", "none");
    });

  instance._initializeImportPanel();

  var gdocs_info = instance.importPanel.element.append("div")
    .attr("id", "gdocs_info");

  gdocs_info.append("div")
    .attr("id", "gdoc_spreadsheet_key_div")
    .attr("class", "gdocs_info_element")
    .append("input")
    .attr({
      type: "text",
      placeholder: "Published spreadsheet URL",
      class: "text_input",
      id: "gdoc_spreadsheet_key_input"
    });

  gdocs_info.append("div")
    .attr("id", "gdoc_spreadsheet_title_div")
    .attr("class", "gdocs_info_element")
    .append("input")
    .attr({
      type: "text",
      placeholder: "OPTIONAL: Worksheet title (tab name)",
      class: "text_input",
      id: "gdoc_worksheet_title_input"
    });

  gdocs_info.append("div")
    .attr("id", "gdoc_spreadsheet_confirm_div")
    .attr("class", "gdocs_info_element")
    .style("width", "20px")
    .append("input")
    .attr({
      type: "image",
      name: "Confirm Google Spreadsheet Data",
      id: "confirm_gdocs_btn",
      class: "img_btn_enabled",
      src: imageUrls("check.png"),
      height: 20,
      width: 20,
      title: "Confirm Google Spreadsheet Data"
    })
    .on("click", function () {
      globals.gdoc_key = selectWithParent("#gdoc_spreadsheet_key_input").property("value");
      globals.gdoc_key = globals.gdoc_key.replace(/.*\/d\//g, "");
      globals.gdoc_key = globals.gdoc_key.replace(/\/.*$/g, "");
      globals.gdoc_worksheet = selectWithParent("#gdoc_worksheet_title_input").property("value");
      //logEvent("gdoc spreadsheet " + globals.gdoc_worksheet + " added using key \"" + globals.gdoc_key + "\"", "load");

      if (globals.gdoc_worksheet !== "") {
        gsheets.getWorksheet(globals.gdoc_key, globals.gdoc_worksheet, function (err, sheet) {
          if (err !== null) {
            alert(err); // eslint-disable-line no-alert
            return true;
          }
          setTimeout(function () {
            instance.load({ timeline_json_data: sheet.data }, false);
          }, 500);
        });
      } else {
        var worksheet_id;

        gsheets.getSpreadsheet(globals.gdoc_key, function (err, sheet) {
          if (err !== null) {
            alert(err); // eslint-disable-line no-alert
            return true;
          }

          //log("worksheet id: " + sheet.worksheets[0].id);

          setTimeout(function () {
            worksheet_id = sheet.worksheets[0].id;
            gsheets.getWorksheetById(globals.gdoc_key, worksheet_id, function (err2, sheetWithData) {
              if (err2 !== null) {
                alert(err2); // eslint-disable-line no-alert
                return true;
              }

              globals.timeline_json_data = sheetWithData.data;
              setTimeout(function () {
                instance.load({ timeline_json_data: sheetWithData.data }, false);
              }, 500);
            });
          }, 500);
        });
      }
    });

  instance.importPanel.element.append("div")
    .attr("class", "loading_data_indicator")
    .style("display", "none")
    .html("<span>Loading data...</span>");

  instance.importPanel.element.append("div")
    .attr("id", "disclaimer")
    .html("<span class='disclaimer_title'style='clear:both'>An expressive visual storytelling environment for presenting timelines.</span><span class='disclaimer_text'><br><strong>A note about privacy</strong>: </span>" +
      "<span class='disclaimer_text'>Your data remains on your machine and is not shared with <a title='Microsoft' href='http://microsoft.com'>Microsoft</a> unless you export the content you create and provide your email address. If you share your content with <a title='Microsoft' href='http://microsoft.com'>Microsoft</a>, we will use it for research and to improve our products and services. We may also include it in a future research publication. " +
      "By using this service, you agree to <a title='Microsoft' href='http://microsoft.com'>Microsoft</a>'s <a title='Privacy' href='https://go.microsoft.com/fwlink/?LinkId=521839'>Privacy Statement</a> and <a title='Terms of Use' href='https://go.microsoft.com/fwlink/?LinkID=760869'>Terms of Use</a>.</span>");

  var timeline_metadata = instance.importPanel.element.append("div")
    .attr("id", "timeline_metadata")
    .style("display", "none");

  timeline_metadata.append("div")
    .attr("id", "timeline_metadata_contents");

  timeline_metadata.append("div")
    .attr({
      id: "draw_timeline",
      class: "img_btn_enabled import_label",
      title: "Draw Timeline"
    })
    .on("click", function () {
      selectWithParent("#gdocs_info").style("height", 0 + "px");
      selectWithParent("#gdoc_spreadsheet_key_input").property("value", "");
      selectWithParent("#gdoc_worksheet_title_input").property("value", "");
      selectAllWithParent(".gdocs_info_element").style("display", "none");

      drawTimeline(globals.active_data);

      instance.setPlaybackMode(false, false);

      updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());
    })
    .append("text")
    .attr("class", "boilerplate_title")
    .style("color", "white")
    .style("cursor", "pointer")
    .style("position", "relative")
    .text("Draw this timeline");

  /**
  --------------------------------------------------------------------------------------
  TIMELINE CONFIG OPTIONS UI
  --------------------------------------------------------------------------------------
  **/

  var option_picker = selectWithParent("#option_div");

  // representation options
  var representation_picker = option_picker.append("div")
    .attr("class", "option_picker")
    .attr("id", "representation_picker");

  representation_picker.append("text")
    .attr("class", "ui_label")
    .text("Timeline representation");

  var representation_rb = representation_picker.selectAll("div")
    .data(globals.representations)
    .enter();

  var representation_rb_label = representation_rb.append("label")
    .attr("class", "option_rb")
    .on("mouseover", function (d) {
      var pos_x = this.getBoundingClientRect().left;
      var offset_x = 0;
      if (pos_x > globals.width / 2) {
        offset_x = pos_x - 235;
      } else {
        offset_x = pos_x + 53;
      }
      var offset_y = this.getBoundingClientRect().top;
      selectWithParent().append("div")
        .attr("id", "rb_hint")
        .style("left", offset_x + "px")
        .style("top", offset_y + "px")
        .attr("class", function () {
          if (pos_x > globals.width / 2) {
            return "rb_hint_right";
          }
          return "rb_hint_left";
        })
        .style("text-align", function () {
          if (pos_x > globals.width / 2) {
            return "right";
          }
          return "left";
        })
        .html(d.hint);
    })
    .on("mouseout", function () {
      selectWithParent("#rb_hint").remove();
    });

  representation_rb_label.append("input")
    .attr({
      type: "radio",
      name: "representation_rb",
      value: function (d) {
        return d.name;
      }
    })
    .property("checked", function (d) {
      return d.name === timeline_vis.tl_representation();
    })
    .property("disabled", true);

  representation_rb_label.append("img")
    .attr({
      height: 40,
      width: 40,
      class: "img_btn_disabled",
      src: function (d) {
        return d.icon;
      }
    });

  representation_rb_label.append("span")
    .attr("class", "option_rb_label")
    .text(function (d) {
      return d.name;
    });

  // scale options
  var scale_picker = option_picker.append("div")
    .attr("class", "option_picker")
    .attr("id", "scale_picker");

  scale_picker.append("text")
    .attr("class", "ui_label")
    .text("Scale");

  var scale_rb = scale_picker.selectAll("div")
    .data(globals.scales)
    .enter();

  var scale_rb_label = scale_rb.append("label")
    .attr("class", "option_rb")
    .on("mouseover", function (d) {
      var pos_x = this.getBoundingClientRect().left;
      var offset_x = 0;
      if (pos_x > globals.width / 2) {
        offset_x = pos_x - 235;
      } else {
        offset_x = pos_x + 53;
      }
      var offset_y = this.getBoundingClientRect().top;
      selectWithParent().append("div")
        .attr("id", "rb_hint")
        .style("left", offset_x + "px")
        .style("top", offset_y + "px")
        .attr("class", function () {
          if (pos_x > globals.width / 2) {
            return "rb_hint_right";
          }
          return "rb_hint_left";
        })
        .style("text-align", function () {
          if (pos_x > globals.width / 2) {
            return "right";
          }
          return "left";
        })
        .html(d.hint);
    })
    .on("mouseout", function () {
      selectWithParent("#rb_hint").remove();
    });

  scale_rb_label.append("input")
    .attr({
      type: "radio",
      name: "scale_rb",
      value: function (d) {
        return d.name;
      }
    })
    .property("checked", function (d) {
      return d.name === timeline_vis.tl_scale();
    })
    .property("disabled", true);

  scale_rb_label.append("img")
    .attr({
      height: 40,
      width: 40,
      class: "img_btn_disabled",
      src: function (d) {
        return d.icon;
      }
    });

  scale_rb_label.append("span")
    .attr("class", "option_rb_label")
    .text(function (d) {
      return d.name;
    });

  // layout options
  var layout_picker = option_picker.append("div")
    .attr("class", "option_picker")
    .style("border-right", "none")
    .attr("id", "layout_picker");

  layout_picker.append("text")
    .attr("class", "ui_label")
    .text("Layout");

  var layout_rb = layout_picker.selectAll("div")
    .data(globals.layouts)
    .enter();

  var layout_rb_label = layout_rb.append("label")
    .attr("class", "option_rb")
    .on("mouseover", function (d) {
      var pos_x = this.getBoundingClientRect().left;
      var offset_x = 0;
      if (pos_x > globals.width / 2) {
        offset_x = pos_x - 235;
      } else {
        offset_x = pos_x + 53;
      }
      var offset_y = this.getBoundingClientRect().top;
      selectWithParent().append("div")
        .attr("id", "rb_hint")
        .attr("class", function () {
          if (pos_x > globals.width / 2) {
            return "rb_hint_right";
          }
          return "rb_hint_left";
        })
        .style("left", offset_x + "px")
        .style("top", offset_y + "px")
        .style("text-align", function () {
          if (pos_x > globals.width / 2) {
            return "right";
          }
          return "left";
        })
        .html(d.hint);
    })
    .on("mouseout", function () {
      selectWithParent("#rb_hint").remove();
    });

  layout_rb_label.append("input")
    .attr({
      type: "radio",
      name: "layout_rb",
      value: function (d) {
        return d.name;
      }
    })
    .property("checked", function (d) {
      return d.name === timeline_vis.tl_layout();
    })
    .property("disabled", true);

  layout_rb_label.append("img")
    .attr({
      height: 40,
      width: 40,
      class: "img_btn_disabled",
      src: function (d) {
        return d.icon;
      }
    });

  layout_rb_label.append("span")
    .attr("class", "option_rb_label")
    .text(function (d) {
      return d.name;
    });

  selectWithParent("#caption_div").append("textarea")
    .attr({
      cols: 37,
      rows: 5,
      placeholder: "Caption text",
      class: "text_input",
      maxlength: 140,
      id: "add_caption_text_input"
    });

  selectWithParent("#caption_div").append("input")
    .attr({
      type: "image",
      name: "Add Caption",
      id: "add_caption_btn",
      class: "img_btn_enabled",
      src: imageUrls("check.png"),
      height: 20,
      width: 20,
      title: "Add Caption"
    })
    .on("click", function () {
      selectWithParent("#caption_div").style("display", "none");
      var caption = selectWithParent("#add_caption_text_input").property("value");
      //logEvent("caption added: \"" + caption + "\"", "annotation");

      let highestCaptionId = getHighestId(globals.caption_list);

      var caption_list_item = {
        id: highestCaptionId + 1,
        caption_text: caption,
        x_rel_pos: 0.5,
        y_rel_pos: 0.25,
        caption_width: d3.min([caption.length * 10, 200]),
        z_index: getNextZIndex()
      };

      globals.caption_list.push(caption_list_item);

      addCaption(caption, d3.min([caption.length * 10, 200]), 0.5, 0.25, caption_list_item);
      selectWithParent("#add_caption_text_input").property("value", "");
    });


  /**
  --------------------------------------------------------------------------------------
  MAIN PREPROCESSING
  --------------------------------------------------------------------------------------
  **/

  function loadTimeline(state, skipConfig) {
    instance._loaded = false;

    instance._hideError();

    var loadDataIndicator = selectWithParent(".loading_data_indicator");
    loadDataIndicator.style("display", "block");

    // Allow the user to configure the timeline first
    if (!skipConfig) {
      instance.importPanel.show();
    } else {
      instance.importPanel.hide();
    }

    instance._component_width = parentElement.clientWidth;
    instance._component_height = parentElement.clientHeight;

    instance.onIntro = false;

    // Give it some time to render the "load data" indicator
    return new Promise(resolve => {
      setTimeout(function () {
        try {
          selectWithParent("#disclaimer").style("display", "none");
          selectWithParent("#timeline_metadata_contents").html("");
          control_panel.selectAll("input").attr("class", "img_btn_disabled");
          selectWithParent("#filter_type_picker").selectAll("input").property("disabled", true);
          selectWithParent("#filter_type_picker").selectAll("img").attr("class", "img_btn_disabled");
          selectWithParent("#playback_bar").selectAll("img").attr("class", "img_btn_disabled");
          selectAllWithParent(".option_rb").select("input").property("disabled", "true");
          selectAllWithParent(".option_rb").select("img").attr("class", "img_btn_disabled");
          selectAllWithParent(".option_rb img").style("border", "2px solid transparent");
          selectWithParent("#menu_div").style("left", -50 + "px");
          selectWithParent("#navigation_div").style("bottom", -100 + "px");
          globals.use_custom_palette = false;

          if (main_svg !== undefined) {
            // console.clear();
            main_svg.remove();
            filter_div.remove();
            navigation_div.remove();
            timeline_vis.prev_tl_representation("None");

            // If we have no scenes, reset everything to default
            if (!(state.scenes && state.scenes.length)) {
              instance._currentSceneIndex = -1;
              globals.gif_index = 0;
              globals.scenes = [];
              globals.caption_list = [];
              globals.image_list = [];
              globals.annotation_list = [];
              timeline_vis.tl_scale("Chronological")
                .tl_layout("Unified")
                .tl_representation("Linear");
              selectAllWithParent(".gif_frame").remove();
              timeline_vis.resetCurve();
            }
          }

          if (globals.legend_panel !== undefined) {
            globals.legend_panel.remove();
          }

          filter_div = selectWithParent()
            .append("div")
            .attr("id", "filter_div")
            .attr("class", "control_div")
            .style("display", "none")
            .style("transition", "all 0.05s ease")
            .style("-webkit-transition", "all 0.05s ease");

          // initialize global variables accessed by multiple visualziations
          globals.date_granularity = "years";
          globals.max_num_tracks = 0;
          globals.max_end_age = 0;
          globals.max_num_seq_tracks = 0;
          globals.legend_rect_size = globals.unit_width;
          globals.legend_spacing = 5;
          globals.categories = undefined;
          globals.categories = d3.scale.ordinal(); // scale for event types
          if (globals.color_palette !== undefined) {
            globals.categories.range(globals.color_palette);
          }
          globals.facets = d3.scale.ordinal(); // scale for facets (timelines)
          globals.segments = d3.scale.ordinal(); // scale for segments
          globals.present_segments = d3.scale.ordinal();
          globals.num_categories = 0;
          globals.num_facets = 0;
          globals.timeline_facets = [];

          instance._main_svg = main_svg = instance._container
            .append("svg")
            .attr("id", "main_svg");

          navigation_div = selectWithParent()
            .append("div")
            .attr("id", "navigation_div")
            .attr("class", "control_div");

          var playback_bar = navigation_div.append("div")
            .attr("id", "playback_bar");

          playback_bar.append("div")
            .attr("id", "record_scene_div")
            .attr("class", "nav_bttn")
            .append("img")
            .attr({
              id: "record_scene_btn",
              class: "img_btn_disabled",
              src: imageUrls("record.png"),
              height: 20,
              width: 20,
              title: "Record Scene"
            })
            .on("click", function () {
              if (!instance.playback_mode) {
                instance._recordScene();
              }
            });

          playback_bar.append("div")
            .attr("id", "prev_scene_div")
            .attr("class", "nav_bttn")
            .append("img")
            .attr("id", "prev_scene_btn")
            .attr("height", 20)
            .attr("width", 20)
            .attr("src", imageUrls("prev.png"))
            .attr("class", "img_btn_disabled")
            .attr("title", "Previous Scene")
            .on("click", function () {
              goPreviousScene();
            });

          playback_bar.append("div")
            .attr("id", "next_scene_div")
            .attr("class", "nav_bttn")
            .append("img")
            .attr("height", 20)
            .attr("width", 20)
            .attr("class", "img_btn_disabled")
            .attr("id", "next_scene_btn")
            .attr("src", imageUrls("next.png"))
            .attr("title", "Next Scene")
            .on("click", function () {
              goNextScene();
            });

          var playback_cb = playback_bar.append("div")
            .attr("id", "playback_div")
            .attr("class", "nav_bttn");

          var playback_cb_label = playback_cb.append("label")
            .attr("class", "nav_cb");

          playback_cb_label.append("input")
            .attr({
              type: "checkbox",
              name: "playback_cb",
              value: instance.playback_mode
            })
            .property("checked", false)
            .on("change", function () {
              instance.setPlaybackMode(!instance.playback_mode);
            });

          playback_cb_label.append("img")
            .attr({
              id: "play_scene_btn",
              class: "img_btn_disabled",
              src: imageUrls("play.png"),
              height: 20,
              width: 20,
              title: "Toggle Playback Mode"
            });

          playback_bar.append("div")
            .attr("id", "stepper_container")
            // .style('width', function () {
            //   return (globals.window_width * 0.9 - 120 - 12) + 'px';
            // })
            .append("svg")
            .attr("id", "stepper_svg")
            .append("text")
            .attr("id", "stepper_svg_placeholder")
            .attr("y", 25)
            .attr("dy", "0.25em")
            .text("Recorded timeline scenes will appear here.");

          window.addEventListener("resize", function () {
            selectWithParent("#stepper_container").style("width", function () {
              return (instance._render_width * 0.9 - 120 - 12 - 5) + "px";
            });
            instance._onResized();
          });

          var defs = main_svg.append("defs");

          var filter = defs.append("filter")
            .attr("id", "drop-shadow")
            .attr("x", 0)
            .attr("y", 0)
            .attr("width", "200%")
            .attr("height", "200%");

          // translate output of Gaussian blur to the right and downwards with 2px
          // store result in offsetBlur
          filter.append("feOffset")
            .attr("in", "SourceAlpha")
            .attr("dx", 2.5)
            .attr("dy", 2.5)
            .attr("result", "offOut");

          filter.append("feGaussianBlur")
            .attr("in", "offOut")
            .attr("stdDeviation", 2.5)
            .attr("result", "blurOut");

          filter.append("feBlend")
            .attr("in", "SourceGraphic")
            .attr("in2", "blurOut")
            .attr("mode", "normal");

          defs.append("filter")
            .attr("id", "greyscale")
            .append("feColorMatrix")
            .attr("type", "matrix")
            .attr("dur", "0.5s")
            .attr("values", "0.4444 0.4444 0.4444 0 0 0.4444 0.4444 0.4444 0 0 0.4444 0.4444 0.4444 0 0 0 0 0 1 0");

          /**
          ---------------------------------------------------------------------------------------
          LOAD DATA
          ---------------------------------------------------------------------------------------
          **/
          if (state) {
            instance._loadTimelineFromState(state, instance._render_height);

            // if we have scenes to show, we don't need the tooltip
            if (state.scenes && state.scenes.length) {
              selectWithParent("#stepper_svg_placeholder").remove();
            }
          }
        } finally {
          // Reapply the UI scale to new elements
          instance.setUIScale(instance.scale);

          loadDataIndicator.style("display", "none");
          instance.applyOptions();

          if (skipConfig) {
            drawTimeline(globals.active_data).then(resolve);
          }

          // call this again afterward, cause some elements are created in loadTimeline function
          // and we need to ensure they are hidden/visible
          instance.setPlaybackMode(instance.playback_mode, false);

          instance._loaded = true;

          if (!skipConfig) {
            resolve();
          }
        }
      }, 10);
    });
  }

  instance._loadTimeline = loadTimeline;

  /**
   * Preprocess data after loading
   * @param {object} data The data to preprocess
   * @param {boolean} shouldDrawTimeline If the timeline should be drawn after it is initialized
   * @returns {void}
   */
  function initTimelineData(data, shouldDrawTimeline) {
    var unique_values = d3.map([]);
    var unique_data = [];

    globals.timeline_json_data = data;

    data.forEach(function (d, i) {
      if (d && !d.hasOwnProperty("id")) {
        d.id = i;
      }
      unique_values.set((d.content_text + d.start_date + d.end_date + d.category + d.facet), d);
    });

    // find unique values
    unique_values.forEach(function (d) {
      unique_data.push(unique_values.get(d));
    });
    //logEvent(unique_data.length + " unique events", "preprocessing");

    processTimeline(unique_data, shouldDrawTimeline);
  }

  function processTimeline(data, shouldDrawTimeline) {
    // check for earliest and latest numerical dates before parsing
    globals.earliest_date = d3.min(data, function (d) {
      if (d.start_date instanceof Date) {
        return d.start_date;
      }
      return +d.start_date;
    });

    globals.latest_start_date = d3.max(data, function (d) {
      if (d.start_date instanceof Date) {
        return d.start_date;
      }
      return +d.start_date;
    });

    globals.latest_end_date = d3.max(data, function (d) {
      if (d.end_date instanceof Date) {
        return d.end_date;
      }
      return +d.end_date;
    });

    // set flag for really epic time scales
    if (globals.isNumber(globals.earliest_date)) {
      if (globals.earliest_date < -9999 || d3.max([globals.latest_start_date, globals.latest_end_date]) > 10000) {
        globals.date_granularity = "epochs";
      }
    }

    //log("date_granularity after: " + globals.date_granularity);

    parseDates(data); // parse all the date values, replace blank end_date values

    // set annotation counter for each item
    data.forEach(function (item) {
      item.annotation_count = 0;
    });

    /**
    ---------------------------------------------------------------------------------------
    PROCESS CATEGORIES OF EVENTS
    ---------------------------------------------------------------------------------------
    **/

    // determine event categories from data
    globals.categories.domain(data.map(function (d) {
      return d.category;
    }));

    globals.num_categories = globals.categories.domain().length;

    globals.max_legend_item_width = 0;

    globals.categories.domain().sort().forEach(function (item) {
      var legend_dummy = document.createElement("span");
      legend_dummy.id = "legend_dummy";
      legend_dummy.style.fontSize = "12px";
      legend_dummy.style.fill = "#fff";
      legend_dummy.style.fontFamily = "Century Gothic";
      legend_dummy.innerHTML = item;
      document.querySelector(".timeline_storyteller").appendChild(legend_dummy);
      var legend_dummy_width = legend_dummy.offsetWidth;
      document.querySelector(".timeline_storyteller").removeChild(legend_dummy);

      if (legend_dummy_width > globals.max_legend_item_width) {
        globals.max_legend_item_width = legend_dummy_width;
      }
    });

    //logEvent("# categories: " + globals.num_categories, "preprocessing");

    var temp_palette;
    // assign colour labels to categories if # categories < 12
    if (globals.num_categories <= 20 && globals.num_categories >= 11) {
      temp_palette = colorSchemes.schema5();
      globals.categories.range(temp_palette);
      temp_palette = undefined;
    } else if (globals.num_categories <= 10 && globals.num_categories >= 3) {
      temp_palette = colorSchemes.schema2();
      globals.categories.range(temp_palette);
      temp_palette = undefined;
    } else if (globals.num_categories === 2) {
      temp_palette = ["#E45641", "#44B3C2"];
      globals.categories.range(temp_palette);
      temp_palette = undefined;
    } else {
      temp_palette = ["#E45641"];
      globals.categories.range(temp_palette);
      temp_palette = undefined;
    }
    if (globals.use_custom_palette) {
      globals.categories.range(globals.color_palette);
      //logEvent("custom palette: " + globals.categories.range(), "color palette");
    }

    filter_div.append("input")
      .attr({
        type: "image",
        name: "Hide filter panel",
        id: "export_close_btn",
        class: "img_btn_enabled",
        src: imageUrls("close.png"),
        height: 15,
        width: 15,
        title: "Hide filter panel"
      })
      .style("position", "absolute")
      .style("top", "0px")
      .style("left", "5px")
      .style("margin-top", "5px")
      .on("click", function () {
        selectWithParent("#filter_div").style("display", "none");

        //logEvent("hide filter panel", "export");
      });

    filter_div.append("text")
      .attr("class", "menu_label filter_label")
      .style("margin-right", "auto")
      .text("Filter Options")
      .style("cursor", "move")
      .call(filterDrag);

    filter_div.append("hr")
      .attr("class", "menu_hr");

    // filter type options
    var filter_type_picker = filter_div.append("div")
      .attr("id", "filter_type_picker")
      .attr("class", "filter_div_section");

    filter_type_picker.append("div")
      .attr("class", "filter_div_header")
      .append("text")
      .attr("class", "menu_label filter_label")
      .text("Filter Mode:");

    var filter_type_rb = filter_type_picker.selectAll("g")
      .data(["Emphasize", "Hide"])
      .enter();

    var filter_type_rb_label = filter_type_rb.append("label")
      .attr("class", "menu_rb");

    filter_type_rb_label.append("input")
      .attr({
        type: "radio",
        name: "filter_type_rb",
        value: function (d) {
          return d;
        }
      })
      .property("disabled", false)
      .property("checked", function (d) {
        return d === "Emphasize";
      });

    filter_type_rb_label.append("img")
      .attr({
        class: "img_btn_enabled",
        height: 30,
        width: 30,
        title: function (d) {
          return d;
        },
        src: function (d) {
          return imageUrls(d === "Emphasize" ? "highlight.png" : "hide.png");
        }
      })
      .style("margin-bottom", "0px");

    filter_type_rb_label.append("span")
      .attr("class", "option_rb_label")
      .html(function (d) {
        return d;
      });

    selectAllWithParent("#filter_type_picker input[name=filter_type_rb]").on("change", function () {
      const newCategories = selectWithParent("#category_picker").select("option");
      const newFacets = selectWithParent("#facet_picker").select("option");
      const newSegments = selectWithParent("#segment_picker").select("option");

      globals.filter_type = this.value;

      selectWithParent("#filter_div").style("display", "inline");

      //logEvent("filter type changed: " + this.value, "filter");

      const isHide = globals.filter_type === "Hide";
      if (!isHide) {
        globals.active_data = globals.all_data;
      }

      const trigger_remove_filter =
        globals.selected_categories[0].length !== 1 || globals.selected_categories[0][0].value !== "( All )" ||
        globals.selected_facets[0].length !== 1 || globals.selected_facets[0][0].value !== "( All )" ||
        globals.selected_segments[0].length !== 1 || globals.selected_segments[0][0].value !== "( All )";

      if (trigger_remove_filter) {
        const remove = globals.dispatch.remove;
        const emphasize = globals.dispatch.Emphasize;
        (isHide ? emphasize : remove).call(globals.dispatch, newCategories, newFacets, newSegments);
        (isHide ? remove : emphasize).call(globals.dispatch, globals.selected_categories, globals.selected_facets, globals.selected_segments);
      }
    });

    var category_filter = filter_div.append("div")
      .attr("class", "filter_div_section");

    var category_filter_header = category_filter.append("div")
      .attr("class", "filter_div_header");

    category_filter_header.append("text")
      .attr("class", "menu_label filter_label")
      .text("Category");

    category_filter_header.append("label")
      .attr("for", "category_picker")
      .style("display", "block")
      .style("margin-right", "100%")
      .attr("id", "category_picker_label")
      .append("img")
      .attr({
        name: "Filter by event category",
        class: "filter_header_icon",
        height: 30,
        width: 30,
        title: "Filter by event category",
        src: imageUrls("categories.png")
      });

    var all_categories = ["( All )"];

    category_filter.append("select")
      .attr("class", "filter_select")
      .attr("size", 8)
      .attr("id", "category_picker")
      .attr({
        multiple: true
      })
      .on("change", function () {
        instance._updateSelectedFilters(d3.select(this), "selected_categories");
      })
      .selectAll("option")
      .data(all_categories.concat(globals.categories.domain().sort()))
      .enter()
      .append("option")
      .text(function (d) { return d; })
      .property("selected", function (d) {
        return d === "( All )";
      });

    globals.selected_categories = selectWithParent("#category_picker")
      .selectAll("option")
      .filter(function () {
        return this.selected;
      });

    /**
    ---------------------------------------------------------------------------------------
    PROCESS FACETS
    ---------------------------------------------------------------------------------------
    **/

    // determine facets (separate timelines) from data
    globals.facets.domain(data.map(function (d) {
      return d.facet;
    }));

    globals.facets.domain().sort();

    globals.num_facets = globals.facets.domain().length;
    globals.total_num_facets = globals.num_facets;
    globals.num_facet_cols = Math.ceil(Math.sqrt(globals.num_facets));
    globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

    //logEvent("# facets: " + globals.num_facets, "preprocessing");

    var facet_filter = filter_div.append("div")
      .attr("class", "filter_div_section");

    var facet_filter_header = facet_filter.append("div")
      .attr("class", "filter_div_header");

    facet_filter_header.append("text")
      .attr("class", "menu_label filter_label")
      .text("Facet");

    facet_filter_header.append("label")
      .attr("for", "facet_picker")
      .style("display", "block")
      .style("margin-right", "100%")
      .attr("id", "facet_picker_label")
      .append("img")
      .attr({
        name: "Filter by event facet",
        class: "filter_header_icon",
        height: 30,
        width: 30,
        title: "Filter by event facet",
        src: imageUrls("facets.png")
      });

    var all_facets = ["( All )"];

    facet_filter.append("select")
      .attr("class", "filter_select")
      .attr("size", 8)
      .attr("id", "facet_picker")
      .attr({
        multiple: true
      })
      .on("change", function () {
        instance._updateSelectedFilters(d3.select(this), "selected_facets");
      })
      .selectAll("option")
      .data(all_facets.concat(globals.facets.domain().sort()))
      .enter()
      .append("option")
      .text(function (d) { return d; })
      .property("selected", function (d) {
        return d === "( All )";
      });

    globals.selected_facets = selectWithParent("#facet_picker")
      .selectAll("option")
      .filter(function () {
        return this.selected;
      });

    /**
    ---------------------------------------------------------------------------------------
    PROCESS SEGMENTS
    ---------------------------------------------------------------------------------------
    **/

    // event sorting function
    data.sort(compareAscending);

    if (globals.date_granularity === "epochs") {
      data.min_start_date = globals.earliest_date;
      data.max_start_date = d3.max([globals.latest_start_date, globals.latest_end_date]);
      data.max_end_date = d3.max([globals.latest_start_date, globals.latest_end_date]);
    } else {
      // determine the time domain of the data along a linear quantitative scale
      data.min_start_date = d3.min(data, function (d) {
        return d.start_date;
      });
      data.max_start_date = d3.max(data, function (d) {
        return d.start_date;
      });
      data.max_end_date = d3.max(data, function (d) {
        return time.minute.floor(d.end_date);
      });
    }

    // determine the granularity of segments
    globals.segment_granularity = getSegmentGranularity(data.min_start_date, data.max_end_date);

    data.forEach(function (item) {
      item.segment = getSegment(item.start_date);
    });

    var segment_list = getSegmentList(data.min_start_date, data.max_end_date);

    globals.present_segments.domain(segment_list.map(function (d) {
      return d;
    }));

    var segment_filter = filter_div.append("div")
      .attr("class", "filter_div_section");

    var segment_filter_header = segment_filter.append("div")
      .attr("class", "filter_div_header");

    segment_filter_header.append("text")
      .attr("class", "menu_label filter_label")
      .text("Segment");

    segment_filter_header.append("label")
      .attr("for", "segment_picker")
      .style("display", "block")
      .style("margin-right", "100%")
      .attr("id", "segment_picker_label")
      .append("img")
      .attr({
        name: "Filter by chronological segment",
        class: "filter_header_icon",
        height: 30,
        width: 30,
        title: "Filter by chronological segment",
        src: imageUrls("segments.png")
      });

    var all_segments = ["( All )"];

    segment_filter.append("select")
      .attr("id", "segment_picker")
      .attr("class", "filter_select")
      .attr("size", 8)
      .attr({
        multiple: true
      })
      .on("change", function () {
        instance._updateSelectedFilters(d3.select(this), "selected_segments");
      })
      .selectAll("option")
      .data(all_segments.concat(globals.present_segments.domain().sort()))
      .enter()
      .append("option")
      .text(function (d) { return d; })
      .property("selected", function (d) {
        return d === "( All )";
      });

    globals.selected_segments = selectWithParent("#segment_picker")
      .selectAll("option")
      .filter(function () {
        return this.selected;
      });

    globals.all_data = data;
    globals.active_data = globals.all_data;

    measureTimeline(globals.active_data);
    selectWithParent("#timeline_metadata").style("display", "inline");
    selectWithParent("#timeline_metadata_contents")
      .append("span")
      .attr("class", "metadata_title")
      .style("text-decoration", "underline")
      .text("About this data:");

    selectWithParent("#timeline_metadata_contents")
      .append("div")
      .attr("class", "timeline_metadata_contents_div")
      .html("<p class='metadata_content'><img src='" + imageUrls("timeline.png") + "' width='36px' style='float: left; padding-right: 5px;'/><strong>Cardinality & extent</strong>: " +
        globals.active_data.length + " unique events spanning " + globals.range_text + " <br><strong>Granularity</strong>: " + globals.segment_granularity + "</p>");

    var category_metadata = selectWithParent("#timeline_metadata_contents")
      .append("div")
      .attr("class", "timeline_metadata_contents_div")
      .style("border-top", "1px dashed #999");

    var category_metadata_p = category_metadata
      .append("p")
      .attr("class", "metadata_content")
      .html("<img src='" + imageUrls("categories.png") + "' width='36px' style='float: left; padding-right: 5px;'/><strong>Event categories</strong>: ( " + globals.num_categories + " ) <em><strong>Note</strong>: click on the swatches to assign custom colors to categories.</em><br>");

    var category_metadata_element = category_metadata_p.selectAll(".category_element")
      .data(globals.categories.domain().sort())
      .enter()
      .append("g")
      .attr("class", "category_element");

    category_metadata_element.append("div")
      .attr("class", "colorpicker_wrapper")
      .attr("filter", "url(#drop-shadow)")
      .style("background-color", globals.categories)
      .on("click", function (d, i) {
        var colorEle = this;
        instance._colorPicker.show(this, globals.categories(d), function (value) {
          // Update the display
          d3.select(colorEle).style("background-color", value);

          instance.setCategoryColor(d, i, value);
        });
      });
    //   .append("input")
    //   .attr("type", "color")
    //   .attr("class", "colorpicker")
    //   .attr("value", globals.categories)
    //   .on("change", function (d, i) {

    //   });

    category_metadata_element.append("span")
      .attr("class", "metadata_content")
      .style("float", "left")
      .text(function (d) {
        return " " + d + " ..";
      });

    category_metadata.append("p")
      .html("<br>");

    selectWithParent("#timeline_metadata_contents")
      .append("div")
      .attr("class", "timeline_metadata_contents_div")
      .style("border-top", "1px dashed #999")
      .html(
        "<p class='metadata_content'><img src='" + imageUrls("facets.png") + "' width='36px' style='float: left; padding-right: 5px;'/><strong>Timeline facets</strong>: " +
        ((globals.facets.domain().length > 1) ? ("( " + globals.num_facets + " ) " + globals.facets.domain().slice(0, 30).join(" .. ")) : "(none)") + "</p>");

    if (shouldDrawTimeline) {
      drawTimeline(globals.active_data);
    }
  }

  /**
  ---------------------------------------------------------------------------------------
  SELECT SCALE
  ---------------------------------------------------------------------------------------
  **/

  selectAllWithParent("#scale_picker input[name=scale_rb]").on("change", function () {
    instance.clearCanvas();

    //logEvent("scale change: " + this.value, "scale_change");

    determineSize(globals.active_data, this.value, timeline_vis.tl_layout(), timeline_vis.tl_representation());

    adjustSvgSize();

    main_svg.call(timeline_vis.duration(instance._getAnimationStepDuration())
      .tl_scale(this.value)
      .height(globals.height)
      .width(globals.width));

    updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());
  });

  /**
  ---------------------------------------------------------------------------------------
  SELECT LAYOUT
  ---------------------------------------------------------------------------------------
  **/

  selectAllWithParent("#layout_picker input[name=layout_rb]").on("change", function () {
    instance.clearCanvas();

    //logEvent("layout change: " + this.value, "layout_change");

    determineSize(globals.active_data, timeline_vis.tl_scale(), this.value, timeline_vis.tl_representation());

    adjustSvgSize();

    main_svg.call(timeline_vis.duration(instance._getAnimationStepDuration())
      .tl_layout(this.value)
      .height(globals.height)
      .width(globals.width));

    updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());
  });

  /**
  ---------------------------------------------------------------------------------------
  SELECT REPRESENTATION
  ---------------------------------------------------------------------------------------
  **/

  selectAllWithParent("#representation_picker input[name=representation_rb]").on("change", function () {
    instance.clearCanvas();

    //logEvent("representation change: " + this.value, "representation_change");

    if (timeline_vis.tl_layout() === "Segmented") {
      if (this.value === "Grid") {
        globals.segment_granularity = "centuries";
      } else if (this.value === "Calendar") {
        globals.segment_granularity = "weeks";
      } else {
        globals.segment_granularity = getSegmentGranularity(globals.global_min_start_date, globals.global_max_end_date);
      }
    }

    determineSize(globals.active_data, timeline_vis.tl_scale(), timeline_vis.tl_layout(), this.value);

    adjustSvgSize();

    main_svg.call(timeline_vis.duration(instance._getAnimationStepDuration())
      .tl_representation(this.value)
      .height(globals.height)
      .width(globals.width));

    if (timeline_vis.tl_representation() === "Curve" && !globals.dirty_curve) {
      selectWithParent(".timeline_frame").style("cursor", "crosshair");
    } else {
      selectWithParent(".timeline_frame").style("cursor", "auto");
    }

    updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());
  });

  /**
  ---------------------------------------------------------------------------------------
  SCENE transitions
  ---------------------------------------------------------------------------------------
  **/

  function updateNavigationStepper() {
    var STEPPER_STEP_WIDTH = 50;

    var navigation_step_svg = selectWithParent("#stepper_svg");

    var navigation_step = navigation_step_svg.selectAll(".framePoint")
      .data(globals.scenes);

    navigation_step.exit().transition()
      .delay(1000)
      .remove();

    var navigation_step_update = navigation_step.transition()
      .duration(instance.options.animations ? 1000 : 0);

    var navigation_step_enter = navigation_step.enter()
      .append("g")
      .attr("class", "framePoint")
      .attr("id", function (d) {
        return "frame" + d.s_order;
      })
      .attr("transform", function (d) {
        return "translate(" + (d.s_order * STEPPER_STEP_WIDTH + d.s_order * 5) + ",0)";
      })
      .style("cursor", "pointer");

    navigation_step_update.attr("transform", function (d) {
      return "translate(" + (d.s_order * STEPPER_STEP_WIDTH + d.s_order * 5) + ",0)";
    })
      .attr("id", function (d) {
        return "frame" + d.s_order;
      });

    navigation_step_enter.append("title")
      .text(function (d) {
        return "Scene " + (d.s_order + 1);
      });

    navigation_step_update.select("title")
      .text(function (d) {
        return "Scene " + (d.s_order + 1);
      });

    function changeSceneClickHandler(d) {
      instance._currentSceneIndex = d.s_order;
      changeScene(instance._currentSceneIndex);
    }

    navigation_step_enter.append("rect")
      .attr("fill", "white")
      .attr("width", STEPPER_STEP_WIDTH)
      .attr("height", STEPPER_STEP_WIDTH)
      .style("stroke", function (d) {
        return d.s_order === instance._currentSceneIndex ? "#f00" : "#ccc";
      })
      .style("stroke-width", "3px")
      .on("click", changeSceneClickHandler);

    navigation_step_update.select("rect")
      .style("stroke", function (d) {
        return d.s_order === instance._currentSceneIndex ? "#f00" : "#ccc";
      });

    if (isIE11) {
      navigation_step_enter.append("svg:text")
        .attr("x", 25)
        .attr("y", 25)
        .attr("font-size", "20px")
        .attr("text-anchor", "middle")
        .attr("alignment-baseline", "central")
        .attr("style", "cursor:pointer")
        .text(function (d) {
          return (d.s_order + 1);
        })
        .on("click", changeSceneClickHandler);

      navigation_step_update.select("text")
        .text(function (d) {
          return (d.s_order + 1);
        });
    } else {
      navigation_step_enter.append("svg:image")
        .attr("xlink:href", function (d) {
          return d.s_src;
        })
        .attr("x", 2)
        .attr("y", 2)
        .attr("width", STEPPER_STEP_WIDTH - 4)
        .attr("height", STEPPER_STEP_WIDTH - 4)
        .on("click", changeSceneClickHandler);
    }

    var navigation_step_delete = navigation_step_enter.append("g")
      .attr("class", "scene_delete")
      .style("opacity", 0);

    navigation_step_delete.append("svg:image")
      .attr("class", "annotation_control annotation_delete")

      .attr("title", "Delete Scene")
      .attr("x", STEPPER_STEP_WIDTH - 17)
      .attr("y", 2)
      .attr("width", 15)
      .attr("height", 15)
      .attr("xlink:href", imageUrls("delete.png"));

    navigation_step_delete.append("rect")
      .attr("title", "Delete Scene")
      .attr("x", STEPPER_STEP_WIDTH - 17)
      .attr("y", 2)
      .attr("width", 15)
      .attr("height", 15)
      .on("mouseover", function () {
        d3.select(this).style("stroke", "#f00");
      })
      .on("mouseout", function () {
        d3.select(this).style("stroke", "#ccc");
      })
      .on("click", function (d) {
        selectWithParent("#frame" + d.s_order).remove();
        selectAllWithParent(".frame_hover").remove();
        // delete current scene unless image or caption div is open
        //logEvent("scene " + (d.s_order + 1) + " deleted.", "deletion");

        var j;
        for (j = 0; j < globals.scenes.length; j++) {
          if (globals.scenes[j].s_order === d.s_order) {
            globals.scenes.splice(j, 1);
          }
        }

        for (j = 0; j < globals.scenes.length; j++) {
          if (globals.scenes[j].s_order > d.s_order) {
            globals.scenes[j].s_order--;
          }
        }

        if (instance._currentSceneIndex > d.s_order) {
          instance._currentSceneIndex--;
        }

        updateNavigationStepper();

        instance._dispatch.stateChanged();

        if (instance._currentSceneIndex === d.s_order) { // is current scene to be deleted?
          if (instance._currentSceneIndex === globals.scenes.length - 1) { // is it the final scene?
            instance._currentSceneIndex = 0; // set current scene to first scene
          } else { // current scene is not the last scene
            instance._currentSceneIndex--; // set current scene to previous scene
            if (instance._currentSceneIndex < 0) { // did you delete the first scene?
              instance._currentSceneIndex = globals.scenes.length - 1; // set current to last scene
            }
          }

          if (globals.scenes.length === 0) { // are there no more scenes left?
            instance._currentSceneIndex = -1; // set current scene to -1
          } else {
            changeScene(instance._currentSceneIndex);
          }
        }
      })
      .append("title")
      .text("Delete Scene");

    if (!isIE11) {
      navigation_step_svg.selectAll(".framePoint")
        .on("mouseover", function () {
          const popupSize = 300;
          const frameRect = this.getBoundingClientRect();
          const relativeParentRect = selectWithParent(".timeline_storyteller-container").node().getBoundingClientRect();
          const offscreenAmount = (frameRect.right + popupSize) - relativeParentRect.right;

          // If we're offscreen, then adjust the position to take the offsceen amount into account
          const x_pos = frameRect.left - relativeParentRect.left - (offscreenAmount > 0 ? offscreenAmount : 0);
          const y_pos = frameRect.top - relativeParentRect.top;

          var img_src = d3.select(this).select("image").attr("href");

          d3.select(this).select("rect")
            .style("stroke", "#666");

          d3.select(this).select(".scene_delete")
            .style("opacity", 1);

          selectWithParent().append("div")
            .attr("class", "frame_hover")
            .style("left", `${x_pos}px`)
            .style("top", `${y_pos - popupSize - 20}px`)
            .append("svg")
            .style("padding", "0px")
            .style("width", `${popupSize}px`)
            .style("height", `${popupSize}px`)
            .append("svg:image")
            .attr("xlink:href", img_src)
            .attr("x", 2)
            .attr("y", 2)
            .attr("width", 296)
            .attr("height", 296);
        })
        .on("mouseout", function (d) {
          d3.select(this).select(".scene_delete")
            .style("opacity", 0);

          if (d.s_order === instance._currentSceneIndex) {
            d3.select(this).select("rect")
              .style("stroke", function () {
                return "#f00";
              });
          } else {
            d3.select(this).select("rect")
              .style("stroke", function () {
                return "#ccc";
              });
          }

          selectAllWithParent(".frame_hover").remove();
        });
    }

    navigation_step_svg.attr("width", (globals.scenes.length + 1) * (STEPPER_STEP_WIDTH + 5));

    const total = (globals.scenes || []).length;
    const sceneIdx = instance._currentSceneIndex;
    selectWithParent("#prev_scene_btn")
      // Always show 1 if at the beginning
      .attr("title", total > 1 ? `Scene ${sceneIdx === 0 ? total : sceneIdx} of ${total}` : "Previous Scene")
      .classed("img_btn_disabled", total < 2)
      .classed("img_btn_enabled", total > 1);

    selectWithParent("#next_scene_btn")
      .attr("title", total > 1 ? `Scene ${sceneIdx === total - 1 ? 1 : sceneIdx + 2} of ${total}` : "Next Scene")
      .classed("img_btn_disabled", total < 2)
      .classed("img_btn_enabled", total > 1);
  }

  instance._updateNavigationStepper = updateNavigationStepper;

  instance._prevTransitioning = false;
  function changeScene(scene_index) {
    // Assume we are waiting for transitions if there is already one going.
    var waitForTransitions = instance._prevTransitioning;

    updateNavigationStepper();

    var scene_found = false,
      i = 0,
      scene = globals.scenes[0];

    while (!scene_found && i < globals.scenes.length) {
      if (globals.scenes[i].s_order === scene_index) {
        scene_found = true;
        scene = globals.scenes[i];
      }
      i++;
    }

    selectWithParent("#timecurve").style("visibility", "hidden");

    if (scene.s_representation === "Curve") {
      selectWithParent("#timecurve").attr("d", globals.scenes[scene_index].s_timecurve);
      timeline_vis.render_path(globals.scenes[scene_index].s_timecurve);
      timeline_vis.reproduceCurve();
    }

    // is the new scene a segmented grid or calendar? if so, re-segment the events
    if (scene.s_layout === "Segmented") {
      if (scene.s_representation === "Grid") {
        globals.segment_granularity = "centuries";
      } else if (scene.s_representation === "Calendar") {
        globals.segment_granularity = "weeks";
      } else {
        globals.segment_granularity = getSegmentGranularity(globals.global_min_start_date, globals.global_max_end_date);
      }
    }

    // set a delay for annotations and captions based on whether the scale, layout, or representation changes
    if (timeline_vis.tl_scale() !== scene.s_scale || timeline_vis.tl_layout() !== scene.s_layout || timeline_vis.tl_representation() !== scene.s_representation) {
      waitForTransitions = true;
      instance._prevTransitioning = true;

      // how big is the new scene?
      determineSize(globals.active_data, scene.s_scale, scene.s_layout, scene.s_representation);

      // resize the main svg to accommodate the scene
      adjustSvgSize();

      // set the scene's scale, layout, representation
      timeline_vis.tl_scale(scene.s_scale)
        .tl_layout(scene.s_layout)
        .tl_representation(scene.s_representation)

        // Uses EFFECTIVE_HEIGHT
        .height(d3.max([globals.height, scene.s_height, (instance._render_height - globals.margin.top - globals.margin.bottom - getScrollbarWidth())]))
        .width(d3.max([globals.width, scene.s_width]));
    }

    updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());

    // initilaize scene filter settings
    var scene_category_values = [],
      scene_facet_values = [],
      scene_segment_values = [];

    // which categories are shown in the scene?
    scene.s_categories[0].forEach(function (item) {
      scene_category_values.push(item.__data__);
    });

    // update the category picker
    selectWithParent("#category_picker")
      .selectAll("option")
      .property("selected", function (d) {
        return scene_category_values.indexOf(d) !== -1;
      });

    // which facets are shown in the scene?
    scene.s_facets[0].forEach(function (item) {
      scene_facet_values.push(item.__data__);
    });

    // update the facet picker
    selectWithParent("#facet_picker")
      .selectAll("option")
      .property("selected", function (d) {
        return scene_facet_values.indexOf(d) !== -1;
      });

    // which segments are shown in the scene?
    scene.s_segments[0].forEach(function (item) {
      scene_segment_values.push(item.__data__);
    });

    // update the segment picker
    selectWithParent("#segment_picker")
      .selectAll("option")
      .property("selected", function (d) {
        return scene_segment_values.indexOf(d) !== -1;
      });

    // if filters change in "remove" mode, delay annoations and captions until after transition
    var scene_filter_set_length = scene_category_values.length + scene_facet_values.length + scene_segment_values.length;

    if (scene.s_filter_type === "Hide") {
      scene_filter_set_length += 1;
    }

    if (scene_filter_set_length !== globals.filter_set_length) {
      globals.filter_set_length = scene_filter_set_length;
      waitForTransitions = true;
      instance._prevTransitioning = true;
    }

    globals.selected_categories = scene.s_categories;
    globals.selected_facets = scene.s_facets;
    globals.selected_segments = scene.s_segments;

    // what type of filtering is used in the scene?
    if (scene.s_filter_type === "Hide") {
      selectAllWithParent("#filter_type_picker input[name=filter_type_rb]")
        .property("checked", function (d) {
          return d === "Hide";
        });
      if (globals.filter_type === "Emphasize") {
        globals.dispatch.Emphasize(selectWithParent("#category_picker").select("option"), selectWithParent("#facet_picker").select("option"), selectWithParent("#segment_picker").select("option"));
      }
      globals.filter_type = "Hide";
      globals.dispatch.remove(globals.selected_categories, globals.selected_facets, globals.selected_segments);
    } else if (scene.s_filter_type === "Emphasize") {
      selectAllWithParent("#filter_type_picker input[name=filter_type_rb]")
        .property("checked", function (d) {
          return d === "Emphasize";
        });
      if (globals.filter_type === "Hide") {
        globals.active_data = globals.all_data;
        globals.dispatch.remove(selectWithParent("#category_picker").select("option"), selectWithParent("#facet_picker").select("option"), selectWithParent("#segment_picker").select("option"));
      }
      globals.filter_type = "Emphasize";
      globals.dispatch.Emphasize(globals.selected_categories, globals.selected_facets, globals.selected_segments);
    }

    // where is the legend in the scene?
    selectWithParent(".legend")
      .transition()
      .duration(instance._getAnimationStepDuration())
      .style("z-index", 1)
      .attr("x", scene.s_legend_x)
      .attr("y", scene.s_legend_y);

    globals.legend_x = scene.s_legend_x;
    globals.legend_y = scene.s_legend_y;

    main_svg.selectAll(".timeline_caption").remove();

    main_svg.selectAll(".timeline_image").remove();

    main_svg.selectAll(".event_annotation").remove();

    selectAllWithParent(".timeline_event_g").each(function () {
      this.__data__.selected = false;
    });

    selectAllWithParent(".event_span")
      .attr("filter", "none")
      .style("stroke", "#fff")
      .style("stroke-width", "0.25px");

    selectAllWithParent(".event_span_component")
      .style("stroke", "#fff")
      .style("stroke-width", "0.25px");

    // delay the appearance of captions and annotations if the scale, layout, or representation changes relative to the previous scene
    if (waitForTransitions && timeline_vis.renderComplete) {
      //log("Waiting for transitions");
      timeline_vis.renderComplete.then(() => instance._loadAnnotations(scene, scene_index));
    } else {
      instance._loadAnnotations(scene, scene_index);
    }
  }

  instance._changeScene = changeScene;

  function measureTimeline(data) {
    /**
    ---------------------------------------------------------------------------------------
    SORT AND NEST THE EVENTS
    ---------------------------------------------------------------------------------------
    **/

    // event sorting function
    data.sort(compareAscending);

    if (globals.date_granularity === "epochs") {
      data.min_start_date = globals.earliest_date;
      data.max_start_date = d3.max([globals.latest_start_date, globals.latest_end_date]);
      data.max_end_date = d3.max([globals.latest_start_date, globals.latest_end_date]);
    } else {
      // determine the time domain of the data along a linear quantitative scale
      data.min_start_date = d3.min(data, function (d) {
        return d.start_date;
      });
      data.max_start_date = d3.max(data, function (d) {
        return d.start_date;
      });
      data.max_end_date = d3.max(data, function (d) {
        return time.minute.floor(d.end_date);
      });
    }

    if (globals.date_granularity === "epochs") {
      var format = function (d) {
        return globals.formatAbbreviation(d);
      };
      globals.range_text = format(data.max_end_date.valueOf() - data.min_start_date.valueOf()) + " years" +
        ": " + data.min_start_date.valueOf() + " - " + data.max_end_date.valueOf();
    } else {
      globals.range_text = moment(data.min_start_date).from(moment(data.max_end_date), true) +
        ": " + moment(data.min_start_date).format("YYYY-MM-DD") + " - " + moment(data.max_end_date).format("YYYY-MM-DD");
    }

    //logEvent("range: " + globals.range_text, "preprocessing");

    // create a nested data structure to contain faceted data
    globals.timeline_facets = d3.nest()
      .key(function (d) {
        return d.facet;
      })
      .sortKeys(d3.ascending)
      .entries(data);

    // get event durations
    data.forEach(function (item) {
      if (globals.date_granularity === "days") {
        item.duration = d3.time.days(item.start_date, item.end_date).length;
      } else if (globals.date_granularity === "years") {
        item.duration = item.end_date.getUTCFullYear() - item.start_date.getUTCFullYear();
      } else if (globals.date_granularity === "epochs") {
        item.duration = item.end_date.valueOf() - item.start_date.valueOf();
      }
    });

    data.max_duration = d3.max(data, function (d) {
      return d.duration;
    });

    data.min_duration = d3.min(data, function (d) {
      return d.duration;
    });

    //logEvent("max event duration: " + data.max_duration + " " + globals.date_granularity, "preprocessing");

    //logEvent("min event duration: " + data.min_duration + " " + globals.date_granularity, "preprocessing");

    // determine the granularity of segments
    globals.segment_granularity = getSegmentGranularity(data.min_start_date, data.max_end_date);

    //logEvent("segment granularity: " + globals.segment_granularity, "preprocessing");

    var segment_list = getSegmentList(data.min_start_date, data.max_end_date);

    globals.segments.domain(segment_list.map(function (d) {
      return d;
    }));

    //logEvent("segments (" + globals.segments.domain().length + "): " + globals.segments.domain(), "preprocessing");

    globals.num_segments = globals.segments.domain().length;
    globals.num_segment_cols = Math.ceil(Math.sqrt(globals.num_segments));
    globals.num_segment_rows = Math.ceil(globals.num_segments / globals.num_segment_cols);
  }
  /**
   * Renders the timeline
   * @param {object[]} data The data to render
   * @returns {void}
   */
  function drawTimeline(data) {
    selectWithParent("#timeline_metadata").style("display", "none");
    selectWithParent("#timeline_metadata_contents").html("");
    instance.importPanel.hide();

    /**
    ---------------------------------------------------------------------------------------
    CALL STANDALONE TIMELINE VISUALIZATIONS
    ---------------------------------------------------------------------------------------
    **/

    control_panel.selectAll("input").attr("class", "img_btn_enabled");
    selectWithParent("#navigation_div").style("bottom", (instance.options.showAbout === false || instance.playback_mode) ? "20px" : "50px");
    selectWithParent("#filter_type_picker").selectAll("input").property("disabled", false);
    selectWithParent("#filter_type_picker").selectAll("img").attr("class", "img_btn_enabled");

    selectAllWithParent("#record_scene_btn, #play_scene_btn").selectAll("img")
      .attr("class", "img_btn_enabled");

    var hasScenes = globals.scenes && globals.scenes.length;
    if (hasScenes) {
      selectWithParent("#record_scene_btn").attr("class", "img_btn_disabled");
      timeline_vis.tl_scale(globals.scenes[0].s_scale)
        .tl_layout(globals.scenes[0].s_layout)
        .tl_representation(globals.scenes[0].s_representation);
    }

    updateRadioBttns(timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());

    determineSize(data, timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());

    adjustSvgSize();

    globals.global_min_start_date = data.min_start_date;
    globals.global_max_end_date = data.max_end_date;

    main_svg.datum(data)
      .call(timeline_vis.duration(instance._getAnimationStepDuration()).height(globals.height).width(globals.width));

    // TODO: This should move into each of the chart renderers when we have some time
    instance._hideError();
    instance._main_svg.style("opacity", 1);

    if (hasScenes) {
      instance._currentSceneIndex = 0;
      changeScene(0);
    }

    if (globals.legend_panel) {
      globals.legend_panel.remove();
      globals.legend_panel = undefined;
    }

    if (globals.num_categories <= 12 && globals.num_categories > 1) {
      // setup legend
      globals.legend_panel = main_svg.append("svg")
        .attr("height", 35 + globals.track_height * (globals.num_categories + 1) + 5)
        .attr("width", globals.max_legend_item_width + 10 + globals.unit_width + 10 + 20)
        .attr("y", 100)
        .attr("id", "legend_panel")
        .attr("class", "legend")
        .on("mouseover", function () {
          // if (selectAllWithParent("foreignObject")[0].length === 0) {
          //   addLegendColorPicker();
          // }
          d3.select(this).select(".legend_rect").attr("filter", "url(#drop-shadow)");
          d3.select(this).select("#legend_expand_btn").style("opacity", 1);
        })
        .on("mouseout", function () {
          d3.select(this).select(".legend_rect").attr("filter", "none");
          d3.select(this).select("#legend_expand_btn").style("opacity", 0.1);
        })
        .call(legendDrag);

      globals.legend_panel.append("rect")
        .attr("class", "legend_rect")
        .attr("height", globals.track_height * (globals.num_categories + 1))
        .attr("width", globals.max_legend_item_width + 5 + globals.unit_width + 10)
        .append("title")
        .text("Click on a color swatch to select a custom color for that category.");

      globals.legend_panel.append("svg:image")
        .attr("id", "legend_expand_btn")
        .attr("x", globals.max_legend_item_width + 5 + globals.unit_width - 10)
        .attr("y", 0)
        .attr("width", 20)
        .attr("height", 20)
        .attr("xlink:href", imageUrls("min.png"))
        .style("cursor", "pointer")
        .style("opacity", 0.1)
        .on("click", function () {
          if (globals.legend_expanded) {
            instance.collapseLegend();
          } else {
            instance.expandLegend();
          }
        })
        .append("title")
        .text("Expand / collapse legend.");

      var legendElementContainer = globals.legend_panel.selectAll(".legend_element_g").data(globals.categories.domain().sort());
      globals.legend = legendElementContainer
        .enter()
        .append("g")
        .attr("class", "legend_element_g");

      // Remove the element when not data bound.
      legendElementContainer.exit().remove();

      globals.legend.append("title")
        .text(function (d) {
          return d;
        });

      globals.legend.attr("transform", function (d, i) {
        return ("translate(0," + (35 + (i + 1) * globals.track_height) + ")");
      });

      globals.legend.on("mouseover", function (d) {
        var hovered_legend_element = d;

        //logEvent("legend hover: " + hovered_legend_element, "legend");

        d3.select(this).select("rect").style("stroke", "#f00");
        d3.select(this).select("text").style("font-weight", "bolder")
          .style("fill", "#f00");
        selectAllWithParent(".timeline_event_g").each(function (d) { // eslint-disable-line no-shadow
          if (d.category === hovered_legend_element || d.selected) {
            d3.select(this).selectAll(".event_span")
              .style("stroke", "#f00")
              .style("stroke-width", "1.25px")
              .attr("filter", "url(#drop-shadow)");
            d3.select(this).selectAll(".event_span_component")
              .style("stroke", "#f00")
              .style("stroke-width", "1px");
          } else {
            d3.select(this).selectAll(".event_span")
              .attr("filter", "url(#greyscale)");
            d3.select(this).selectAll(".event_span_component")
              .attr("filter", "url(#greyscale)");
          }
        });
      });

      globals.legend.on("mouseout", function (d) {
        d3.select(this).select("rect").style("stroke", "#fff");
        d3.select(this).select("text").style("font-weight", "normal")
          .style("fill", "#666");
        selectAllWithParent(".timeline_event_g").each(function () {
          d3.select(this).selectAll(".event_span")
            .style("stroke", "#fff")
            .style("stroke-width", "0.25px")
            .attr("filter", "none");
          d3.select(this).selectAll(".event_span_component")
            .style("stroke", "#fff")
            .style("stroke-width", "0.25px")
            .attr("filter", "none");
          if (d.selected) {
            d3.select(this)
              .selectAll(".event_span")
              .attr("filter", "url(#drop-shadow)")
              .style("stroke", "#f00")
              .style("stroke-width", "1.25px");
            d3.select(this)
              .selectAll(".event_span_component")
              .style("stroke", "#f00")
              .style("stroke-width", "1px");
          }
        });
      });

      globals.legend.append("rect")
        .attr("class", "legend_element")
        .attr("x", globals.legend_spacing)
        .attr("y", 2)
        .attr("width", globals.legend_rect_size)
        .attr("height", globals.legend_rect_size)
        .attr("transform", "translate(0,-35)")
        .style("fill", globals.categories)
        .on("click", function (d, i) {
          var colorEle = this;
          instance._colorPicker.show(this, globals.categories(d), function (value) {
            // Update the display
            selectWithParent(".legend").selectAll(".legend_element_g rect").each(function () {
              if (this.__data__ === d) {
                d3.select(colorEle).style("fill", value);
              }
            });

            instance.setCategoryColor(d, i, value);

            if (main_svg && timeline_vis) {
              main_svg.call(timeline_vis.duration(instance._getAnimationStepDuration()));
            }
          });
        })
        .append("title");

      globals.legend.append("text")
        .attr("class", "legend_element")
        .attr("x", globals.legend_rect_size + 2 * globals.legend_spacing)
        .attr("y", globals.legend_rect_size - globals.legend_spacing)
        .attr("dy", 3)
        .style("fill-opacity", "1")
        .style("display", "inline")
        .attr("transform", "translate(0,-35)")
        .text(function (d) {
          return d;
        });

      globals.legend_panel.append("text")
        .text("LEGEND")
        .attr("class", "legend_title")
        .attr("dy", "1.4em")
        .attr("dx", "0em")
        .attr("transform", "translate(5,0)rotate(0)");
    }

    return new Promise(resolve => {
      if (timeline_vis.renderComplete) {
        timeline_vis.renderComplete.then(resolve);
      } else {
        resolve();
      }
    });
  }

  instance._drawTimeline = drawTimeline;

  /**

  --------------------------------------------------------------------------------------
  TIMELINE DATA PROCESSING UTILITY FUNCTIONS
  --------------------------------------------------------------------------------------
  **/

  function parseDates(data) {
    var i = 0;

    // parse the event dates
    // assign an end date if none is provided
    data.forEach(function (item) {
      item.event_id = i;
      globals.active_event_list.push(i);
      i++;

      // if there are numerical dates before -9999 or after 10000, don't attempt to parse them
      if (globals.date_granularity === "epochs") {
        return;
      }

      instance._parseStartAndEndDates(item);

      globals.active_event_list.push(item.event_id);
      globals.prev_active_event_list.push(item.event_id);
      globals.all_event_ids.push(item.event_id);
    });
  }

  // sort events according to start / end dates
  function compareAscending(item1, item2) {
    // Every item must have two fields: 'start_date' and 'end_date'.
    var result = item1.start_date - item2.start_date;

    // later first
    if (result < 0) {
      return -1;
    }
    if (result > 0) {
      return 1;
    }

    // shorter first
    result = item2.end_date - item1.end_date;
    if (result < 0) {
      return -1;
    }
    if (result > 0) {
      return 1;
    }

    // categorical tie-breaker
    if (item1.category < item2.category) {
      return -1;
    }
    if (item1.category > item2.category) {
      return 1;
    }

    // facet tie-breaker
    if (item1.facet < item2.facet) {
      return -1;
    }
    if (item1.facet > item2.facet) {
      return 1;
    }
    return 0;
  }

  // assign a track to each event item to prevent event overlap
  function assignTracks(data, tracks, layout) {
    // reset tracks first
    if (data && data.length) {
      data.forEach(function (item) {
        item.track = 0;
      });

      var i, track, min_width, effective_width;

      if (globals.date_granularity !== "epochs") {
        data.min_start_date = d3.min(data, function (d) {
          return d.start_date;
        });
        data.max_start_date = d3.max(data, function (d) {
          return d.start_date;
        });
        data.max_end_date = d3.max(data, function (d) {
          return d.end_date;
        });

        if (globals.width > (instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth())) {
          effective_width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
        } else {
          effective_width = globals.width;
        }


        var w = (effective_width - globals.padding.left - globals.padding.right - globals.unit_width),
          d = (data.max_end_date.getTime() - data.min_start_date.getTime());

        if (globals.segment_granularity === "days") {
          min_width = 0;
        } else if (layout === "Segmented") {
          min_width = 0;
        } else {
          min_width = (d / w * globals.unit_width);
        }
      }

      // older items end deeper
      data.forEach(function (item) {
        if (globals.date_granularity === "epochs") {
          item.track = 0;
        } else {
          for (i = 0, track = 0; i < tracks.length; i++, track++) {
            if (globals.segment_granularity === "days") {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "weeks") {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "months") {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "years") {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "decades" && globals.date_granularity === "days" && data.max_duration < 31) {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "centuries" && globals.date_granularity === "days" && data.max_duration < 31) {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (globals.segment_granularity === "millenia") {
              if (item.start_date.getTime() > tracks[i].getTime()) {
                break;
              }
            } else if (item.start_date.getTime() > tracks[i].getTime()) {
              break;
            }
          }
          item.track = track;

          if (min_width > item.end_date.getTime() - item.start_date.getTime()) {
            tracks[track] = moment(item.end_date.getTime() + min_width).toDate();
          } else {
            tracks[track] = item.end_date;
          }
        }
      });

      globals.num_tracks = d3.max(data, function (d) { // eslint-disable-line no-shadow
        return d.track;
      });
    } else {
      globals.num_tracks = 0;
    }
  }

  // assign a track to each event item to prevent event overlap
  function assignSequenceTracks(data) {
    var angle = 0,
      j = 0;

    // reset tracks and indices first, assign spiral coordinates
    data.forEach(function (item) {
      item.item_index = j;
      if (!globals.dirty_curve) {
        item.curve_x = (j * globals.spiral_padding) % (globals.width - globals.margin.left - globals.margin.right - globals.spiral_padding - globals.unit_width);
        item.curve_y = Math.floor((j * globals.spiral_padding) / (globals.width - globals.margin.left - globals.margin.right - globals.spiral_padding - globals.unit_width)) * globals.spiral_padding;
      }
      item.seq_track = 0;
      item.seq_index = 0;
      var radius = Math.sqrt(j + 1);
      angle += Math.asin(1 / radius);
      j++;
      item.spiral_index = j;
      item.spiral_x = Math.cos(angle) * (radius * globals.spiral_padding);
      item.spiral_y = Math.sin(angle) * (radius * globals.spiral_padding);
    });

    globals.max_item_index = d3.max(data, function (d) { return d.item_index; });

    var index = 0;
    if (globals.date_granularity !== "epochs") {
      globals.latest_start_date = data[0].start_date.getTime();
    }

    // older items end deeper
    data.forEach(function (item) {
      item.seq_index = index;
      item.seq_track = 0;
      index++;
    });

    globals.num_seq_tracks = d3.max(data, function (d) {
      return d.seq_track;
    });
  }

  // analyze each facet individually and assign within-facet tracks and relative start and end dates
  function processFacets() {
    globals.max_end_age = 0;
    globals.max_num_tracks = 0;
    globals.max_num_seq_tracks = 0;

    // calculate derived age measure for each event in each timeline
    globals.timeline_facets.forEach(function (timeline) {
      // determine maximum number of tracks for chronological and sequential scales
      assignTracks(timeline.values, [], "Faceted");
      assignSequenceTracks(timeline.values);
      timeline.values.num_tracks = d3.max(timeline.values, function (d) {
        return d.track;
      });
      timeline.values.num_seq_tracks = d3.max(timeline.values, function (d) {
        return d.seq_track;
      });

      if (timeline.values.num_tracks > globals.max_num_tracks) {
        globals.max_num_tracks = timeline.values.num_tracks + 1;
      }

      if (timeline.values.num_seq_tracks > globals.max_num_seq_tracks) {
        globals.max_num_seq_tracks = timeline.values.num_seq_tracks + 1;
      }

      timeline.values.min_start_date = d3.min(timeline.values, function (d) {
        return d.start_date;
      });

      var angle = 0;
      var i = 0;

      timeline.values.forEach(function (item) {
        // assign spiral coordinates
        var radius = Math.sqrt(i + 1);
        angle += Math.asin(1 / radius);
        i++;
        item.spiral_index = i;
        item.spiral_x = Math.cos(angle) * (radius * globals.spiral_padding);
        item.spiral_x = Math.sin(angle) * (radius * globals.spiral_padding);

        if (globals.date_granularity === "epochs") {
          item.start_age = item.start_date - timeline.values.min_start_date;
          item.start_age_label = "";
          item.end_age = item.end_date - timeline.values.min_start_date;
          item.end_age_label = "";
        } else {
          item.start_age = item.start_date - timeline.values.min_start_date;
          item.start_age_label = moment(timeline.values.min_start_date).from(moment(item.start_date), true);
          item.end_age = item.end_date - timeline.values.min_start_date;
          item.end_age_label = moment(timeline.values.min_start_date).from(moment(item.end_date), true);
        }
      });
      timeline.values.max_end_age = d3.max(timeline.values, function (d) {
        return d.end_age;
      });

      if (timeline.values.max_end_age > globals.max_end_age) {
        globals.max_end_age = timeline.values.max_end_age;
      }
    });
  }

  function getSegmentGranularity(min_date, max_date) {
    if (min_date === undefined || max_date === undefined) {
      return "";
    }

    var timeline_range,  // limit the number of facets to less than 20, rounding up / down to nearest natural temporal boundary
      days_to_years; // flag for transitioning to granularities of years or longer

    if (globals.date_granularity === "days") {
      timeline_range = time.day.count(time.day.floor(min_date), time.day.floor(max_date));

      if (timeline_range <= 7) {
        return "days";
      } else if (timeline_range > 7 && timeline_range <= 42) {
        return "weeks";
      } else if (timeline_range > 42 && timeline_range <= 732) {
        return "months";
      }
      days_to_years = true;
    }
    if (globals.date_granularity === "years" || days_to_years) {
      timeline_range = max_date.getUTCFullYear() - min_date.getUTCFullYear();

      if (timeline_range <= 10) {
        return "years";
      } else if (timeline_range > 10 && timeline_range <= 100) {
        return "decades";
      } else if (timeline_range > 100 && timeline_range <= 1000) {
        return "centuries";
      }
      return "millenia";
    } else if (globals.date_granularity === "epochs") {
      return "epochs";
    }
  }

  function getSegment(item) {
    var segment = "";

    switch (globals.segment_granularity) {
      case "days":
        segment = moment(item.end_date).format("MMM Do");
        break;
      case "weeks":
        segment = moment(item).format("WW / YY");
        break;
      case "months":
        segment = moment(item).format("MM-YY (MMM)");
        break;
      case "years":
        segment = moment(item).format("YYYY");
        break;
      case "decades":
        segment = (Math.floor(item.getUTCFullYear() / 10) * 10).toString() + "s";
        break;
      case "centuries":
        segment = (Math.floor(item.getUTCFullYear() / 100) * 100).toString() + "s";
        break;
      case "millenia":
        segment = (Math.floor(item.getUTCFullYear() / 1000) * 1000).toString() + " - " + (Math.ceil((item.getUTCFullYear() + 1) / 1000) * 1000).toString();
        break;
      case "epochs":
      default:
        segment = "";
        break;
    }
    return segment;
  }

  function getSegmentList(start_date, end_date) {
    var segments_domain = [];
    switch (globals.segment_granularity) {

      case "days":
        var day_array = d3.time.days(start_date, end_date);
        day_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "weeks":
        var week_array = d3.time.weeks(d3.time.week.floor(start_date), d3.time.week.ceil(end_date));
        week_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "months":
        var month_array = d3.time.months(d3.time.month.floor(start_date), d3.time.month.ceil(end_date));
        month_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "years":
        var year_array = d3.time.years(d3.time.year.floor(start_date), d3.time.year.ceil(end_date));
        year_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "decades":
        var min_decade_start_date = d3.time.year.floor(start_date);
        var min_decade_offset = start_date.getUTCFullYear() % 10;
        if (min_decade_offset < 0) {
          min_decade_offset += 10;
        }
        min_decade_start_date.setUTCFullYear(start_date.getUTCFullYear() - min_decade_offset);
        var decade_array = d3.time.years(d3.time.year.floor(min_decade_start_date), d3.time.year.ceil(end_date), 10);
        decade_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "centuries":
        var min_century_start_date = d3.time.year.floor(start_date);
        var min_century_offset = start_date.getUTCFullYear() % 100;
        if (min_century_offset < 0) {
          min_century_offset += 100;
        }
        min_century_start_date.setUTCFullYear(start_date.getUTCFullYear() - min_century_offset);
        var century_array = d3.time.years(d3.time.year.floor(min_century_start_date), d3.time.year.ceil(end_date), 100);
        century_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "millenia":
        var min_millenia_start_date = d3.time.year.floor(start_date);
        var min_millenia_offset = start_date.getUTCFullYear() % 1000;
        if (min_millenia_offset < 0) {
          min_millenia_offset += 1000;
        }
        min_millenia_start_date.setUTCFullYear(start_date.getUTCFullYear() - min_millenia_offset);
        var millenia_array = d3.time.years(d3.time.year.floor(min_millenia_start_date), d3.time.year.ceil(end_date), 1000);
        millenia_array.forEach(function (d) {
          segments_domain.push(getSegment(d));
        });
        break;

      case "epochs":
        segments_domain = [""];
        break;
      default:
        break;
    }
    return segments_domain;
  }

  // resizes the timeline container based on combination of scale, layout, representation
  function determineSize(data, scale, layout, representation) {
    //logEvent("timeline: " + scale + " - " + layout + " - " + representation, "sizing");

    switch (representation) {

      case "Linear":
        switch (scale) {

          case "Chronological":
            switch (layout) {

              case "Unified":
                // justifiable
                assignTracks(data, [], layout);
                //logEvent("# tracks: " + globals.num_tracks, "sizing");

                globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
                globals.height = globals.num_tracks * globals.track_height + 1.5 * globals.track_height + globals.margin.top + globals.margin.bottom;
                break;

              case "Faceted":
                // justifiable
                processFacets();
                //logEvent("# within-facet tracks: " + (globals.max_num_tracks + 1), "sizing");

                globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
                globals.height = (globals.max_num_tracks * globals.track_height + 1.5 * globals.track_height) * globals.num_facets + globals.margin.top + globals.margin.bottom;
                break;

              case "Segmented":
                // justifiable
                assignTracks(data, [], layout);
                //logEvent("# tracks: " + globals.num_tracks, "sizing");

                globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
                globals.height = (globals.num_tracks * globals.track_height + 1.5 * globals.track_height) * globals.num_segments + globals.margin.top + globals.margin.bottom;
                break;
              default:
                break;
            }
            break;

          case "Relative":
            if (layout === "Faceted") {
              // justifiable
              processFacets();
              //logEvent("# within-facet tracks: " + (globals.max_num_tracks + 1), "sizing");

              globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
              globals.height = (globals.max_num_tracks * globals.track_height + 1.5 * globals.track_height) * globals.num_facets + globals.margin.top + globals.margin.bottom;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;

          case "Log":
            if (layout === "Unified") {
              // justifiable
              assignTracks(data, [], layout);
              //logEvent("# tracks: " + globals.num_tracks, "sizing");

              globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
              globals.height = globals.num_tracks * globals.track_height + 1.5 * globals.track_height + globals.margin.top + globals.margin.bottom;
            } else if (layout === "Faceted") {
              // justifiable
              processFacets();
              //logEvent("# within-facet tracks: " + (globals.max_num_tracks + 1), "sizing");

              globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
              globals.height = (globals.max_num_tracks * globals.track_height + 1.5 * globals.track_height) * globals.num_facets + globals.margin.top + globals.margin.bottom;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;

          case "Collapsed":
            if (layout === "Unified") {
              // justifiable
              assignSequenceTracks(data);
              globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
              var bar_chart_height = (4 * globals.unit_width);
              globals.width = globals.max_seq_index * 1.5 * globals.unit_width + globals.margin.left + 3 * globals.margin.right;
              globals.height = (globals.num_seq_tracks * globals.track_height + 1.5 * globals.track_height) + bar_chart_height + globals.margin.top + globals.margin.bottom;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;

          case "Sequential":
            if (layout === "Unified") {
              // justifiable
              assignSequenceTracks(data);
              globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
              globals.width = d3.max([
                globals.max_seq_index * 1.5 * globals.unit_width + globals.margin.left + globals.margin.right,
                instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth()
              ]);
              globals.height = globals.num_seq_tracks * globals.track_height + 1.5 * globals.track_height + globals.margin.top + globals.margin.bottom;
            } else if (layout === "Faceted") {
              // justifiable
              processFacets();
              globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
              globals.width = d3.max([
                globals.max_seq_index * 1.5 * globals.unit_width + globals.margin.left + globals.margin.right,
                instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth()
              ]);
              globals.height = (globals.max_num_seq_tracks * globals.track_height + 1.5 * globals.track_height) * globals.num_facets + globals.margin.top + globals.margin.bottom;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;
          default:
            break;
        }
        break;

      case "Radial":

        globals.centre_radius = 50;

        var effective_size = instance._render_width - globals.margin.right - globals.padding.right - globals.margin.left - globals.padding.left - getScrollbarWidth();

        switch (scale) {

          case "Chronological":

            switch (layout) {

              case "Unified":
                // justifiable
                assignTracks(data, [], layout);
                //logEvent("# tracks: " + globals.num_tracks, "sizing");

                globals.centre_radius = d3.max([50, (effective_size - ((globals.num_tracks + 2) * 2 * globals.track_height)) / 2]);
                globals.width = (2 * globals.centre_radius + (globals.num_tracks + 2) * 2 * globals.track_height) + globals.margin.left + globals.margin.right;
                if (globals.centre_radius > 200) { globals.centre_radius = 200; }
                globals.height = (2 * globals.centre_radius + (globals.num_tracks + 2) * 2 * globals.track_height) + globals.margin.top + globals.margin.bottom;
                break;

              case "Faceted":
                // justifiable
                processFacets();

                globals.centre_radius = 50;
                var estimated_facet_width = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height);

                globals.num_facet_cols = d3.max([1, d3.min([globals.num_facet_cols, Math.floor(effective_size / estimated_facet_width)])]);
                globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

                globals.centre_radius = d3.max([50, (effective_size / globals.num_facet_cols - ((globals.max_num_tracks + 2) * 2 * globals.track_height)) / 2]);
                globals.width = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height) * globals.num_facet_cols + globals.margin.left + globals.margin.right;
                if (globals.centre_radius > 200) { globals.centre_radius = 200; }
                globals.height = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height) * globals.num_facet_rows + globals.margin.top + globals.margin.bottom + globals.num_facet_rows * globals.buffer;
                break;

              case "Segmented":
                // justifiable
                assignTracks(data, [], layout);
                //logEvent("# tracks: " + globals.num_tracks, "sizing");

                globals.centre_radius = 50;
                var estimated_segment_width = (2 * globals.centre_radius + (globals.num_tracks + 2) * 2 * globals.track_height);

                globals.num_segment_cols = d3.max([1, d3.min([globals.num_segment_cols, Math.floor(effective_size / estimated_segment_width)])]);
                globals.num_segment_rows = Math.ceil(globals.num_segments / globals.num_segment_cols);

                globals.centre_radius = d3.max([50, (effective_size / globals.num_segment_cols - ((globals.num_tracks + 2) * 2 * globals.track_height)) / 2]);
                globals.width = (2 * globals.centre_radius + (globals.num_tracks + 2) * 2 * globals.track_height) * globals.num_segment_cols + globals.margin.left + globals.margin.right;
                if (globals.centre_radius > 200) {
                  globals.centre_radius = 200;
                }
                globals.height = (2 * globals.centre_radius + (globals.num_tracks + 2) * 2 * globals.track_height) * globals.num_segment_rows + globals.margin.top + globals.margin.bottom + globals.num_segment_rows * globals.buffer;
                break;
              default:
                break;
            }
            break;

          case "Relative":
            if (layout === "Faceted") {
              // justifiable
              processFacets();
              //logEvent("# within-facet tracks: " + (globals.max_num_tracks + 1), "sizing");

              globals.centre_radius = 50;
              estimated_facet_width = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height);

              globals.num_facet_cols = d3.min([globals.num_facet_cols, Math.floor(effective_size / estimated_facet_width)]);
              globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

              globals.centre_radius = d3.max([50, (effective_size / globals.num_facet_cols - ((globals.max_num_tracks + 2) * 2 * globals.track_height)) / 2]);
              globals.width = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height) * globals.num_facet_cols + globals.margin.left + globals.margin.right;
              if (globals.centre_radius > 200) {
                globals.centre_radius = 200;
              }
              globals.height = (2 * globals.centre_radius + (globals.max_num_tracks + 2) * 2 * globals.track_height) * globals.num_facet_rows + globals.margin.top + globals.margin.bottom + globals.num_facet_rows * globals.buffer;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;

          case "Sequential":
            if (layout === "Unified") {
              // justifiable
              assignSequenceTracks(data);
              globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
              globals.centre_radius = (effective_size - (4 * globals.track_height)) / 2;
              globals.width = (2 * globals.centre_radius + 4 * globals.track_height) + globals.margin.left + globals.margin.right;
              if (globals.centre_radius > 200) {
                globals.centre_radius = 200;
              }
              globals.height = (2 * globals.centre_radius + 4 * globals.track_height) + globals.margin.top + globals.margin.bottom;
            } else if (layout === "Faceted") {
              // justifiable

              processFacets();
              globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;

              globals.centre_radius = 50;
              estimated_facet_width = (2 * globals.centre_radius + (4 * globals.track_height));

              globals.num_facet_cols = d3.min([globals.num_facet_cols, Math.floor(effective_size / estimated_facet_width)]);
              globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

              globals.centre_radius = d3.max([50, (effective_size / globals.num_facet_cols - (4 * globals.track_height)) / 2]);
              globals.width = (2 * globals.centre_radius + 4 * globals.track_height) * globals.num_facet_cols + globals.margin.left + globals.margin.right;
              if (globals.centre_radius > 200) {
                globals.centre_radius = 200;
              }
              globals.height = (2 * globals.centre_radius + 4 * globals.track_height) * globals.num_facet_rows + globals.margin.top + globals.margin.bottom + globals.num_facet_rows * globals.buffer;
            } else {
              // not justifiable
              //logEvent("scale-layout-representation combination not possible/justifiable", "error");

              globals.width = 0;
              globals.height = 0;
            }
            break;
          default:
            break;
        }
        break;

      case "Grid":

        if (scale === "Chronological" && layout === "Segmented") {
          // justifiable

          assignTracks(data, [], layout);

          var cell_size = 50,
            century_height = cell_size * globals.unit_width,
            century_width = cell_size * 10;

          // determine the range, round to whole centuries
          var range_floor = Math.floor(data.min_start_date.getUTCFullYear() / 100) * 100,
            range_ceil = Math.ceil((data.max_end_date.getUTCFullYear() + 1) / 100) * 100;

          // determine the time domain of the data along a linear quantitative scale
          var year_range = d3.range(range_floor, range_ceil);

          // determine maximum number of centuries given year_range
          var num_centuries = (Math.ceil(year_range.length / 100));

          globals.width = century_width + globals.margin.left + globals.margin.right;
          globals.height = num_centuries * century_height + num_centuries * cell_size + globals.margin.top + globals.margin.bottom - cell_size;
        } else {
          // not justifiable
          //logEvent("scale-layout-representation combination not possible/justifiable", "error");

          globals.width = 0;
          globals.height = 0;
        }
        break;

      case "Calendar":

        if (scale === "Chronological" && layout === "Segmented") {
          // justifiable

          assignTracks(data, [], layout);

          cell_size = 20;
          var year_height = cell_size * 8, // 7 days of week + buffer
            year_width = cell_size * 53; // 53 weeks of the year + buffer

          // determine the range, round to whole centuries
          range_floor = data.min_start_date.getUTCFullYear();
          range_ceil = data.max_end_date.getUTCFullYear();

          // determine the time domain of the data along a linear quantitative scale
          year_range = d3.range(range_floor, range_ceil + 1);

          globals.width = year_width + globals.margin.left + globals.margin.right;
          globals.height = year_range.length * year_height + globals.margin.top + globals.margin.bottom - cell_size;
        } else {
          // not justifiable
          //logEvent("scale-layout-representation combination not possible/justifiable", "error");

          globals.width = 0;
          globals.height = 0;
        }
        break;

      case "Spiral":

        if (scale === "Sequential") {
          if (layout === "Unified") {
            // justifiable

            assignSequenceTracks(data);
            globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
            var angle = 0,
              i = 0;

            data.forEach(function (item) {
              var radius = Math.sqrt(i + 1);
              angle += Math.asin(1 / radius);
              i++;
              item.spiral_index = i;
              item.spiral_x = Math.cos(angle) * (radius * globals.spiral_padding);
              item.spiral_y = Math.sin(angle) * (radius * globals.spiral_padding);
            });

            var max_x = d3.max(data, function (d) { return d.spiral_x; });
            var max_y = d3.max(data, function (d) { return d.spiral_y; });
            var min_x = d3.min(data, function (d) { return d.spiral_x; });
            var min_y = d3.min(data, function (d) { return d.spiral_y; });

            globals.spiral_dim = d3.max([(max_x + 2 * globals.spiral_padding) - (min_x - 2 * globals.spiral_padding), (max_y + 2 * globals.spiral_padding) - (min_y - 2 * globals.spiral_padding)]);

            globals.width = d3.max([
              globals.spiral_dim + globals.spiral_padding + globals.margin.right + globals.margin.left,
              instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth()
            ]);

            // USES EFFECTIVE_HEIGHT
            globals.height = d3.max([
              globals.spiral_dim + globals.spiral_padding + globals.margin.top + globals.margin.bottom,
              instance._render_height - globals.margin.top - globals.margin.bottom - getScrollbarWidth()
            ]);
          } else if (layout === "Faceted") {
            // justifiable
            processFacets();
            globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;

            globals.timeline_facets.forEach(function (timeline) {
              angle = 0;
              i = 0;

              timeline.values.forEach(function (item) {
                var radius = Math.sqrt(i + 1);
                angle += Math.asin(1 / radius);
                i++;
                item.spiral_index = i;
                item.spiral_x = Math.cos(angle) * (radius * globals.spiral_padding);
                item.spiral_y = Math.sin(angle) * (radius * globals.spiral_padding);
              });
            });

            max_x = d3.max(data, function (d) { return d.spiral_x; });
            max_y = d3.max(data, function (d) { return d.spiral_y; });
            min_x = d3.min(data, function (d) { return d.spiral_x; });
            min_y = d3.min(data, function (d) { return d.spiral_y; });

            globals.spiral_dim = d3.max([(max_x + 2 * globals.spiral_padding) - (min_x - 2 * globals.spiral_padding), (max_y + 2 * globals.spiral_padding) - (min_y - 2 * globals.spiral_padding)]);

            effective_size = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();

            globals.num_facet_cols = d3.min([globals.num_facet_cols, Math.floor(effective_size / globals.spiral_dim)]);
            globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

            globals.width = d3.max([
              globals.num_facet_cols * globals.spiral_dim + globals.margin.right + globals.margin.left,
              instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth()
            ]);
            globals.height = globals.num_facet_rows * globals.spiral_dim + globals.margin.top + globals.margin.bottom;
          } else {
            // not justifiable
            globals.width = 0;
            globals.height = 0;
          }
        } else {
          // not justifiable
          //logEvent("scale-layout-representation combination not possible/justifiable", "error");

          globals.width = 0;
          globals.height = 0;
        }
        break;

      case "Curve":
        if (scale === "Sequential" && layout === "Unified") {
          // justifiable
          assignSequenceTracks(data);
          globals.max_seq_index = d3.max(data, function (d) { return d.seq_index; }) + 1;
          globals.width = instance._render_width - globals.margin.right - globals.margin.left - getScrollbarWidth();
          globals.height = instance._render_height - globals.margin.top - globals.margin.bottom - getScrollbarWidth();
        } else {
          // not justifiable
          //logEvent("scale-layout-representation combination not possible/justifiable", "error");

          globals.width = 0;
          globals.height = 0;
        }
        break;
      default:
        break;
    }
    //logEvent("dimensions: " + globals.width + " (W) x " + globals.height + " (H)", "sizing");
  }

  instance._determineSize = determineSize;

  function updateRadioBttns(scale, layout, representation) {
    // update the control radio buttons
    selectAllWithParent("#scale_picker input[name=scale_rb]").property("checked", function (d) {
      return d === scale;
    });
    selectAllWithParent("#layout_picker input[name=layout_rb]").property("checked", function (d) {
      return d === layout;
    });
    selectAllWithParent("#representation_picker input[name=representation_rb]").property("checked", function (d) {
      return d === representation;
    });

    selectAllWithParent("#scale_picker img")
      .style("border-bottom", function (d) {
        if (d.name === scale) { return "2px solid #f00"; }
      })
      .style("border-right", function (d) {
        if (d.name === scale) { return "2px solid #f00"; }
      });
    selectAllWithParent("#layout_picker img")
      .style("border-bottom", function (d) {
        if (d.name === layout) { return "2px solid #f00"; }
      })
      .style("border-right", function (d) {
        if (d.name === layout) { return "2px solid #f00"; }
      });
    selectAllWithParent("#representation_picker img")
      .style("border-bottom", function (d) {
        if (d.name === representation) { return "2px solid #f00"; }
      })
      .style("border-right", function (d) {
        if (d.name === representation) { return "2px solid #f00"; }
      });

    selectAllWithParent(".option_rb").select("input").property("disabled", function (d) {
      switch (d.name) {

        case "Chronological":
          return !(representation !== "Spiral" && representation !== "Curve");

        case "Relative":
          return !(layout === "Faceted" && (representation === "Linear" || representation === "Radial"));

        case "Log":
          return !(representation === "Linear" && layout !== "Segmented");

        case "Collapsed":
          return !(representation === "Linear" && layout === "Unified");

        case "Sequential":
          return !((representation !== "Grid" && representation !== "Calendar") && layout !== "Segmented");

        case "Unified":
          return !(scale !== "Relative" && representation !== "Grid" && representation !== "Calendar");

        case "Faceted":
          return !(scale !== "Collapsed" && representation !== "Grid" && representation !== "Calendar" && representation !== "Curve" && globals.total_num_facets > 1);

        case "Segmented":
          return !(scale === "Chronological" && representation !== "Spiral" && representation !== "Curve");

        case "Linear":
          return false;

        case "Calendar":
          return !(scale === "Chronological" && layout === "Segmented" && (["weeks", "months", "years", "decades"].indexOf(globals.segment_granularity) !== -1));

        case "Grid":
          return !(scale === "Chronological" && layout === "Segmented" && (["decades", "centuries", "millenia"].indexOf(globals.segment_granularity) !== -1));

        case "Radial":
          return !(scale !== "Log" && scale !== "Collapsed");

        case "Spiral":
          return !(scale === "Sequential" && (layout === "Unified" || layout === "Faceted"));

        case "Curve":
          return !(scale === "Sequential" && layout === "Unified");
        default:
          return;
      }
    });

    selectAllWithParent(".option_rb").select("img").attr("class", function (d) {
      switch (d.name) {
        case "Chronological":
          return (representation !== "Spiral" && representation !== "Curve") ? "img_btn_enabled" : "img_btn_disabled";
        case "Relative":
          return (layout === "Faceted" && (representation === "Linear" || representation === "Radial")) ? "img_btn_enabled" : "img_btn_disabled";
        case "Log":
          return (representation === "Linear" && layout !== "Segmented") ? "img_btn_enabled" : "img_btn_disabled";
        case "Collapsed":
          return (representation === "Linear" && layout === "Unified") ? "img_btn_enabled" : "img_btn_disabled";
        case "Sequential":
          return ((representation !== "Grid" && representation !== "Calendar") && layout !== "Segmented") ? "img_btn_enabled" : "img_btn_disabled";
        case "Unified":
          return (scale !== "Relative" && representation !== "Grid" && representation !== "Calendar") ? "img_btn_enabled" : "img_btn_disabled";
        case "Faceted":
          return (scale !== "Collapsed" && representation !== "Grid" && representation !== "Calendar" && representation !== "Curve" && globals.total_num_facets > 1) ? "img_btn_enabled" : "img_btn_disabled";
        case "Segmented":
          return (scale === "Chronological" && representation !== "Spiral" && representation !== "Curve") ? "img_btn_enabled" : "img_btn_disabled";
        case "Linear":
          return "img_btn_enabled";
        case "Calendar":
          return (scale === "Chronological" && layout === "Segmented" && (["weeks", "months", "years", "decades"].indexOf(globals.segment_granularity) !== -1)) ? "img_btn_enabled" : "img_btn_disabled";
        case "Grid":
          return (scale === "Chronological" && layout === "Segmented" && (["decades", "centuries", "millenia"].indexOf(globals.segment_granularity) !== -1)) ? "img_btn_enabled" : "img_btn_disabled";
        case "Radial":
          return (scale !== "Log" && scale !== "Collapsed") ? "img_btn_enabled" : "img_btn_disabled";
        case "Spiral":
          return (scale === "Sequential" && (layout === "Unified" || layout === "Faceted")) ? "img_btn_enabled" : "img_btn_disabled";
        case "Curve":
          return (scale === "Sequential" && layout === "Unified") ? "img_btn_enabled" : "img_btn_disabled";
        default:
          return;
      }
    });
  }

  // highlight matches and de-emphasize (grey-out) mismatches
  globals.dispatch.on("Emphasize", function (selected_categories, selected_facets, selected_segments) {
    var timeline_events = selectAllWithParent(".timeline_event_g");
    var matches, mismatches,
      selected_category_values = [],
      selected_facet_values = [],
      selected_segment_values = [];

    globals.prev_active_event_list = globals.active_event_list;

    globals.active_event_list = [];

    selected_categories[0].forEach(function (item) {
      selected_category_values.push(item.__data__);
    });

    selected_facets[0].forEach(function (item) {
      selected_facet_values.push(item.__data__);
    });

    selected_segments[0].forEach(function (item) {
      selected_segment_values.push(item.__data__);
    });

    mismatches = timeline_events.filter(function (d) {
      return (selected_category_values.indexOf("( All )") === -1 && selected_category_values.indexOf(d.category) === -1) ||
        (selected_facet_values.indexOf("( All )") === -1 && selected_facet_values.indexOf(d.facet) === -1) ||
        (selected_segment_values.indexOf("( All )") === -1 && selected_segment_values.indexOf(d.segment) === -1);
    });

    matches = timeline_events.filter(function (d) {
      return (selected_category_values.indexOf("( All )") !== -1 || selected_category_values.indexOf(d.category) !== -1) &&
        (selected_facet_values.indexOf("( All )") !== -1 || selected_facet_values.indexOf(d.facet) !== -1) &&
        (selected_segment_values.indexOf("( All )") !== -1 || selected_segment_values.indexOf(d.segment) !== -1);
    });

    // if (mismatches[0].length !== 0) {
    //   logEvent(matches[0].length + " out of " + (matches[0].length + mismatches[0].length) + " events", "Emphasize");
    // } else {
    //   logEvent(matches[0].length + " events", "Emphasize");
    // }

    globals.all_data.forEach(function (item) {
      if ((selected_category_values.indexOf("( All )") !== -1 || selected_category_values.indexOf(item.category) !== -1) &&
        (selected_facet_values.indexOf("( All )") !== -1 || selected_facet_values.indexOf(item.facet) !== -1) &&
        (selected_segment_values.indexOf("( All )") !== -1 || selected_segment_values.indexOf(item.segment) !== -1)) {
        globals.active_event_list.push(item.event_id);
      }
    });

    main_svg.call(timeline_vis.duration(instance._getAnimationStepDuration()));

    globals.prev_active_event_list = globals.active_event_list;
  });

  // remove mismatches
  globals.dispatch.on("remove", function (selected_categories, selected_facets, selected_segments) {
    instance.clearCanvas();

    const active_event_list = [];

    var matches, mismatches,
      selected_category_values = [],
      selected_facet_values = [],
      selected_segment_values = [],
      reset_segmented_layout = false;

    selected_categories[0].forEach(function (item) {
      selected_category_values.push(item.__data__);
    });

    selected_facets[0].forEach(function (item) {
      selected_facet_values.push(item.__data__);
    });

    selected_segments[0].forEach(function (item) {
      selected_segment_values.push(item.__data__);
    });

    globals.all_data.forEach(function (item) {
      if ((selected_category_values.indexOf("( All )") !== -1 || selected_category_values.indexOf(item.category) !== -1) &&
        (selected_facet_values.indexOf("( All )") !== -1 || selected_facet_values.indexOf(item.facet) !== -1) &&
        (selected_segment_values.indexOf("( All )") !== -1 || selected_segment_values.indexOf(item.segment) !== -1)) {
        active_event_list.push(item.event_id);
      }
    });

    mismatches = selectAllWithParent(".timeline_event_g").filter(function (d) {
      return active_event_list.indexOf(d.event_id) === -1;
    });

    matches = selectAllWithParent(".timeline_event_g").filter(function (d) {
      return active_event_list.indexOf(d.event_id) !== -1;
    });

    const active_data = globals.all_data.filter(function (d) {
      return active_event_list.indexOf(d.event_id) !== -1;
    });

    // We only support having at least on item.
    if (active_data.length > 0) {
      globals.prev_active_event_list = globals.active_event_list;
      globals.active_event_list = active_event_list;
      globals.active_data = active_data;

      // if (mismatches[0].length !== 0) {
      //   logEvent(matches[0].length + " out of " + (matches[0].length + mismatches[0].length) + " events", "remove");
      // } else {
      //   logEvent(matches[0].length + " events", "remove");
      // }

      measureTimeline(globals.active_data);

      globals.active_data.min_start_date = d3.min(globals.active_data, function (d) {
        return d.start_date;
      });
      globals.active_data.max_start_date = d3.max(globals.active_data, function (d) {
        return d.start_date;
      });
      globals.active_data.max_end_date = d3.max(globals.active_data, function (d) {
        return time.minute.floor(d.end_date);
      });

      globals.all_data.min_start_date = globals.active_data.min_start_date;
      globals.all_data.max_end_date = globals.active_data.max_end_date;

      globals.max_end_age = 0;

      // determine facets (separate timelines) from data
      globals.facets.domain(globals.active_data.map(function (d) {
        return d.facet;
      }));

      globals.facets.domain().sort();

      globals.num_facets = globals.facets.domain().length;
      globals.num_facet_cols = Math.ceil(Math.sqrt(globals.num_facets));
      globals.num_facet_rows = Math.ceil(globals.num_facets / globals.num_facet_cols);

      //logEvent("num facets: " + globals.num_facet_cols, "remove");

      if (timeline_vis.tl_layout() === "Segmented") {
        if (timeline_vis.tl_representation() === "Grid") {
          globals.segment_granularity = "centuries";
        } else if (timeline_vis.tl_representation() === "Calendar") {
          globals.segment_granularity = "weeks";
        } else {
          globals.segment_granularity = getSegmentGranularity(globals.global_min_start_date, globals.global_max_end_date);
        }
      }

      var segment_list = getSegmentList(globals.active_data.min_start_date, globals.active_data.max_end_date);

      globals.segments.domain(segment_list.map(function (d) {
        return d;
      }));

      //logEvent("segments (" + globals.segments.domain().length + "): " + globals.segments.domain(), "preprocessing");

      globals.num_segments = globals.segments.domain().length;
      globals.num_segment_cols = Math.ceil(Math.sqrt(globals.num_segments));
      globals.num_segment_rows = Math.ceil(globals.num_segments / globals.num_segment_cols);

      determineSize(globals.active_data, timeline_vis.tl_scale(), timeline_vis.tl_layout(), timeline_vis.tl_representation());

      //logEvent("num facets after sizing: " + globals.num_facet_cols, "remove");

      adjustSvgSize();

      main_svg.datum(globals.active_data)
        .call(timeline_vis.duration(instance._getAnimationStepDuration())
          .height(globals.height)
          .width(globals.width));

      instance._hideError();
      instance._main_svg.style("opacity", 1);

      if (reset_segmented_layout) {
        mismatches = selectAllWithParent(".timeline_event_g").filter(function (d) {
          return globals.active_event_list.indexOf(d.event_id) === -1;
        });

        matches = selectAllWithParent(".timeline_event_g").filter(function (d) {
          return globals.active_event_list.indexOf(d.event_id) !== -1;
        });
      }

      globals.prev_active_event_list = globals.active_event_list;
    } else {
      instance._main_svg.style("opacity", 0);
      instance._showError("No data available for the selected set of filters.");
    }
  });

  function importIntro() {
    var import_intro = introJs();
    var steps: any = [
      {
        intro: "This tour will describe the types of data that the tool can ingest."
      }
    ];

    if (showDemoData()) {
      steps.push({
        element: ".timeline_storyteller #demo_dataset_picker_label",
        intro: "Load one of several demonstration timeline datasets, featuring timelines that span astronomical epochs or just a single day.",
        position: "right"
      });
    }

    if (instance.options.showImportLoadDataOptions) {
      steps = steps.concat([
        {
          element: ".timeline_storyteller #json_picker_label",
          intro: "Load a timeline dataset in JSON format, where each event is specified by at least a start_date (in either YYYY, YYYY-MM, YYYY-MM-DD, or YYYY-MM-DD HH:MM format); optionally, events can also be specified by end_date, content_text (a text string that describes the event), category, and facet (a second categorical attribute used for distinguishing between multiple timelines).",
          position: "right"
        },
        {
          element: ".timeline_storyteller #csv_picker_label",
          intro: "Load a timeline dataset in CSV format; ensure that the header row contains at least a start_date column; as with JSON datasets, end_date, content_text, category, and facet columns are optional.",
          position: "right"
        },
        {
          element: ".timeline_storyteller #gdocs_picker_label",
          intro: "Load a timeline dataset from a published Google Spreadsheet; you will need to provide the spreadsheet key and worksheet title; the worksheet columns must be formatted as text.",
          position: "right"
        }
      ]);
    }

    if (showDemoData()) {
      steps.push({
        element: ".timeline_storyteller #story_demo_label",
        intro: "Load a demonstration timeline story.",
        position: "right"
      });
    }
    steps.push(
      {
        element: ".timeline_storyteller #story_picker_label",
        intro: "Load a previously saved timeline story in .cdc format.",
        position: "right"
      }
    );

    import_intro.setOptions({
      steps: steps
    });
    import_intro.start();
  }

  function mainIntro() {
    var main_intro = introJs();
    var steps: any = [
      {
        intro: "This tour will introduce the timeline story authoring features."
      }
    ];

    if (instance.options.showViewOptions !== false) {
      steps = steps.concat([
        {
          element: "#representation_picker",
          intro: "Select the visual representation of the timeline or timelines here. Note that some representations are incompatible with some combinations of scales and layouts.",
          position: "bottom"
        },
        {
          element: "#scale_picker",
          intro: "Select the scale of the timeline or timelines here. Note that some scales are incompatible with some combinations of representations and layouts.",
          position: "bottom"
        },
        {
          element: "#layout_picker",
          intro: "Select the layout of the timeline or timelines here. Note that some layouts are incompatible with some combinations of representations and scales.",
          position: "bottom"
        }
      ]);
    }

    if (instance.options.showImportOptions !== false) {
      steps.push(
        {
          element: "#import_visible_btn",
          intro: "This button toggles the import panel, allowing you to open a different timeline dataset or story.",
          position: "right"
        });
    }

    steps = steps.concat([
      {
        element: "#control_panel",
        intro: "This panel contains controls for adding text or image annotations to a timeline, for highlighting and filtering events, and for exporting the timeline or timeline story.",
        position: "right"
      },
      {
        element: "#record_scene_btn",
        intro: "This button records the current canvas of timeline or timelines, labels, and annotations as a scene in a story.",
        position: "top"
      }]);

    main_intro.setOptions({
      steps: steps
    });

    main_intro.start();
  }

  function playbackIntro() {
    var playback_intro = introJs();
    playback_intro.setOptions({
      steps: [
        {
          intro: "This tour will introduce timeline story plaback features."
        },
        {
          element: "#play_scene_btn",
          intro: "You are now in story playback mode. Click this button to leave playback mode and restore the story editing tool panels.",
          position: "top"
        },
        {
          element: "#stepper_container",
          intro: "Scenes in the story appear in this panel. Click on any scene thumbnail to jump to the corresponding scene.",
          position: "top"
        },
        {
          element: "#next_scene_btn",
          intro: "Advance to the next scene by clicking this button.",
          position: "top"
        },
        {
          element: "#prev_scene_btn",
          intro: "Return to the previous scene by clicking this button.",
          position: "top"
        }
      ]
    });
    playback_intro.start();
  }


  selectWithParent()
    .append("div")
    .attr("id", "hint_div")
    .attr("data-hint", "Click on the [TOUR] button for a tour of the interface.")
    .attr("data-hintPosition", "bottom-left")
    .attr("data-position", "bottom-left-aligned")
    .attr("class", "control_div");

  var intro_div = selectWithParent("#hint_div")
    .append("div")
    .attr("id", "intro_div");

  // Give it some time to load, then initialize the hints, otherwise the positioning is wierd
  setTimeout(function () {
    introJs().addHints();
  }, 100);

  intro_div.append("input")
    .attr({
      type: "image",
      name: "Start tour",
      id: "start_intro_btn",
      class: "img_btn_enabled",
      src: imageUrls("info.png"),
      height: 30,
      width: 30,
      title: "Start tour"
    })
    .on("click", function () {
      if (instance.importPanel.visible) {
        importIntro();
      } else if (!instance.playback_mode) {
        mainIntro();
      } else {
        playbackIntro();
      }
    });

  intro_div.append("div")
    .attr("class", "intro_btn")
    .html("<a title='About & getting started' href='../../' target='_blank'><img src='" + imageUrls("q.png") + "' width=30 height=30 class='img_btn_enabled'></img></a>");

  intro_div.append("div")
    .attr("class", "intro_btn")
    .html("<a title='Contact the project team' href='mailto:timelinestoryteller@microsoft.com' target='_top'><img src='" + imageUrls("mail.png") + "' width=30 height=30 class='img_btn_enabled'></img></a>");

  /**
   * Sets the color for the given category
   * @param {string} category The category to change
   * @param {number} categoryIndex The index of the category
   * @param {string} value The category color
   * @returns {void}
   */
  this._setCategoryColor = function (category, categoryIndex, value) {
    globals.color_swap_target = globals.categories.range().indexOf(globals.categories(category));
    //log("category " + categoryIndex + ": " + category + " / " + value + " (index # " + globals.color_swap_target + ")");

    setScaleValue(globals.categories, category, value);

    globals.use_custom_palette = true;
  };

  /**
   * Loads the data from the given state
   * @param {object} state The state to load data from
   * @param {number} min_story_height The minimum height to show the story
   * @returns {void}
   */
  this._loadTimelineFromState = function (state, min_story_height) {
    var timelineData = state.timeline_json_data;
    var hasScenes = !!(state.scenes && state.scenes.length);

    /**
     * {
     *    timeline_json_data: ...,
     *    scenes: ...,
     *    caption_list: ...,
     *    image_list: ...,
     *    annotation_list: ...,
     *    width: ...,
     *    height: ...
     * }
     */

    globals.timeline_json_data = timelineData;
    if (hasScenes) {
      if (state.color_palette !== undefined) {
        globals.color_palette = state.color_palette;
        globals.use_custom_palette = true;
      }
      globals.scenes = state.scenes;
      globals.caption_list = state.caption_list;
      globals.image_list = state.image_list;
      globals.annotation_list = state.annotation_list;

      var min_story_width = instance._render_width,
        max_story_width = instance._render_width;

      globals.scenes.forEach(function (d, i) {
        if (d.s_order === undefined) {
          d.s_order = i;
        }
        if ((d.s_width + globals.margin.left + globals.margin.right + getScrollbarWidth()) < min_story_width) {
          min_story_width = (d.s_width + globals.margin.left + globals.margin.right + getScrollbarWidth());
        }
        if ((d.s_width + globals.margin.left + globals.margin.right + getScrollbarWidth()) > max_story_width) {
          max_story_width = (d.s_width + globals.margin.left + globals.margin.right + getScrollbarWidth());
        }
        if ((d.s_height + globals.margin.top + globals.margin.bottom + getScrollbarWidth()) < min_story_height) {
          min_story_height = (d.s_height + globals.margin.top + globals.margin.bottom + getScrollbarWidth());
        }
      });

      if (state.width === undefined) {
        if (max_story_width > instance._render_width) {
          state.width = max_story_width;
        } else {
          state.width = min_story_width;
        }
      }
      if (state.height === undefined) {
        state.height = min_story_height;
      }

      //log("s_width: " + state.width + "; window_width: " + instance._render_width);
      instance._render_width = state.width;
      instance._render_height = state.height;
    }

    initTimelineData(timelineData, hasScenes);

    if (hasScenes) {
      updateNavigationStepper();
    }
  };
}