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