github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/tool/lsm_data.go (about)

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