github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/tool/data/lsm.js (about)

     1  // TODO(peter):
     2  //
     3  // - interactions
     4  //   - mouse wheel: horizontal zoom
     5  //   - click/drag: horizontal pan
     6  
     7  "use strict";
     8  
     9  // The heights of each level. The first few levels are given smaller
    10  // heights to account for the increasing target file size.
    11  //
    12  // TODO(peter): Use the TargetFileSizes specified in the OPTIONS file.
    13  let levelHeights = [16, 16, 16, 16, 32, 64, 128];
    14  const offsetStart = 24;
    15  let levelOffsets = generateLevelOffsets();
    16  const lineStart = 105;
    17  const sublevelHeight = 16;
    18  let levelWidth = 0;
    19  
    20  {
    21      // Create the base DOM elements.
    22      let c = d3
    23          .select("body")
    24          .append("div")
    25          .attr("id", "container");
    26      let h = c.append("div").attr("id", "header");
    27      h
    28          .append("div")
    29          .attr("id", "index-container")
    30          .append("input")
    31          .attr("type", "text")
    32          .attr("id", "index")
    33          .attr("autocomplete", "off");
    34      let checkboxContainer = h
    35          .append("div")
    36          .attr("id", "checkbox-container");
    37      checkboxContainer.append("input")
    38          .attr("type", "checkbox")
    39          .attr("id", "flatten-sublevels")
    40          .on("change", () => {version.onCheckboxChange(d3.event.target.checked)});
    41      checkboxContainer.append("label")
    42          .attr("for", "flatten-sublevels")
    43          .text("Show sublevels");
    44      h.append("svg").attr("id", "slider");
    45      c.append("svg").attr("id", "vis");
    46  }
    47  
    48  let vis = d3.select("#vis");
    49  
    50  function renderHelp() {
    51      vis
    52          .append("text")
    53          .attr("class", "help")
    54          .attr("x", 10)
    55          .attr("y", levelOffsets[6] + 30)
    56          .text(
    57              "(space: start/stop, left-arrow[+shift]: step-back, right-arrow[+shift]: step-forward)"
    58          );
    59  }
    60  
    61  function renderReason() {
    62      return vis
    63          .append("text")
    64          .attr("class", "reason")
    65          .attr("x", 10)
    66          .attr("y", 16);
    67  }
    68  
    69  let reason = renderReason();
    70  
    71  let index = d3.select("#index");
    72  
    73  // Pretty formatting of a number in human readable units.
    74  function humanize(s) {
    75      const iecSuffixes = [" B", " KB", " MB", " GB", " TB", " PB", " EB"];
    76      if (s < 10) {
    77          return "" + s;
    78      }
    79      let e = Math.floor(Math.log(s) / Math.log(1024));
    80      let suffix = iecSuffixes[Math.floor(e)];
    81      let val = Math.floor(s / Math.pow(1024, e) * 10 + 0.5) / 10;
    82      return val.toFixed(val < 10 ? 1 : 0) + suffix;
    83  }
    84  
    85  function generateLevelOffsets() {
    86      return levelHeights.map((v, i) =>
    87          levelHeights.slice(0, i + 1).reduce((sum, elem) => sum + elem, offsetStart)
    88      );
    89  }
    90  
    91  function styleWidth(e) {
    92      let width = +e.style("width").slice(0, -2);
    93      return Math.round(Number(width));
    94  }
    95  
    96  function styleHeight(e) {
    97      let height = +e.style("height").slice(0, -2);
    98      return Math.round(Number(height));
    99  }
   100  
   101  let sliderX, sliderHandle;
   102  let offsetSliderX;
   103  
   104  // The version object holds the current LSM state.
   105  let version = {
   106      levels: [[], [], [], [], [], [], []],
   107      sublevels: [],
   108      numSublevels: 0,
   109      showSublevels: false,
   110      // Generated after every change using setLevelsInfo().
   111      levelsInfo: [],
   112      // The version edit index.
   113      index: -1,
   114  
   115      init: function() {
   116          for (let edit of data.Edits) {
   117              if (edit.Sublevels === null || edit.Sublevels === undefined) {
   118                  continue;
   119              }
   120              for (let [file, sublevel] of Object.entries(edit.Sublevels)) {
   121                  if (sublevel >= this.numSublevels) {
   122                      this.numSublevels = sublevel + 1;
   123                  }
   124              }
   125          }
   126          for (let i = 0; i < this.numSublevels; i++) {
   127              this.sublevels.push([]);
   128          }
   129          d3.select("#checkbox-container label")
   130              .text("Show sublevels (" + this.numSublevels.toString() + ")");
   131          this.setHeights();
   132          this.setLevelsInfo();
   133          renderHelp();
   134      },
   135  
   136      setHeights: function() {
   137          // Update the height of level 0 to account for the number of sublevels,
   138          // if there are any.
   139          if (this.numSublevels > 0 && this.showSublevels === true) {
   140              levelHeights[0] = sublevelHeight * this.numSublevels;
   141          } else {
   142              levelHeights[0] = sublevelHeight;
   143          }
   144          levelOffsets = generateLevelOffsets();
   145          vis.style("height", levelOffsets[6] + 100);
   146      },
   147  
   148      onCheckboxChange: function(value) {
   149          this.showSublevels = value;
   150          vis.selectAll("*")
   151              .remove();
   152          reason = renderReason();
   153          this.setHeights();
   154          this.setLevelsInfo();
   155          renderHelp();
   156  
   157          this.render(true);
   158          this.updateSize();
   159      },
   160  
   161      // Set the version edit index. This steps either forward or
   162      // backward through the version edits, applying or unapplying each
   163      // edit.
   164      set: function(index) {
   165          let prevIndex = this.index;
   166          if (index < 0) {
   167              index = 0;
   168          } else if (index >= data.Edits.length) {
   169              index = data.Edits.length - 1;
   170          }
   171          if (index == this.index) {
   172              return;
   173          }
   174  
   175          // If the current edit index is less than the target index,
   176          // step forward applying edits.
   177          for (; this.index < index; this.index++) {
   178              let edit = data.Edits[this.index + 1];
   179              for (let level in edit.Deleted) {
   180                  this.remove(level, edit.Deleted[level]);
   181              }
   182              for (let level in edit.Added) {
   183                  this.add(level, edit.Added[level]);
   184              }
   185          }
   186  
   187          // If the current edit index is greater than the target index,
   188          // step backward unapplying edits.
   189          for (; this.index > index; this.index--) {
   190              let edit = data.Edits[this.index];
   191              for (let level in edit.Added) {
   192                  this.remove(level, edit.Added[level]);
   193              }
   194              for (let level in edit.Deleted) {
   195                  this.add(level, edit.Deleted[level]);
   196              }
   197          }
   198  
   199          // Build the sublevels from this.levels[0]. They need to be rebuilt from
   200          // scratch each time there's a change to L0.
   201          this.sublevels = [];
   202          while(this.sublevels.length < this.numSublevels) {
   203              this.sublevels.push([]);
   204          }
   205          for (let file of this.levels[0]) {
   206              let sublevel = null;
   207              for (let i = index; i >= 0 && (sublevel === null || sublevel === undefined); i--) {
   208                  if (data.Edits[i].Sublevels == null || data.Edits[i].Sublevels == undefined) {
   209                    continue;
   210                  }
   211                  sublevel = data.Edits[i].Sublevels[file];
   212              }
   213              this.sublevels[sublevel].push(file);
   214          }
   215  
   216          // Sort the levels.
   217          for (let i in this.levels) {
   218              if (i == 0) {
   219                  for (let j in this.sublevels) {
   220                      this.sublevels[j].sort(function(a, b) {
   221                          let fa = data.Files[a];
   222                          let fb = data.Files[b];
   223                          if (fa.Smallest < fb.Smallest) {
   224                              return -1;
   225                          }
   226                          if (fa.Smallest > fb.Smallest) {
   227                              return +1;
   228                          }
   229                          return 0;
   230                      });
   231                  }
   232                  this.levels[i].sort(function(a, b) {
   233                      let fa = data.Files[a];
   234                      let fb = data.Files[b];
   235                      if (fa.LargestSeqNum < fb.LargestSeqNum) {
   236                          return -1;
   237                      }
   238                      if (fa.LargestSeqNum > fb.LargestSeqNum) {
   239                          return +1;
   240                      }
   241                      if (fa.SmallestSeqNum < fb.SmallestSeqNum) {
   242                          return -1;
   243                      }
   244                      if (fa.SmallestSeqNum > fb.SmallestSeqNum) {
   245                          return +1;
   246                      }
   247                      return a < b;
   248                  });
   249              } else {
   250                  this.levels[i].sort(function(a, b) {
   251                      let fa = data.Files[a];
   252                      let fb = data.Files[b];
   253                      if (fa.Smallest < fb.Smallest) {
   254                          return -1;
   255                      }
   256                      if (fa.Smallest > fb.Smallest) {
   257                          return +1;
   258                      }
   259                      return 0;
   260                  });
   261              }
   262          }
   263  
   264          this.updateLevelsInfo();
   265          this.render(prevIndex === -1);
   266      },
   267  
   268      // Add the specified sstables to the specifed level.
   269      add: function(level, fileNums) {
   270          for (let i = 0; i < fileNums.length; i++) {
   271              this.levels[level].push(fileNums[i]);
   272          }
   273      },
   274  
   275      // Remove the specified sstables from the specifed level.
   276      remove: function(level, fileNums) {
   277          let l = this.levels[level];
   278          for (let i = 0; i < l.length; i++) {
   279              if (fileNums.indexOf(l[i]) != -1) {
   280                  l[i] = l[l.length - 1];
   281                  l.pop();
   282                  i--;
   283              }
   284          }
   285      },
   286  
   287      // Return the size of the sstables in a level.
   288      size: function(level, sublevel) {
   289          if (level == 0 && sublevel !== null && sublevel !== undefined) {
   290              return this.sublevels[sublevel].reduce(
   291                  (sum, elem) => sum + data.Files[elem].Size,
   292                  0
   293              );
   294          }
   295          return (this.levels[level] || []).reduce(
   296              (sum, elem) => sum + data.Files[elem].Size,
   297              0
   298          );
   299      },
   300  
   301      // Returns the height to use for an sstable.
   302      height: function(fileNum) {
   303          let meta = data.Files[fileNum];
   304          return Math.ceil((meta.Size + 1024.0 * 1024.0 - 1) / (1024.0 * 1024.0));
   305      },
   306  
   307      scale: function(level) {
   308          return levelWidth < this.levelsInfo[level].files.length
   309              ? levelWidth / this.levelsInfo[level].files.length
   310              : 1;
   311      },
   312  
   313      // Return a summary of the count and size of the specified sstables.
   314      summarize: function(level, fileNums) {
   315          let count = 0;
   316          let size = 0;
   317          for (let fileNum of fileNums) {
   318              count++;
   319              size += data.Files[fileNum].Size;
   320          }
   321          return count + " @ " + "L" + level + " (" + humanize(size) + ")";
   322      },
   323  
   324      // Return a textual description of a version edit.
   325      describe: function(edit) {
   326          let s = edit.Reason;
   327  
   328          if (edit.Deleted) {
   329              let sep = " ";
   330              for (let i = 0; i < 7; i++) {
   331                  if (edit.Deleted[i]) {
   332                      s += sep + this.summarize(i, edit.Deleted[i]);
   333                      sep = " + ";
   334                  }
   335              }
   336          }
   337  
   338          if (edit.Added) {
   339              let sep = " => ";
   340              for (let i = 0; i < 7; i++) {
   341                  if (edit.Added[i]) {
   342                      s += sep + this.summarize(i, edit.Added[i]);
   343                      sep = " + ";
   344                  }
   345              }
   346          }
   347  
   348          return s;
   349      },
   350  
   351      setLevelsInfo: function() {
   352          let sublevelCount = this.numSublevels;
   353          let levelsInfo = [];
   354          let levelsStart = 1;
   355          if (this.showSublevels === true) {
   356              levelsInfo = this.sublevels.map((files, sublevel) => ({
   357                  files: files,
   358                  levelString: "L0." + sublevel.toString(),
   359                  levelDisplayString: (sublevel === this.numSublevels - 1 ?
   360                      "L0." : "&nbsp;&nbsp;&nbsp;&nbsp;.") + sublevel.toString(),
   361                  levelClass: "L0-" + sublevel.toString(),
   362                  level: 0,
   363                  offset: offsetStart + (sublevelHeight * (sublevelCount - sublevel)),
   364                  height: sublevelHeight,
   365                  size: humanize(this.size(0, sublevel)),
   366              }));
   367              if (levelsInfo.length === 0) {
   368                  levelsStart = 0;
   369              }
   370              levelsInfo.reverse();
   371          } else {
   372              levelsStart = 0;
   373          }
   374  
   375          levelsInfo = levelsInfo.concat(this.levels.slice(levelsStart).map((files, level) => ({
   376              files: files,
   377              levelString: "L" + (level+levelsStart).toString(),
   378              levelDisplayString: "L" + (level+levelsStart).toString(),
   379              levelClass: "L" + (level+levelsStart).toString(),
   380              level: level,
   381              offset: levelOffsets[level+levelsStart],
   382              height: levelHeights[level+levelsStart],
   383              size: humanize(this.size(level+levelsStart)),
   384          })));
   385          this.levelsInfo = levelsInfo;
   386      },
   387  
   388      updateLevelsInfo: function() {
   389          let levelsStart = 1;
   390          if (this.showSublevels === true) {
   391              this.sublevels.forEach((files, sublevel) => {
   392                  this.levelsInfo[this.numSublevels - (sublevel + 1)].files = files;
   393                  this.levelsInfo[this.numSublevels - (sublevel + 1)].size = humanize(this.size(0, sublevel));
   394              });
   395              if (this.numSublevels === 0) {
   396                  levelsStart = 0;
   397              }
   398          } else {
   399              levelsStart = 0;
   400          }
   401  
   402          this.levels.slice(levelsStart).forEach((files, level) => {
   403              let sublevelOffset = this.showSublevels === true ? this.numSublevels : 0;
   404              this.levelsInfo[sublevelOffset + level].files = files;
   405              this.levelsInfo[sublevelOffset + level].size = humanize(this.size(levelsStart + level));
   406          });
   407      },
   408  
   409      render: function(redraw) {
   410          let version = this;
   411  
   412          vis.interrupt();
   413  
   414          // Render the edit info.
   415          let info = "[" + this.describe(data.Edits[this.index]) + "]";
   416          reason.text(info);
   417  
   418          // Render the text for each level: sstable count and size.
   419          vis
   420              .selectAll("text.levels")
   421              .data(this.levelsInfo)
   422              .enter()
   423              .append("text")
   424              .attr("class", "levels")
   425              .attr("x", 10)
   426              .attr("y", d => d.offset)
   427              .html(d => d.levelDisplayString);
   428          vis
   429              .selectAll("text.counts")
   430              .data(this.levelsInfo)
   431              .text((d, i) => d.files.length)
   432              .enter()
   433              .append("text")
   434              .attr("class", "counts")
   435              .attr("text-anchor", "end")
   436              .attr("x", 55)
   437              .attr("y", d => d.offset)
   438              .text(d => d.files.length);
   439          vis
   440              .selectAll("text.sizes")
   441              .data(this.levelsInfo)
   442              .text((d, i) => d.size)
   443              .enter()
   444              .append("text")
   445              .attr("class", "sizes")
   446              .attr("text-anchor", "end")
   447              .attr("x", 100)
   448              .attr("y", (d, i) => d.offset)
   449              .text(d => d.size);
   450  
   451          // Render each of the levels. Each level is composed of an
   452          // outer group which provides a clipping recentangle, an inner
   453          // group defining the coordinate system, an overlap rectangle
   454          // to capture mouse events, an indicator rectangle used to
   455          // display sstable overlaps, and the per-sstable rectangles.
   456          for (let i in this.levelsInfo) {
   457              let g, clipG;
   458              if (redraw === false) {
   459                  g = vis
   460                      .selectAll("g.clip" + this.levelsInfo[i].levelClass)
   461                      .select("g")
   462                      .data([i]);
   463                  clipG = g
   464                      .enter()
   465                      .append("g")
   466                      .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass)
   467                      .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")");
   468              } else {
   469                  clipG = vis
   470                      .append("g")
   471                      .attr("class", "clipRect clip" + this.levelsInfo[i].levelClass)
   472                      .attr("clip-path", "url(#" + this.levelsInfo[i].levelClass + ")")
   473                      .data([i]);
   474                  g = clipG
   475                      .append("g");
   476              }
   477              clipG
   478                  .append("g")
   479                  .attr(
   480                      "transform",
   481                      "translate(" +
   482                          lineStart +
   483                          "," +
   484                          this.levelsInfo[i].offset +
   485                          ") scale(1,-1)"
   486                  );
   487              clipG.append("rect").attr("class", "indicator");
   488  
   489              // Define the overlap rectangle for capturing mouse events.
   490              clipG
   491                  .append("rect")
   492                  .attr("x", lineStart)
   493                  .attr("y", this.levelsInfo[i].offset - this.levelsInfo[i].height)
   494                  .attr("width", levelWidth)
   495                  .attr("height", this.levelsInfo[i].height)
   496                  .attr("opacity", 0)
   497                  .attr("pointer-events", "all")
   498                  .on("mousemove", i => version.onMouseMove(i))
   499                  .on("mouseout", function() {
   500                      reason.text(info);
   501                      vis.selectAll("rect.indicator").attr("fill", "none");
   502                  });
   503  
   504              // Scale each level to fit within the display.
   505              let s = this.scale(i);
   506              g.attr(
   507                  "transform",
   508                  "translate(" +
   509                      lineStart +
   510                      "," +
   511                      this.levelsInfo[i].offset +
   512                      ") scale(" +
   513                      s +
   514                      "," +
   515                      -(1 / s) +
   516                      ")"
   517              );
   518  
   519              // Render the sstables for the level.
   520              let level = g.selectAll("rect." + this.levelsInfo[i].levelClass).data(this.levelsInfo[i].files);
   521              level.attr("fill", "#555").attr("x", (fileNum, i) => i);
   522              level
   523                  .enter()
   524                  .append("rect")
   525                  .attr("class", this.levelsInfo[i].levelClass + " sstable")
   526                  .attr("id", fileNum => fileNum)
   527                  .attr("fill", "red")
   528                  .attr("x", (fileNum, i) => i)
   529                  .attr("y", 0)
   530                  .attr("width", 1)
   531                  .attr("height", fileNum => version.height(fileNum));
   532              level.exit().remove();
   533          }
   534  
   535          sliderHandle.attr("cx", sliderX(version.index));
   536          index.node().value = version.index + data.StartEdit;
   537      },
   538  
   539      onMouseMove: function(i) {
   540          i = Number(i);
   541          if (Number.isNaN(i) || i >= this.levelsInfo.length || this.levelsInfo[i].files.length === 0) {
   542              return;
   543          }
   544  
   545          // The mouse coordinates are relative to the
   546          // SVG element. Adjust to be relative to the
   547          // level position.
   548          let mousex = d3.mouse(vis.node())[0] - lineStart;
   549          let index = Math.round(mousex / this.scale(i));
   550          if (index < 0) {
   551              index = 0;
   552          } else if (index >= this.levelsInfo[i].files.length) {
   553              index = this.levelsInfo[i].files.length - 1;
   554          }
   555          let fileNum = this.levelsInfo[i].files[index];
   556          let meta = data.Files[fileNum];
   557  
   558          // Find the start and end index of the tables
   559          // that overlap with filenum.
   560          let overlapInfo = "";
   561          for (let j = 1; j < this.levelsInfo.length; j++) {
   562              if (this.levelsInfo[i].files.length === 0) {
   563                  continue;
   564              }
   565              let indicator = vis.select("g.clip" + this.levelsInfo[j].levelClass + " rect.indicator");
   566              indicator
   567                  .attr("fill", "black")
   568                  .attr("opacity", 0.3)
   569                  .attr("y", this.levelsInfo[j].offset - this.levelsInfo[j].height)
   570                  .attr("height", this.levelsInfo[j].height);
   571              if (j === i) {
   572                  continue;
   573              }
   574              let fileNums = this.levelsInfo[j].files;
   575              for (let k in fileNums) {
   576                  let other = data.Files[fileNums[k]];
   577                  if (other.Largest < meta.Smallest) {
   578                      continue;
   579                  }
   580                  let s = this.scale(j);
   581                  let t = k;
   582                  for (; k < fileNums.length; k++) {
   583                      let other = data.Files[fileNums[k]];
   584                      if (other.Smallest >= meta.Largest) {
   585                          break;
   586                      }
   587                  }
   588                  if (k === t) {
   589                      indicator.attr("x", lineStart + s * t).attr("width", s);
   590                  } else {
   591                      indicator
   592                          .attr("x", lineStart + s * t)
   593                          .attr("width", Math.max(0.5, s * (k - t)));
   594                  }
   595                  if (i + 1 === j && k > t) {
   596                      let overlapSize = this.levelsInfo[j].files
   597                          .slice(t, k)
   598                          .reduce((sum, elem) => sum + data.Files[elem].Size, 0);
   599  
   600                      overlapInfo =
   601                          " overlaps " +
   602                          (k - t) +
   603                          " @ " +
   604                          this.levelsInfo[j].levelString +
   605                          " (" +
   606                          humanize(overlapSize) +
   607                          ")";
   608                  }
   609                  break;
   610              }
   611          }
   612  
   613          reason.text(
   614              "[" +
   615                  this.levelsInfo[i].levelString +
   616                  " " +
   617                  fileNum +
   618                  " (" +
   619                  humanize(data.Files[fileNum].Size) +
   620                  ")" +
   621                  overlapInfo +
   622                  " <" +
   623                  data.Keys[data.Files[fileNum].Smallest].Pretty +
   624                  ", " +
   625                  data.Keys[data.Files[fileNum].Largest].Pretty +
   626                  ">" +
   627                  "]"
   628          );
   629  
   630          vis
   631              .select("g.clip" + this.levelsInfo[i].levelClass + " rect.indicator")
   632              .attr("x", lineStart + this.scale(i) * index)
   633              .attr("width", 1);
   634      },
   635  
   636      // Recalculate structures related to the page width.
   637      updateSize: function() {
   638          let svg = d3.select("#slider").html("");
   639  
   640          let margin = { right: 10, left: 10 };
   641  
   642          let width = styleWidth(d3.select("#slider")) - margin.left - margin.right,
   643              height = styleHeight(svg);
   644  
   645          sliderX = d3
   646              .scaleLinear()
   647              .domain([0, data.Edits.length - 1])
   648              .range([0, width])
   649              .clamp(true);
   650  
   651          // Used only to generate offset ticks for slider.
   652          // sliderX is used to index into the data.Edits array (0-indexed).
   653          offsetSliderX = d3
   654            .scaleLinear()
   655            .domain([data.StartEdit, data.StartEdit + data.Edits.length - 1])
   656            .range([0, width]);
   657  
   658          let slider = svg
   659              .append("g")
   660              .attr("class", "slider")
   661              .attr("transform", "translate(" + margin.left + "," + height / 2 + ")");
   662  
   663          slider
   664              .append("line")
   665              .attr("class", "track")
   666              .attr("x1", sliderX.range()[0])
   667              .attr("x2", sliderX.range()[1])
   668              .select(function() {
   669                  return this.parentNode.appendChild(this.cloneNode(true));
   670              })
   671              .attr("class", "track-inset")
   672              .select(function() {
   673                  return this.parentNode.appendChild(this.cloneNode(true));
   674              })
   675              .attr("class", "track-overlay")
   676              .call(
   677                  d3
   678                      .drag()
   679                      .on("start.interrupt", function() {
   680                          slider.interrupt();
   681                      })
   682                      .on("start drag", function() {
   683                          version.set(Math.round(sliderX.invert(d3.event.x)));
   684                      })
   685              );
   686  
   687          slider
   688              .insert("g", ".track-overlay")
   689              .attr("class", "ticks")
   690              .attr("transform", "translate(0," + 18 + ")")
   691              .selectAll("text")
   692              .data(offsetSliderX.ticks(10))
   693              .enter()
   694              .append("text")
   695              .attr("x", offsetSliderX)
   696              .attr("text-anchor", "middle")
   697              .text(function(d) {
   698                  return d;
   699              });
   700  
   701          sliderHandle = slider
   702              .insert("circle", ".track-overlay")
   703              .attr("class", "handle")
   704              .attr("r", 9)
   705              .attr("cx", sliderX(version.index));
   706  
   707          levelWidth = styleWidth(vis) - 10 - lineStart;
   708          let lineEnd = lineStart + levelWidth;
   709  
   710          vis
   711              .selectAll("line")
   712              .data(this.levelsInfo)
   713              .attr("x2", lineEnd)
   714              .enter()
   715              .append("line")
   716              .attr("x1", lineStart)
   717              .attr("x2", lineEnd)
   718              .attr("y1", d => d.offset)
   719              .attr("y2", d => d.offset)
   720              .attr("stroke", "#ddd");
   721  
   722          vis
   723              .selectAll("defs clipPath rect")
   724              .data(this.levelsInfo)
   725              .attr("width", lineEnd - lineStart)
   726              .enter()
   727              .append("defs")
   728              .append("clipPath")
   729              .attr("id", d => d.levelClass)
   730              .append("rect")
   731              .attr("x", lineStart)
   732              .attr("y", d => d.offset - d.height)
   733              .attr("width", lineEnd - lineStart)
   734              .attr("height", d => d.height);
   735      },
   736  };
   737  
   738  window.onload = function() {
   739      version.init();
   740      version.updateSize();
   741      version.set(0);
   742  };
   743  
   744  window.addEventListener("resize", function() {
   745      version.updateSize();
   746      version.render();
   747  });
   748  
   749  let timer;
   750  
   751  function startPlayback(increment) {
   752      timer = d3.timer(function() {
   753          let lastIndex = version.index;
   754          version.set(version.index + increment);
   755          if (lastIndex == version.index) {
   756              timer.stop();
   757              timer = null;
   758          }
   759      });
   760  }
   761  
   762  function stopPlayback() {
   763      if (timer == null) {
   764          return false;
   765      }
   766      timer.stop();
   767      timer = null;
   768      return true;
   769  }
   770  
   771  document.addEventListener("keydown", function(e) {
   772      switch (e.keyCode) {
   773          case 37: // left arrow
   774              stopPlayback();
   775              version.set(version.index - (e.shiftKey ? 10 : 1));
   776              return;
   777          case 39: // right arrow
   778              stopPlayback();
   779              version.set(version.index + (e.shiftKey ? 10 : 1));
   780              return;
   781          case 32: // space
   782              if (stopPlayback()) {
   783                  return;
   784              }
   785              startPlayback(1);
   786              return;
   787      }
   788  });
   789  
   790  index.on("input", function() {
   791      if (!isNaN(+this.value)) {
   792          const val = Number(this.value) - data.StartEdit;
   793          if (val >= 0) {
   794              version.set(val);
   795          }
   796      }
   797  });