• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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