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." : " .") + 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 `