// Copyright 2015 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. "use strict"; class GraphView extends View { constructor (d3, id, nodes, edges, broker) { super(id, broker); var graph = this; var svg = this.divElement.append("svg").attr('version','1.1').attr("width", "100%"); graph.svg = svg; graph.nodes = nodes || []; graph.edges = edges || []; graph.minGraphX = 0; graph.maxGraphX = 1; graph.minGraphY = 0; graph.maxGraphY = 1; graph.state = { selection: null, mouseDownNode: null, justDragged: false, justScaleTransGraph: false, lastKeyDown: -1, showTypes: false }; var selectionHandler = { clear: function() { broker.clear(selectionHandler); }, select: function(items, selected) { var ranges = []; for (var d of items) { if (selected) { d.classList.add("selected"); } else { d.classList.remove("selected"); } var data = d.__data__; ranges.push([data.pos, data.pos + 1, data.id]); } broker.select(selectionHandler, ranges, selected); }, selectionDifference: function(span1, inclusive1, span2, inclusive2) { // Should not be called }, brokeredSelect: function(ranges, selected) { var test = [].entries().next(); var selection = graph.nodes .filter(function(n) { var pos = n.pos; for (var range of ranges) { var start = range[0]; var end = range[1]; var id = range[2]; if (end != undefined) { if (pos >= start && pos < end) { return true; } } else if (start != undefined) { if (pos === start) { return true; } } else { if (n.id === id) { return true; } } } return false; }); var newlySelected = new Set(); selection.forEach(function(n) { newlySelected.add(n); if (!n.visible) { n.visible = true; } }); graph.updateGraphVisibility(); graph.visibleNodes.each(function(n) { if (newlySelected.has(n)) { graph.state.selection.select(this, selected); } }); graph.updateGraphVisibility(); graph.viewSelection(); }, brokeredClear: function() { graph.state.selection.clear(); } }; broker.addSelectionHandler(selectionHandler); graph.state.selection = new Selection(selectionHandler); var defs = svg.append('svg:defs'); defs.append('svg:marker') .attr('id', 'end-arrow') .attr('viewBox', '0 -4 8 8') .attr('refX', 2) .attr('markerWidth', 2.5) .attr('markerHeight', 2.5) .attr('orient', 'auto') .append('svg:path') .attr('d', 'M0,-4L8,0L0,4'); this.graphElement = svg.append("g"); graph.visibleEdges = this.graphElement.append("g").selectAll("g"); graph.visibleNodes = this.graphElement.append("g").selectAll("g"); graph.drag = d3.behavior.drag() .origin(function(d){ return {x: d.x, y: d.y}; }) .on("drag", function(args){ graph.state.justDragged = true; graph.dragmove.call(graph, args); }) d3.select("#upload").on("click", function(){ document.getElementById("hidden-file-upload").click(); }); d3.select("#layout").on("click", function(){ graph.updateGraphVisibility(); graph.layoutGraph(); graph.updateGraphVisibility(); graph.viewWholeGraph(); }); d3.select("#show-all").on("click", function(){ graph.nodes.filter(function(n) { n.visible = true; }) graph.edges.filter(function(e) { e.visible = true; }) graph.updateGraphVisibility(); graph.viewWholeGraph(); }); d3.select("#hide-unselected").on("click", function() { var unselected = graph.visibleNodes.filter(function(n) { return !this.classList.contains("selected"); }); unselected.each(function(n) { n.visible = false; }); graph.updateGraphVisibility(); }); d3.select("#hide-selected").on("click", function() { var selected = graph.visibleNodes.filter(function(n) { return this.classList.contains("selected"); }); selected.each(function(n) { n.visible = false; }); graph.state.selection.clear(); graph.updateGraphVisibility(); }); d3.select("#zoom-selection").on("click", function() { graph.viewSelection(); }); d3.select("#toggle-types").on("click", function() { graph.toggleTypes(); }); d3.select("#search-input").on("keydown", function() { if (d3.event.keyCode == 13) { graph.state.selection.clear(); var reg = new RegExp(this.value); var filterFunction = function(n) { return (reg.exec(n.getDisplayLabel()) != null || (graph.state.showTypes && reg.exec(n.getDisplayType())) || reg.exec(n.opcode) != null); }; if (d3.event.ctrlKey) { graph.nodes.forEach(function(n, i) { if (filterFunction(n)) { n.visible = true; } }); graph.updateGraphVisibility(); } var selected = graph.visibleNodes.each(function(n) { if (filterFunction(n)) { graph.state.selection.select(this, true); } }); graph.connectVisibleSelectedNodes(); graph.updateGraphVisibility(); this.blur(); graph.viewSelection(); } d3.event.stopPropagation(); }); // listen for key events d3.select(window).on("keydown", function(e){ graph.svgKeyDown.call(graph); }) .on("keyup", function(){ graph.svgKeyUp.call(graph); }); svg.on("mousedown", function(d){graph.svgMouseDown.call(graph, d);}); svg.on("mouseup", function(d){graph.svgMouseUp.call(graph, d);}); graph.dragSvg = d3.behavior.zoom() .on("zoom", function(){ if (d3.event.sourceEvent.shiftKey){ return false; } else{ graph.zoomed.call(graph); } return true; }) .on("zoomstart", function(){ if (!d3.event.sourceEvent.shiftKey) d3.select('body').style("cursor", "move"); }) .on("zoomend", function(){ d3.select('body').style("cursor", "auto"); }); svg.call(graph.dragSvg).on("dblclick.zoom", null); } static get selectedClass() { return "selected"; } static get rectClass() { return "nodeStyle"; } static get activeEditId() { return "active-editing"; } static get nodeRadius() { return 50; } getNodeHeight(graph) { if (this.state.showTypes) { return DEFAULT_NODE_HEIGHT + TYPE_HEIGHT; } else { return DEFAULT_NODE_HEIGHT; } } getEdgeFrontier(nodes, inEdges, edgeFilter) { let frontier = new Set(); nodes.forEach(function(element) { var edges = inEdges ? element.__data__.inputs : element.__data__.outputs; var edgeNumber = 0; edges.forEach(function(edge) { if (edgeFilter == undefined || edgeFilter(edge, edgeNumber)) { frontier.add(edge); } ++edgeNumber; }); }); return frontier; } getNodeFrontier(nodes, inEdges, edgeFilter) { let graph = this; var frontier = new Set(); var newState = true; var edgeFrontier = graph.getEdgeFrontier(nodes, inEdges, edgeFilter); // Control key toggles edges rather than just turning them on if (d3.event.ctrlKey) { edgeFrontier.forEach(function(edge) { if (edge.visible) { newState = false; } }); } edgeFrontier.forEach(function(edge) { edge.visible = newState; if (newState) { var node = inEdges ? edge.source : edge.target; node.visible = true; frontier.add(node); } }); graph.updateGraphVisibility(); if (newState) { return graph.visibleNodes.filter(function(n) { return frontier.has(n); }); } else { return undefined; } } dragmove(d) { var graph = this; d.x += d3.event.dx; d.y += d3.event.dy; graph.updateGraphVisibility(); } initializeContent(data, rememberedSelection) { this.createGraph(data, rememberedSelection); if (rememberedSelection != null) { this.attachSelection(rememberedSelection); this.connectVisibleSelectedNodes(); } this.updateGraphVisibility(); } deleteContent() { if (this.visibleNodes) { this.nodes = []; this.edges = []; this.nodeMap = []; this.updateGraphVisibility(); } }; createGraph(data, initiallyVisibileIds) { var g = this; g.nodes = data.nodes; g.nodeMap = []; var textMeasure = document.getElementById('text-measure'); g.nodes.forEach(function(n, i){ n.__proto__ = Node; n.visible = false; n.x = 0; n.y = 0; n.rank = MAX_RANK_SENTINEL; n.inputs = []; n.outputs = []; n.rpo = -1; n.outputApproach = MINIMUM_NODE_OUTPUT_APPROACH; n.cfg = n.control; g.nodeMap[n.id] = n; n.displayLabel = n.getDisplayLabel(); textMeasure.textContent = n.getDisplayLabel(); var width = textMeasure.getComputedTextLength(); textMeasure.textContent = n.getDisplayType(); width = Math.max(width, textMeasure.getComputedTextLength()); n.width = Math.alignUp(width + NODE_INPUT_WIDTH * 2, NODE_INPUT_WIDTH); }); g.edges = []; data.edges.forEach(function(e, i){ var t = g.nodeMap[e.target]; var s = g.nodeMap[e.source]; var newEdge = new Edge(t, e.index, s, e.type); t.inputs.push(newEdge); s.outputs.push(newEdge); g.edges.push(newEdge); if (e.type == 'control') { s.cfg = true; } }); g.nodes.forEach(function(n, i) { n.visible = isNodeInitiallyVisible(n); if (initiallyVisibileIds != undefined) { if (initiallyVisibileIds.has(n.id)) { n.visible = true; } } }); g.fitGraphViewToWindow(); g.updateGraphVisibility(); g.layoutGraph(); g.updateGraphVisibility(); g.viewWholeGraph(); } connectVisibleSelectedNodes() { var graph = this; graph.state.selection.selection.forEach(function(element) { var edgeNumber = 0; element.__data__.inputs.forEach(function(edge) { if (edge.source.visible && edge.target.visible) { edge.visible = true; } }); element.__data__.outputs.forEach(function(edge) { if (edge.source.visible && edge.target.visible) { edge.visible = true; } }); }); } updateInputAndOutputBubbles() { var g = this; var s = g.visibleBubbles; s.classed("filledBubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return edge.isVisible(); } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 2; } }).classed("halfFilledBubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return false; } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 1; } }).classed("bubbleStyle", function(c) { var components = this.id.split(','); if (components[0] == "ib") { var edge = g.nodeMap[components[3]].inputs[components[2]]; return !edge.isVisible(); } else { return g.nodeMap[components[1]].areAnyOutputsVisible() == 0; } }); s.each(function(c) { var components = this.id.split(','); if (components[0] == "ob") { var from = g.nodeMap[components[1]]; var x = from.getOutputX(); var y = g.getNodeHeight() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4; var transform = "translate(" + x + "," + y + ")"; this.setAttribute('transform', transform); } }); } attachSelection(s) { var graph = this; if (s.size != 0) { this.visibleNodes.each(function(n) { if (s.has(this.__data__.id)) { graph.state.selection.select(this, true); } }); } } detachSelection() { var selection = this.state.selection.detachSelection(); var s = new Set(); for (var i of selection) { s.add(i.__data__.id); }; return s; } pathMouseDown(path, d) { d3.event.stopPropagation(); this.state.selection.clear(); this.state.selection.add(path); }; nodeMouseDown(node, d) { d3.event.stopPropagation(); this.state.mouseDownNode = d; } nodeMouseUp(d3node, d) { var graph = this, state = graph.state, consts = graph.consts; var mouseDownNode = state.mouseDownNode; if (!mouseDownNode) return; if (mouseDownNode !== d){ // we're in a different node: create new edge for mousedown edge and add to graph var newEdge = {source: mouseDownNode, target: d}; var filtRes = graph.visibleEdges.filter(function(d){ if (d.source === newEdge.target && d.target === newEdge.source){ graph.edges.splice(graph.edges.indexOf(d), 1); } return d.source === newEdge.source && d.target === newEdge.target; }); if (!filtRes[0].length){ graph.edges.push(newEdge); graph.updateGraphVisibility(); } } else{ // we're in the same node if (state.justDragged) { // dragged, not clicked state.justDragged = false; } else{ // clicked, not dragged var extend = d3.event.shiftKey; var selection = graph.state.selection; if (!extend) { selection.clear(); } selection.select(d3node[0][0], true); } } } selectSourcePositions(start, end, selected) { var graph = this; var map = []; var sel = graph.nodes.filter(function(n) { var pos = (n.pos === undefined) ? -1 : n.getFunctionRelativeSourcePosition(graph); if (pos >= start && pos < end) { map[n.id] = true; n.visible = true; } }); graph.updateGraphVisibility(); graph.visibleNodes.filter(function(n) { return map[n.id]; }) .each(function(n) { var selection = graph.state.selection; selection.select(d3.select(this), selected); }); } selectAllNodes(inEdges, filter) { var graph = this; if (!d3.event.shiftKey) { graph.state.selection.clear(); } graph.state.selection.select(graph.visibleNodes[0], true); graph.updateGraphVisibility(); } svgMouseDown() { this.state.graphMouseDown = true; } svgMouseUp() { var graph = this, state = graph.state; if (state.justScaleTransGraph) { // Dragged state.justScaleTransGraph = false; } else { // Clicked if (state.mouseDownNode == null) { graph.state.selection.clear(); } } state.mouseDownNode = null; state.graphMouseDown = false; } svgKeyDown() { var state = this.state; var graph = this; // Don't handle key press repetition if(state.lastKeyDown !== -1) return; var showSelectionFrontierNodes = function(inEdges, filter, select) { var frontier = graph.getNodeFrontier(state.selection.selection, inEdges, filter); if (frontier != undefined) { if (select) { if (!d3.event.shiftKey) { state.selection.clear(); } state.selection.select(frontier[0], true); } graph.updateGraphVisibility(); } allowRepetition = false; } var allowRepetition = true; var eventHandled = true; // unless the below switch defaults switch(d3.event.keyCode) { case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: // '1'-'9' showSelectionFrontierNodes(true, (edge, index) => { return index == (d3.event.keyCode - 49); }, false); break; case 67: // 'c' showSelectionFrontierNodes(true, (edge, index) => { return edge.type == 'control'; }, false); break; case 69: // 'e' showSelectionFrontierNodes(true, (edge, index) => { return edge.type == 'effect'; }, false); break; case 79: // 'o' showSelectionFrontierNodes(false, undefined, false); break; case 73: // 'i' showSelectionFrontierNodes(true, undefined, false); break; case 65: // 'a' graph.selectAllNodes(); allowRepetition = false; break; case 38: case 40: { showSelectionFrontierNodes(d3.event.keyCode == 38, undefined, true); break; } default: eventHandled = false; break; } if (eventHandled) { d3.event.preventDefault(); } if (!allowRepetition) { state.lastKeyDown = d3.event.keyCode; } } svgKeyUp() { this.state.lastKeyDown = -1 }; layoutEdges() { var graph = this; graph.maxGraphX = graph.maxGraphNodeX; this.visibleEdges.attr("d", function(edge){ return edge.generatePath(graph); }); } layoutGraph() { layoutNodeGraph(this); } // call to propagate changes to graph updateGraphVisibility() { var graph = this, state = graph.state; var filteredEdges = graph.edges.filter(function(e) { return e.isVisible(); }); var visibleEdges = graph.visibleEdges.data(filteredEdges, function(edge) { return edge.stringID(); }); // add new paths visibleEdges.enter() .append('path') .style('marker-end','url(#end-arrow)') .classed('hidden', function(e) { return !e.isVisible(); }) .attr("id", function(edge){ return "e," + edge.stringID(); }) .on("mousedown", function(d){ graph.pathMouseDown.call(graph, d3.select(this), d); }) // Set the correct styles on all of the paths visibleEdges.classed('value', function(e) { return e.type == 'value' || e.type == 'context'; }).classed('control', function(e) { return e.type == 'control'; }).classed('effect', function(e) { return e.type == 'effect'; }).classed('frame-state', function(e) { return e.type == 'frame-state'; }).attr('stroke-dasharray', function(e) { if (e.type == 'frame-state') return "10,10"; return (e.type == 'effect') ? "5,5" : ""; }); // remove old links visibleEdges.exit().remove(); graph.visibleEdges = visibleEdges; // update existing nodes var filteredNodes = graph.nodes.filter(function(n) { return n.visible; }); graph.visibleNodes = graph.visibleNodes.data(filteredNodes, function(d) { return d.id; }); graph.visibleNodes.attr("transform", function(n){ return "translate(" + n.x + "," + n.y + ")"; }).select('rect'). attr(HEIGHT, function(d) { return graph.getNodeHeight(); }); // add new nodes var newGs = graph.visibleNodes.enter() .append("g"); newGs.classed("control", function(n) { return n.isControl(); }) .classed("javascript", function(n) { return n.isJavaScript(); }) .classed("input", function(n) { return n.isInput(); }) .classed("simplified", function(n) { return n.isSimplified(); }) .classed("machine", function(n) { return n.isMachine(); }) .attr("transform", function(d){ return "translate(" + d.x + "," + d.y + ")";}) .on("mousedown", function(d){ graph.nodeMouseDown.call(graph, d3.select(this), d); }) .on("mouseup", function(d){ graph.nodeMouseUp.call(graph, d3.select(this), d); }) .call(graph.drag); newGs.append("rect") .attr("rx", 10) .attr("ry", 10) .attr(WIDTH, function(d) { return d.getTotalNodeWidth(); }) .attr(HEIGHT, function(d) { return graph.getNodeHeight(); }) function appendInputAndOutputBubbles(g, d) { for (var i = 0; i < d.inputs.length; ++i) { var x = d.getInputX(i); var y = -DEFAULT_NODE_BUBBLE_RADIUS / 2 - 4; var s = g.append('circle') .classed("filledBubbleStyle", function(c) { return d.inputs[i].isVisible(); } ) .classed("bubbleStyle", function(c) { return !d.inputs[i].isVisible(); } ) .attr("id", "ib," + d.inputs[i].stringID()) .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) .attr("transform", function(d) { return "translate(" + x + "," + y + ")"; }) .on("mousedown", function(d){ var components = this.id.split(','); var node = graph.nodeMap[components[3]]; var edge = node.inputs[components[2]]; var visible = !edge.isVisible(); node.setInputVisibility(components[2], visible); d3.event.stopPropagation(); graph.updateGraphVisibility(); }); } if (d.outputs.length != 0) { var x = d.getOutputX(); var y = graph.getNodeHeight() + DEFAULT_NODE_BUBBLE_RADIUS / 2 + 4; var s = g.append('circle') .classed("filledBubbleStyle", function(c) { return d.areAnyOutputsVisible() == 2; } ) .classed("halFilledBubbleStyle", function(c) { return d.areAnyOutputsVisible() == 1; } ) .classed("bubbleStyle", function(c) { return d.areAnyOutputsVisible() == 0; } ) .attr("id", "ob," + d.id) .attr("r", DEFAULT_NODE_BUBBLE_RADIUS) .attr("transform", function(d) { return "translate(" + x + "," + y + ")"; }) .on("mousedown", function(d) { d.setOutputVisibility(d.areAnyOutputsVisible() == 0); d3.event.stopPropagation(); graph.updateGraphVisibility(); }); } } newGs.each(function(d){ appendInputAndOutputBubbles(d3.select(this), d); }); newGs.each(function(d){ d3.select(this).append("text") .classed("label", true) .attr("text-anchor","right") .attr("dx", "5") .attr("dy", DEFAULT_NODE_HEIGHT / 2 + 5) .append('tspan') .text(function(l) { return d.getDisplayLabel(); }) .append("title") .text(function(l) { return d.getLabel(); }) if (d.type != undefined) { d3.select(this).append("text") .classed("label", true) .classed("type", true) .attr("text-anchor","right") .attr("dx", "5") .attr("dy", DEFAULT_NODE_HEIGHT / 2 + TYPE_HEIGHT + 5) .append('tspan') .text(function(l) { return d.getDisplayType(); }) .append("title") .text(function(l) { return d.getType(); }) } }); graph.visibleNodes.select('.type').each(function (d) { this.setAttribute('visibility', graph.state.showTypes ? 'visible' : 'hidden'); }); // remove old nodes graph.visibleNodes.exit().remove(); graph.visibleBubbles = d3.selectAll('circle'); graph.updateInputAndOutputBubbles(); graph.layoutEdges(); graph.svg.style.height = '100%'; } getVisibleTranslation(translate, scale) { var graph = this; var height = (graph.maxGraphY - graph.minGraphY + 2 * GRAPH_MARGIN) * scale; var width = (graph.maxGraphX - graph.minGraphX + 2 * GRAPH_MARGIN) * scale; var dimensions = this.getSvgViewDimensions(); var baseY = translate[1]; var minY = (graph.minGraphY - GRAPH_MARGIN) * scale; var maxY = (graph.maxGraphY + GRAPH_MARGIN) * scale; var adjustY = 0; var adjustYCandidate = 0; if ((maxY + baseY) < dimensions[1]) { adjustYCandidate = dimensions[1] - (maxY + baseY); if ((minY + baseY + adjustYCandidate) > 0) { adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; } else { adjustY = adjustYCandidate; } } else if (-baseY < minY) { adjustYCandidate = -(baseY + minY); if ((maxY + baseY + adjustYCandidate) < dimensions[1]) { adjustY = (dimensions[1] / 2) - (maxY - (height / 2)) - baseY; } else { adjustY = adjustYCandidate; } } translate[1] += adjustY; var baseX = translate[0]; var minX = (graph.minGraphX - GRAPH_MARGIN) * scale; var maxX = (graph.maxGraphX + GRAPH_MARGIN) * scale; var adjustX = 0; var adjustXCandidate = 0; if ((maxX + baseX) < dimensions[0]) { adjustXCandidate = dimensions[0] - (maxX + baseX); if ((minX + baseX + adjustXCandidate) > 0) { adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; } else { adjustX = adjustXCandidate; } } else if (-baseX < minX) { adjustXCandidate = -(baseX + minX); if ((maxX + baseX + adjustXCandidate) < dimensions[0]) { adjustX = (dimensions[0] / 2) - (maxX - (width / 2)) - baseX; } else { adjustX = adjustXCandidate; } } translate[0] += adjustX; return translate; } translateClipped(translate, scale, transition) { var graph = this; var graphNode = this.graphElement[0][0]; var translate = this.getVisibleTranslation(translate, scale); if (transition) { graphNode.classList.add('visible-transition'); clearTimeout(graph.transitionTimout); graph.transitionTimout = setTimeout(function(){ graphNode.classList.remove('visible-transition'); }, 1000); } var translateString = "translate(" + translate[0] + "px," + translate[1] + "px) scale(" + scale + ")"; graphNode.style.transform = translateString; graph.dragSvg.translate(translate); graph.dragSvg.scale(scale); } zoomed(){ this.state.justScaleTransGraph = true; var scale = this.dragSvg.scale(); this.translateClipped(d3.event.translate, scale); } getSvgViewDimensions() { var canvasWidth = this.parentNode.clientWidth; var documentElement = document.documentElement; var canvasHeight = documentElement.clientHeight; return [canvasWidth, canvasHeight]; } minScale() { var graph = this; var dimensions = this.getSvgViewDimensions(); var width = graph.maxGraphX - graph.minGraphX; var height = graph.maxGraphY - graph.minGraphY; var minScale = dimensions[0] / (width + GRAPH_MARGIN * 2); var minScaleYCandidate = dimensions[1] / (height + GRAPH_MARGIN * 2); if (minScaleYCandidate < minScale) { minScale = minScaleYCandidate; } this.dragSvg.scaleExtent([minScale, 1.5]); return minScale; } fitGraphViewToWindow() { this.svg.attr("height", document.documentElement.clientHeight + "px"); this.translateClipped(this.dragSvg.translate(), this.dragSvg.scale()); } toggleTypes() { var graph = this; graph.state.showTypes = !graph.state.showTypes; var element = document.getElementById('toggle-types'); if (graph.state.showTypes) { element.classList.add('button-input-toggled'); } else { element.classList.remove('button-input-toggled'); } graph.updateGraphVisibility(); } viewSelection() { var graph = this; var minX, maxX, minY, maxY; var hasSelection = false; graph.visibleNodes.each(function(n) { if (this.classList.contains("selected")) { hasSelection = true; minX = minX ? Math.min(minX, n.x) : n.x; maxX = maxX ? Math.max(maxX, n.x + n.getTotalNodeWidth()) : n.x + n.getTotalNodeWidth(); minY = minY ? Math.min(minY, n.y) : n.y; maxY = maxY ? Math.max(maxY, n.y + DEFAULT_NODE_HEIGHT) : n.y + DEFAULT_NODE_HEIGHT; } }); if (hasSelection) { graph.viewGraphRegion(minX - NODE_INPUT_WIDTH, minY - 60, maxX + NODE_INPUT_WIDTH, maxY + 60, true); } } viewGraphRegion(minX, minY, maxX, maxY, transition) { var graph = this; var dimensions = this.getSvgViewDimensions(); var width = maxX - minX; var height = maxY - minY; var scale = Math.min(dimensions[0] / width, dimensions[1] / height); scale = Math.min(1.5, scale); scale = Math.max(graph.minScale(), scale); var translation = [-minX*scale, -minY*scale]; translation = graph.getVisibleTranslation(translation, scale); graph.translateClipped(translation, scale, transition); } viewWholeGraph() { var graph = this; var minScale = graph.minScale(); var translation = [0, 0]; translation = graph.getVisibleTranslation(translation, minScale); graph.translateClipped(translation, minScale); } }