1// Copyright 2015 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import * as d3 from "d3" 6import {layoutNodeGraph} from "./graph-layout.js" 7import {MAX_RANK_SENTINEL} from "./constants.js" 8import {GNode, nodeToStr, isNodeInitiallyVisible} from "./node.js" 9import {NODE_INPUT_WIDTH, MINIMUM_NODE_OUTPUT_APPROACH} from "./node.js" 10import {DEFAULT_NODE_BUBBLE_RADIUS} from "./node.js" 11import {Edge, edgeToStr} from "./edge.js" 12import {View, PhaseView} from "./view.js" 13import {MySelection} from "./selection.js" 14import {partial, alignUp} from "./util.js" 15 16function nodeToStringKey(n) { 17 return "" + n.id; 18} 19 20interface GraphState { 21 showTypes: boolean; 22 selection: MySelection; 23 mouseDownNode: any; 24 justDragged: boolean, 25 justScaleTransGraph: boolean, 26 lastKeyDown: number, 27 hideDead: boolean 28} 29 30export class GraphView extends View implements PhaseView { 31 divElement: d3.Selection<any, any, any, any>; 32 svg: d3.Selection<any, any, any, any>; 33 showPhaseByName: (string) => void; 34 state: GraphState; 35 nodes: Array<GNode>; 36 edges: Array<any>; 37 selectionHandler: NodeSelectionHandler; 38 graphElement: d3.Selection<any, any, any, any>; 39 visibleNodes: d3.Selection<any, GNode, any, any>; 40 visibleEdges: d3.Selection<any, Edge, any, any>; 41 minGraphX: number; 42 maxGraphX: number; 43 minGraphY: number; 44 maxGraphY: number; 45 width: number; 46 height: number; 47 maxGraphNodeX: number; 48 drag: d3.DragBehavior<any, GNode, GNode>; 49 panZoom: d3.ZoomBehavior<SVGElement, any>; 50 nodeMap: Array<any>; 51 visibleBubbles: d3.Selection<any, any, any, any>; 52 transitionTimout: number; 53 54 createViewElement() { 55 const pane = document.createElement('div'); 56 pane.setAttribute('id', "graph"); 57 return pane; 58 } 59 60 constructor(id, broker, showPhaseByName: (string) => void) { 61 super(id); 62 var graph = this; 63 this.showPhaseByName = showPhaseByName; 64 this.divElement = d3.select(this.divNode); 65 const svg = this.divElement.append("svg").attr('version', '1.1') 66 .attr("width", "100%") 67 .attr("height", "100%"); 68 svg.on("click", function (d) { 69 graph.selectionHandler.clear(); 70 }); 71 graph.svg = svg; 72 73 graph.nodes = []; 74 graph.edges = []; 75 76 graph.minGraphX = 0; 77 graph.maxGraphX = 1; 78 graph.minGraphY = 0; 79 graph.maxGraphY = 1; 80 81 graph.state = { 82 selection: null, 83 mouseDownNode: null, 84 justDragged: false, 85 justScaleTransGraph: false, 86 lastKeyDown: -1, 87 showTypes: false, 88 hideDead: false 89 }; 90 91 this.selectionHandler = { 92 clear: function () { 93 graph.state.selection.clear(); 94 broker.broadcastClear(this); 95 graph.updateGraphVisibility(); 96 }, 97 select: function (nodes, selected) { 98 let locations = []; 99 for (const node of nodes) { 100 if (node.sourcePosition) { 101 locations.push(node.sourcePosition); 102 } 103 if (node.origin && node.origin.bytecodePosition) { 104 locations.push({ bytecodePosition: node.origin.bytecodePosition }); 105 } 106 } 107 graph.state.selection.select(nodes, selected); 108 broker.broadcastSourcePositionSelect(this, locations, selected); 109 graph.updateGraphVisibility(); 110 }, 111 brokeredNodeSelect: function (locations, selected) { 112 let selection = graph.nodes 113 .filter(function (n) { 114 return locations.has(nodeToStringKey(n)) 115 && (!graph.state.hideDead || n.isLive()); 116 }); 117 graph.state.selection.select(selection, selected); 118 // Update edge visibility based on selection. 119 graph.nodes.forEach((n) => { 120 if (graph.state.selection.isSelected(n)) n.visible = true; 121 }); 122 graph.edges.forEach(function (e) { 123 e.visible = e.visible || 124 (graph.state.selection.isSelected(e.source) && graph.state.selection.isSelected(e.target)); 125 }); 126 graph.updateGraphVisibility(); 127 }, 128 brokeredClear: function () { 129 graph.state.selection.clear(); 130 graph.updateGraphVisibility(); 131 } 132 }; 133 broker.addNodeHandler(this.selectionHandler); 134 135 graph.state.selection = new MySelection(nodeToStringKey); 136 137 const defs = svg.append('svg:defs'); 138 defs.append('svg:marker') 139 .attr('id', 'end-arrow') 140 .attr('viewBox', '0 -4 8 8') 141 .attr('refX', 2) 142 .attr('markerWidth', 2.5) 143 .attr('markerHeight', 2.5) 144 .attr('orient', 'auto') 145 .append('svg:path') 146 .attr('d', 'M0,-4L8,0L0,4'); 147 148 this.graphElement = svg.append("g"); 149 graph.visibleEdges = this.graphElement.append("g"); 150 graph.visibleNodes = this.graphElement.append("g"); 151 152 graph.drag = d3.drag<any, GNode, GNode>() 153 .on("drag", function (d) { 154 d.x += d3.event.dx; 155 d.y += d3.event.dy; 156 graph.updateGraphVisibility(); 157 }); 158 159 160 d3.select("#layout").on("click", partial(this.layoutAction, graph)); 161 d3.select("#show-all").on("click", partial(this.showAllAction, graph)); 162 d3.select("#toggle-hide-dead").on("click", partial(this.toggleHideDead, graph)); 163 d3.select("#hide-unselected").on("click", partial(this.hideUnselectedAction, graph)); 164 d3.select("#hide-selected").on("click", partial(this.hideSelectedAction, graph)); 165 d3.select("#zoom-selection").on("click", partial(this.zoomSelectionAction, graph)); 166 d3.select("#toggle-types").on("click", partial(this.toggleTypesAction, graph)); 167 168 // listen for key events 169 d3.select(window).on("keydown", function (e) { 170 graph.svgKeyDown.call(graph); 171 }).on("keyup", function () { 172 graph.svgKeyUp.call(graph); 173 }); 174 175 function zoomed() { 176 if (d3.event.shiftKey) return false; 177 graph.graphElement.attr("transform", d3.event.transform); 178 } 179 180 const zoomSvg = d3.zoom<SVGElement, any>() 181 .scaleExtent([0.2, 40]) 182 .on("zoom", zoomed) 183 .on("start", function () { 184 if (d3.event.shiftKey) return; 185 d3.select('body').style("cursor", "move"); 186 }) 187 .on("end", function () { 188 d3.select('body').style("cursor", "auto"); 189 }); 190 191 svg.call(zoomSvg).on("dblclick.zoom", null); 192 193 graph.panZoom = zoomSvg; 194 195 } 196 197 198 static get selectedClass() { 199 return "selected"; 200 } 201 static get rectClass() { 202 return "nodeStyle"; 203 } 204 static get activeEditId() { 205 return "active-editing"; 206 } 207 static get nodeRadius() { 208 return 50; 209 } 210 211 getNodeHeight(d): number { 212 if (this.state.showTypes) { 213 return d.normalheight + d.labelbbox.height; 214 } else { 215 return d.normalheight; 216 } 217 } 218 219 getEdgeFrontier(nodes, inEdges, edgeFilter) { 220 let frontier = new Set(); 221 for (const n of nodes) { 222 var edges = inEdges ? n.inputs : n.outputs; 223 var edgeNumber = 0; 224 edges.forEach(function (edge) { 225 if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { 226 frontier.add(edge); 227 } 228 ++edgeNumber; 229 }); 230 } 231 return frontier; 232 } 233 234 getNodeFrontier(nodes, inEdges, edgeFilter) { 235 let graph = this; 236 var frontier = new Set(); 237 var newState = true; 238 var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); 239 // Control key toggles edges rather than just turning them on 240 if (d3.event.ctrlKey) { 241 edgeFrontier.forEach(function (edge) { 242 if (edge.visible) { 243 newState = false; 244 } 245 }); 246 } 247 edgeFrontier.forEach(function (edge) { 248 edge.visible = newState; 249 if (newState) { 250 var node = inEdges ? edge.source : edge.target; 251 node.visible = true; 252 frontier.add(node); 253 } 254 }); 255 graph.updateGraphVisibility(); 256 if (newState) { 257 return frontier; 258 } else { 259 return undefined; 260 } 261 } 262 263 initializeContent(data, rememberedSelection) { 264 this.createGraph(data, rememberedSelection); 265 if (rememberedSelection != null) { 266 this.attachSelection(rememberedSelection); 267 this.connectVisibleSelectedNodes(); 268 this.viewSelection(); 269 } else { 270 this.viewWholeGraph(); 271 } 272 } 273 274 deleteContent() { 275 if (this.visibleNodes) { 276 this.nodes = []; 277 this.edges = []; 278 this.nodeMap = []; 279 this.updateGraphVisibility(); 280 } 281 }; 282 283 measureText(text) { 284 const textMeasure = document.getElementById('text-measure') as SVGTSpanElement; 285 textMeasure.textContent = text; 286 return { 287 width: textMeasure.getBBox().width, 288 height: textMeasure.getBBox().height, 289 }; 290 } 291 292 createGraph(data, rememberedSelection) { 293 var g = this; 294 g.nodes = []; 295 g.nodeMap = []; 296 data.nodes.forEach(function (n, i) { 297 n.__proto__ = GNode.prototype; 298 n.visible = false; 299 n.x = 0; 300 n.y = 0; 301 if (typeof n.pos === "number") { 302 // Backwards compatibility. 303 n.sourcePosition = { scriptOffset: n.pos, inliningId: -1 }; 304 } 305 n.rank = MAX_RANK_SENTINEL; 306 n.inputs = []; 307 n.outputs = []; 308 n.rpo = -1; 309 n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; 310 n.cfg = n.control; 311 g.nodeMap[n.id] = n; 312 n.displayLabel = n.getDisplayLabel(); 313 n.labelbbox = g.measureText(n.displayLabel); 314 n.typebbox = g.measureText(n.getDisplayType()); 315 var innerwidth = Math.max(n.labelbbox.width, n.typebbox.width); 316 n.width = alignUp(innerwidth + NODE_INPUT_WIDTH * 2, 317 NODE_INPUT_WIDTH); 318 var innerheight = Math.max(n.labelbbox.height, n.typebbox.height); 319 n.normalheight = innerheight + 20; 320 g.nodes.push(n); 321 }); 322 g.edges = []; 323 data.edges.forEach(function (e, i) { 324 var t = g.nodeMap[e.target]; 325 var s = g.nodeMap[e.source]; 326 var newEdge = new Edge(t, e.index, s, e.type); 327 t.inputs.push(newEdge); 328 s.outputs.push(newEdge); 329 g.edges.push(newEdge); 330 if (e.type == 'control') { 331 s.cfg = true; 332 } 333 }); 334 g.nodes.forEach(function (n, i) { 335 n.visible = isNodeInitiallyVisible(n) && (!g.state.hideDead || n.isLive()); 336 if (rememberedSelection != undefined) { 337 if (rememberedSelection.has(nodeToStringKey(n))) { 338 n.visible = true; 339 } 340 } 341 }); 342 g.updateGraphVisibility(); 343 g.layoutGraph(); 344 g.updateGraphVisibility(); 345 g.viewWholeGraph(); 346 } 347 348 connectVisibleSelectedNodes() { 349 var graph = this; 350 for (const n of graph.state.selection) { 351 n.inputs.forEach(function (edge) { 352 if (edge.source.visible && edge.target.visible) { 353 edge.visible = true; 354 } 355 }); 356 n.outputs.forEach(function (edge) { 357 if (edge.source.visible && edge.target.visible) { 358 edge.visible = true; 359 } 360 }); 361 } 362 } 363 364 updateInputAndOutputBubbles() { 365 var g = this; 366 var s = g.visibleBubbles; 367 s.classed("filledBubbleStyle", function (c) { 368 var components = this.id.split(','); 369 if (components[0] == "ib") { 370 var edge = g.nodeMap[components[3]].inputs[components[2]]; 371 return edge.isVisible(); 372 } else { 373 return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; 374 } 375 }).classed("halfFilledBubbleStyle", function (c) { 376 var components = this.id.split(','); 377 if (components[0] == "ib") { 378 var edge = g.nodeMap[components[3]].inputs[components[2]]; 379 return false; 380 } else { 381 return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; 382 } 383 }).classed("bubbleStyle", function (c) { 384 var components = this.id.split(','); 385 if (components[0] == "ib") { 386 var edge = g.nodeMap[components[3]].inputs[components[2]]; 387 return !edge.isVisible(); 388 } else { 389 return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; 390 } 391 }); 392 s.each(function (c) { 393 var components = this.id.split(','); 394 if (components[0] == "ob") { 395 var from = g.nodeMap[components[1]]; 396 var x = from.getOutputX(); 397 var y = g.getNodeHeight(from) + DEFAULT_NODE_BUBBLE_RADIUS; 398 var transform = "translate(" + x + "," + y + ")"; 399 this.setAttribute('transform', transform); 400 } 401 }); 402 } 403 404 attachSelection(s) { 405 const graph = this; 406 if (!(s instanceof Set)) return; 407 graph.selectionHandler.clear(); 408 const selected = graph.nodes.filter((n) => 409 s.has(graph.state.selection.stringKey(n)) && (!graph.state.hideDead || n.isLive())); 410 graph.selectionHandler.select(selected, true); 411 } 412 413 detachSelection() { 414 return this.state.selection.detachSelection(); 415 } 416 417 selectAllNodes() { 418 var graph = this; 419 if (!d3.event.shiftKey) { 420 graph.state.selection.clear(); 421 } 422 const allVisibleNodes = graph.nodes.filter((n) => n.visible); 423 graph.state.selection.select(allVisibleNodes, true); 424 graph.updateGraphVisibility(); 425 } 426 427 layoutAction(graph) { 428 graph.updateGraphVisibility(); 429 graph.layoutGraph(); 430 graph.updateGraphVisibility(); 431 graph.viewWholeGraph(); 432 } 433 434 showAllAction(graph) { 435 graph.nodes.forEach(function (n) { 436 n.visible = !graph.state.hideDead || n.isLive(); 437 }); 438 graph.edges.forEach(function (e) { 439 e.visible = !graph.state.hideDead || (e.source.isLive() && e.target.isLive()); 440 }); 441 graph.updateGraphVisibility(); 442 graph.viewWholeGraph(); 443 } 444 445 toggleHideDead(graph) { 446 graph.state.hideDead = !graph.state.hideDead; 447 if (graph.state.hideDead) graph.hideDead(); 448 var element = document.getElementById('toggle-hide-dead'); 449 element.classList.toggle('button-input-toggled', graph.state.hideDead); 450 } 451 452 hideDead() { 453 const graph = this; 454 graph.nodes.filter(function (n) { 455 if (!n.isLive()) { 456 n.visible = false; 457 graph.state.selection.select([n], false); 458 } 459 }) 460 graph.updateGraphVisibility(); 461 } 462 463 hideUnselectedAction(graph) { 464 graph.nodes.forEach(function (n) { 465 if (!graph.state.selection.isSelected(n)) { 466 n.visible = false; 467 } 468 }); 469 graph.updateGraphVisibility(); 470 } 471 472 hideSelectedAction(graph) { 473 graph.nodes.forEach(function (n) { 474 if (graph.state.selection.isSelected(n)) { 475 n.visible = false; 476 } 477 }); 478 graph.selectionHandler.clear(); 479 } 480 481 zoomSelectionAction(graph) { 482 graph.viewSelection(); 483 } 484 485 toggleTypesAction(graph) { 486 graph.toggleTypes(); 487 } 488 489 searchInputAction(searchBar, e: KeyboardEvent) { 490 const graph = this; 491 if (e.keyCode == 13) { 492 graph.selectionHandler.clear(); 493 var query = searchBar.value; 494 window.sessionStorage.setItem("lastSearch", query); 495 if (query.length == 0) return; 496 497 var reg = new RegExp(query); 498 var filterFunction = function (n) { 499 return (reg.exec(n.getDisplayLabel()) != null || 500 (graph.state.showTypes && reg.exec(n.getDisplayType())) || 501 (reg.exec(n.getTitle())) || 502 reg.exec(n.opcode) != null); 503 }; 504 505 const selection = graph.nodes.filter( 506 function (n, i) { 507 if ((e.ctrlKey || n.visible) && filterFunction(n)) { 508 if (e.ctrlKey) n.visible = true; 509 return true; 510 } 511 return false; 512 }); 513 514 graph.selectionHandler.select(selection, true); 515 graph.connectVisibleSelectedNodes(); 516 graph.updateGraphVisibility(); 517 searchBar.blur(); 518 graph.viewSelection(); 519 } 520 e.stopPropagation(); 521 } 522 523 svgKeyDown() { 524 var state = this.state; 525 var graph = this; 526 527 // Don't handle key press repetition 528 if (state.lastKeyDown !== -1) return; 529 530 var showSelectionFrontierNodes = function (inEdges, filter, select) { 531 var frontier = graph.getNodeFrontier(state.selection, inEdges, filter); 532 if (frontier != undefined && frontier.size) { 533 if (select) { 534 if (!d3.event.shiftKey) { 535 state.selection.clear(); 536 } 537 state.selection.select(frontier, true); 538 } 539 graph.updateGraphVisibility(); 540 } 541 allowRepetition = false; 542 } 543 544 var allowRepetition = true; 545 var eventHandled = true; // unless the below switch defaults 546 switch (d3.event.keyCode) { 547 case 49: 548 case 50: 549 case 51: 550 case 52: 551 case 53: 552 case 54: 553 case 55: 554 case 56: 555 case 57: 556 // '1'-'9' 557 showSelectionFrontierNodes(true, 558 (edge, index) => { return index == (d3.event.keyCode - 49); }, 559 false); 560 break; 561 case 97: 562 case 98: 563 case 99: 564 case 100: 565 case 101: 566 case 102: 567 case 103: 568 case 104: 569 case 105: 570 // 'numpad 1'-'numpad 9' 571 showSelectionFrontierNodes(true, 572 (edge, index) => { return index == (d3.event.keyCode - 97); }, 573 false); 574 break; 575 case 67: 576 // 'c' 577 showSelectionFrontierNodes(d3.event.altKey, 578 (edge, index) => { return edge.type == 'control'; }, 579 true); 580 break; 581 case 69: 582 // 'e' 583 showSelectionFrontierNodes(d3.event.altKey, 584 (edge, index) => { return edge.type == 'effect'; }, 585 true); 586 break; 587 case 79: 588 // 'o' 589 showSelectionFrontierNodes(false, undefined, false); 590 break; 591 case 73: 592 // 'i' 593 showSelectionFrontierNodes(true, undefined, false); 594 break; 595 case 65: 596 // 'a' 597 graph.selectAllNodes(); 598 allowRepetition = false; 599 break; 600 case 38: 601 case 40: { 602 showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); 603 break; 604 } 605 case 82: 606 // 'r' 607 if (!d3.event.ctrlKey) { 608 this.layoutAction(this); 609 } else { 610 eventHandled = false; 611 } 612 break; 613 case 83: 614 // 's' 615 graph.selectOrigins(); 616 break; 617 case 191: 618 // '/' 619 document.getElementById("search-input").focus(); 620 break; 621 default: 622 eventHandled = false; 623 break; 624 } 625 if (eventHandled) { 626 d3.event.preventDefault(); 627 } 628 if (!allowRepetition) { 629 state.lastKeyDown = d3.event.keyCode; 630 } 631 } 632 633 svgKeyUp() { 634 this.state.lastKeyDown = -1 635 }; 636 637 layoutGraph() { 638 layoutNodeGraph(this); 639 } 640 641 selectOrigins() { 642 const state = this.state; 643 const origins = []; 644 let phase = null; 645 for (const n of state.selection) { 646 if (n.origin) { 647 const node = this.nodeMap[n.origin.nodeId]; 648 origins.push(node); 649 phase = n.origin.phase; 650 } 651 } 652 if (origins.length) { 653 state.selection.clear(); 654 state.selection.select(origins, true); 655 if (phase) { 656 this.showPhaseByName(phase); 657 } 658 } 659 } 660 661 // call to propagate changes to graph 662 updateGraphVisibility() { 663 let graph = this; 664 let state = graph.state; 665 666 var filteredEdges = graph.edges.filter(function (e) { 667 return e.isVisible(); 668 }); 669 const selEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>("path").data(filteredEdges, edgeToStr); 670 671 // remove old links 672 selEdges.exit().remove(); 673 674 // add new paths 675 selEdges.enter() 676 .append('path') 677 .style('marker-end', 'url(#end-arrow)') 678 .classed('hidden', function (e) { 679 return !e.isVisible(); 680 }) 681 .attr("id", function (edge) { return "e," + edge.stringID(); }) 682 .on("click", function (edge) { 683 d3.event.stopPropagation(); 684 if (!d3.event.shiftKey) { 685 graph.selectionHandler.clear(); 686 } 687 graph.selectionHandler.select([edge.source, edge.target], true); 688 }) 689 .attr("adjacentToHover", "false"); 690 691 // Set the correct styles on all of the paths 692 selEdges.classed('value', function (e) { 693 return e.type == 'value' || e.type == 'context'; 694 }).classed('control', function (e) { 695 return e.type == 'control'; 696 }).classed('effect', function (e) { 697 return e.type == 'effect'; 698 }).classed('frame-state', function (e) { 699 return e.type == 'frame-state'; 700 }).attr('stroke-dasharray', function (e) { 701 if (e.type == 'frame-state') return "10,10"; 702 return (e.type == 'effect') ? "5,5" : ""; 703 }); 704 705 // select existing nodes 706 const filteredNodes = graph.nodes.filter(n => n.visible); 707 const allNodes = graph.visibleNodes.selectAll<SVGGElement, GNode>("g"); 708 const selNodes = allNodes.data(filteredNodes, nodeToStr); 709 710 // remove old nodes 711 selNodes.exit().remove(); 712 713 // add new nodes 714 var newGs = selNodes.enter() 715 .append("g"); 716 717 newGs.classed("turbonode", function (n) { return true; }) 718 .classed("control", function (n) { return n.isControl(); }) 719 .classed("live", function (n) { return n.isLive(); }) 720 .classed("dead", function (n) { return !n.isLive(); }) 721 .classed("javascript", function (n) { return n.isJavaScript(); }) 722 .classed("input", function (n) { return n.isInput(); }) 723 .classed("simplified", function (n) { return n.isSimplified(); }) 724 .classed("machine", function (n) { return n.isMachine(); }) 725 .on('mouseenter', function (node) { 726 const visibleEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>('path'); 727 const adjInputEdges = visibleEdges.filter(e => { return e.target === node; }); 728 const adjOutputEdges = visibleEdges.filter(e => { return e.source === node; }); 729 adjInputEdges.attr('relToHover', "input"); 730 adjOutputEdges.attr('relToHover', "output"); 731 const adjInputNodes = adjInputEdges.data().map(e => e.source); 732 const visibleNodes = graph.visibleNodes.selectAll<SVGGElement, GNode>("g"); 733 const input = visibleNodes.data<GNode>(adjInputNodes, nodeToStr) 734 .attr('relToHover', "input"); 735 const adjOutputNodes = adjOutputEdges.data().map(e => e.target); 736 const output = visibleNodes.data<GNode>(adjOutputNodes, nodeToStr) 737 .attr('relToHover', "output"); 738 graph.updateGraphVisibility(); 739 }) 740 .on('mouseleave', function (node) { 741 const visibleEdges = graph.visibleEdges.selectAll<SVGPathElement, Edge>('path'); 742 const adjEdges = visibleEdges.filter(e => { return e.target === node || e.source === node; }); 743 adjEdges.attr('relToHover', "none"); 744 const adjNodes = adjEdges.data().map(e => e.target).concat(adjEdges.data().map(e => e.source)); 745 const visibleNodes = graph.visibleNodes.selectAll<SVGPathElement, GNode>("g"); 746 const nodes = visibleNodes.data(adjNodes, nodeToStr) 747 .attr('relToHover', "none"); 748 graph.updateGraphVisibility(); 749 }) 750 .on("click", (d) => { 751 if (!d3.event.shiftKey) graph.selectionHandler.clear(); 752 graph.selectionHandler.select([d], undefined); 753 d3.event.stopPropagation(); 754 }) 755 .call(graph.drag) 756 757 newGs.append("rect") 758 .attr("rx", 10) 759 .attr("ry", 10) 760 .attr('width', function (d) { 761 return d.getTotalNodeWidth(); 762 }) 763 .attr('height', function (d) { 764 return graph.getNodeHeight(d); 765 }) 766 767 function appendInputAndOutputBubbles(g, d) { 768 for (var i = 0; i < d.inputs.length; ++i) { 769 var x = d.getInputX(i); 770 var y = -DEFAULT_NODE_BUBBLE_RADIUS; 771 var s = g.append('circle') 772 .classed("filledBubbleStyle", function (c) { 773 return d.inputs[i].isVisible(); 774 }) 775 .classed("bubbleStyle", function (c) { 776 return !d.inputs[i].isVisible(); 777 }) 778 .attr("id", "ib," + d.inputs[i].stringID()) 779 .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 780 .attr("transform", function (d) { 781 return "translate(" + x + "," + y + ")"; 782 }) 783 .on("click", function (d) { 784 var components = this.id.split(','); 785 var node = graph.nodeMap[components[3]]; 786 var edge = node.inputs[components[2]]; 787 var visible = !edge.isVisible(); 788 node.setInputVisibility(components[2], visible); 789 d3.event.stopPropagation(); 790 graph.updateGraphVisibility(); 791 }); 792 } 793 if (d.outputs.length != 0) { 794 var x = d.getOutputX(); 795 var y = graph.getNodeHeight(d) + DEFAULT_NODE_BUBBLE_RADIUS; 796 var s = g.append('circle') 797 .classed("filledBubbleStyle", function (c) { 798 return d.areAnyOutputsVisible() == 2; 799 }) 800 .classed("halFilledBubbleStyle", function (c) { 801 return d.areAnyOutputsVisible() == 1; 802 }) 803 .classed("bubbleStyle", function (c) { 804 return d.areAnyOutputsVisible() == 0; 805 }) 806 .attr("id", "ob," + d.id) 807 .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) 808 .attr("transform", function (d) { 809 return "translate(" + x + "," + y + ")"; 810 }) 811 .on("click", function (d) { 812 d.setOutputVisibility(d.areAnyOutputsVisible() == 0); 813 d3.event.stopPropagation(); 814 graph.updateGraphVisibility(); 815 }); 816 } 817 } 818 819 newGs.each(function (d) { 820 appendInputAndOutputBubbles(d3.select(this), d); 821 }); 822 823 newGs.each(function (d) { 824 d3.select(this).append("text") 825 .classed("label", true) 826 .attr("text-anchor", "right") 827 .attr("dx", 5) 828 .attr("dy", 5) 829 .append('tspan') 830 .text(function (l) { 831 return d.getDisplayLabel(); 832 }) 833 .append("title") 834 .text(function (l) { 835 return d.getTitle(); 836 }) 837 if (d.type != undefined) { 838 d3.select(this).append("text") 839 .classed("label", true) 840 .classed("type", true) 841 .attr("text-anchor", "right") 842 .attr("dx", 5) 843 .attr("dy", d.labelbbox.height + 5) 844 .append('tspan') 845 .text(function (l) { 846 return d.getDisplayType(); 847 }) 848 .append("title") 849 .text(function (l) { 850 return d.getType(); 851 }) 852 } 853 }); 854 855 const newAndOldNodes = newGs.merge(selNodes); 856 857 newAndOldNodes.select<SVGTextElement>('.type').each(function (d) { 858 this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); 859 }); 860 861 newAndOldNodes 862 .classed("selected", function (n) { 863 if (state.selection.isSelected(n)) return true; 864 return false; 865 }) 866 .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) 867 .select('rect') 868 .attr('height', function (d) { return graph.getNodeHeight(d); }); 869 870 graph.visibleBubbles = d3.selectAll('circle'); 871 872 graph.updateInputAndOutputBubbles(); 873 874 graph.maxGraphX = graph.maxGraphNodeX; 875 selEdges.attr("d", function (edge) { 876 return edge.generatePath(graph); 877 }); 878 } 879 880 getSvgViewDimensions() { 881 return [this.container.clientWidth, this.container.clientHeight]; 882 } 883 884 getSvgExtent(): [[number, number], [number, number]] { 885 return [[0, 0], [this.container.clientWidth, this.container.clientHeight]]; 886 } 887 888 minScale() { 889 const graph = this; 890 const dimensions = this.getSvgViewDimensions(); 891 const minXScale = dimensions[0] / (2 * graph.width); 892 const minYScale = dimensions[1] / (2 * graph.height); 893 const minScale = Math.min(minXScale, minYScale); 894 this.panZoom.scaleExtent([minScale, 40]); 895 return minScale; 896 } 897 898 onresize() { 899 const trans = d3.zoomTransform(this.svg.node()); 900 const ctrans = this.panZoom.constrain()(trans, this.getSvgExtent(), this.panZoom.translateExtent()) 901 this.panZoom.transform(this.svg, ctrans) 902 } 903 904 toggleTypes() { 905 var graph = this; 906 graph.state.showTypes = !graph.state.showTypes; 907 var element = document.getElementById('toggle-types'); 908 element.classList.toggle('button-input-toggled', graph.state.showTypes); 909 graph.updateGraphVisibility(); 910 } 911 912 viewSelection() { 913 var graph = this; 914 var minX, maxX, minY, maxY; 915 var hasSelection = false; 916 graph.visibleNodes.selectAll<SVGGElement, GNode>("g").each(function (n) { 917 if (graph.state.selection.isSelected(n)) { 918 hasSelection = true; 919 minX = minX ? Math.min(minX, n.x) : n.x; 920 maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : 921 n.x + n.getTotalNodeWidth(); 922 minY = minY ? Math.min(minY, n.y) : n.y; 923 maxY = maxY ? Math.max(maxY, n.y + graph.getNodeHeight(n)) : 924 n.y + graph.getNodeHeight(n); 925 } 926 }); 927 if (hasSelection) { 928 graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, 929 maxX + NODE_INPUT_WIDTH, maxY + 60, 930 true); 931 } 932 } 933 934 viewGraphRegion(minX, minY, maxX, maxY, transition) { 935 const [width, height] = this.getSvgViewDimensions(); 936 const dx = maxX - minX; 937 const dy = maxY - minY; 938 const x = (minX + maxX) / 2; 939 const y = (minY + maxY) / 2; 940 const scale = Math.min(width / (1.1 * dx), height / (1.1 * dy)); 941 const transform = d3.zoomIdentity.translate(1500, 100).scale(0.75); 942 this.svg 943 .transition().duration(300).call(this.panZoom.translateTo, x, y) 944 .transition().duration(300).call(this.panZoom.scaleTo, scale) 945 .transition().duration(300).call(this.panZoom.translateTo, x, y); 946 } 947 948 viewWholeGraph() { 949 this.panZoom.scaleTo(this.svg, 0); 950 this.panZoom.translateTo(this.svg, this.minGraphX + this.width / 2, this.minGraphY + this.height / 2) 951 } 952} 953