aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/tensorboard/components/tf-graph-common/lib/scene
diff options
context:
space:
mode:
Diffstat (limited to 'tensorflow/tensorboard/components/tf-graph-common/lib/scene')
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts223
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts177
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts269
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts525
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts409
5 files changed, 1603 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts
new file mode 100644
index 0000000000..82609e8652
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts
@@ -0,0 +1,223 @@
+/// <reference path="../graph.ts" />
+/// <reference path="../render.ts" />
+/// <reference path="scene.ts" />
+/// <reference path="edge.ts" />
+
+module tf.graph.scene.annotation {
+
+/**
+ * Populate a given annotation container group
+ *
+ * <g class="{in|out}-annotations"></g>
+ *
+ * with annotation group of the following structure:
+ *
+ * <g class="annotation">
+ * <g class="annotation-node">
+ * <!--
+ * Content here determined by Scene.node.buildGroup.
+ * -->
+ * </g>
+ * </g>
+ *
+ * @param container selection of the container.
+ * @param annotationData node.{in|out}Annotations
+ * @param d node to build group for.
+ * @param sceneBehavior polymer scene element.
+ * @return selection of appended objects
+ */
+export function buildGroup(container, annotationData: render.AnnotationList,
+ d: render.RenderNodeInformation, sceneBehavior) {
+ // Select all children and join with data.
+ let annotationGroups = container.selectAll(function() {
+ // using d3's selector function
+ // See https://github.com/mbostock/d3/releases/tag/v2.0.0
+ // (It's not listed in the d3 wiki.)
+ return this.childNodes;
+ })
+ .data(annotationData.list, d => { return d.node.name; });
+
+ annotationGroups.enter()
+ .append("g")
+ .attr("data-name", a => { return a.node.name; })
+ .each(function(a) {
+ let aGroup = d3.select(this);
+
+ // Add annotation to the index in the scene
+ sceneBehavior.addAnnotationGroup(a, d, aGroup);
+ // Append annotation edge
+ let edgeType = Class.Annotation.EDGE;
+ let metaedge = a.renderMetaedgeInfo && a.renderMetaedgeInfo.metaedge;
+ if (metaedge && !metaedge.numRegularEdges) {
+ edgeType += " " + Class.Annotation.CONTROL_EDGE;
+ }
+ // If any edges are reference edges, add the reference edge class.
+ if (metaedge && metaedge.numRefEdges) {
+ edgeType += " " + Class.Edge.REF_LINE;
+ }
+ edge.appendEdge(aGroup, a, sceneBehavior, edgeType);
+
+ if (a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) {
+ addAnnotationLabelFromNode(aGroup, a);
+ buildShape(aGroup, a, sceneBehavior);
+ } else {
+ addAnnotationLabel(aGroup, a.node.name, a, Class.Annotation.ELLIPSIS);
+ }
+ });
+
+ annotationGroups
+ .attr("class", a => {
+ return Class.Annotation.GROUP + " " +
+ annotationToClassName(a.annotationType) +
+ " " + node.nodeClass(a);
+ })
+ .each(function(a) {
+ let aGroup = d3.select(this);
+ update(aGroup, d, a, sceneBehavior);
+ if (a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) {
+ addInteraction(aGroup, d, sceneBehavior);
+ }
+ });
+
+ annotationGroups.exit()
+ .each(function(a) {
+ let aGroup = d3.select(this);
+
+ // Remove annotation from the index in the scene
+ sceneBehavior.removeAnnotationGroup(a, d, aGroup);
+ })
+ .remove();
+ return annotationGroups;
+};
+
+/**
+ * Maps an annotation enum to a class name used in css rules.
+ */
+function annotationToClassName(annotationType: render.AnnotationType) {
+ return (tf.graph.render.AnnotationType[annotationType] || "")
+ .toLowerCase() || null;
+}
+
+function buildShape(aGroup, a: render.Annotation, sceneBehavior) {
+ if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) {
+ let image = scene.selectOrCreateChild(aGroup, "image");
+ image.attr({
+ "xlink:href": sceneBehavior.resolveUrl("../../lib/svg/summary-icon.svg"),
+ "height": "12px",
+ "width": "12px",
+ "cursor": "pointer"
+ });
+ } else {
+ let shape = node.buildShape(aGroup, a, Class.Annotation.NODE);
+ // add title tag to get native tooltips
+ scene.selectOrCreateChild(shape, "title").text(a.node.name);
+ }
+}
+
+function addAnnotationLabelFromNode(aGroup, a: render.Annotation) {
+ let namePath = a.node.name.split("/");
+ let text = namePath[namePath.length - 1];
+ let shortenedText = text.length > 8 ? text.substring(0, 8) + "..." : text;
+ return addAnnotationLabel(aGroup, shortenedText, a, null, text);
+}
+
+function addAnnotationLabel(aGroup, label, a, additionalClassNames,
+ fullLabel?) {
+ let classNames = Class.Annotation.LABEL;
+ if (additionalClassNames) {
+ classNames += " " + additionalClassNames;
+ }
+ let titleText = fullLabel ? fullLabel : label;
+ return aGroup.append("text")
+ .attr("class", classNames)
+ .attr("dy", ".35em")
+ .attr("text-anchor", a.isIn ? "end" : "start")
+ .text(label)
+ .append("title").text(titleText);
+}
+
+function addInteraction(selection, d: render.RenderNodeInformation,
+ sceneBehavior) {
+ selection
+ .on("mouseover", a => {
+ sceneBehavior.fire("annotation-highlight", {
+ name: a.node.name,
+ hostName: d.node.name
+ });
+ })
+ .on("mouseout", a => {
+ sceneBehavior.fire("annotation-unhighlight", {
+ name: a.node.name,
+ hostName: d.node.name
+ });
+ })
+ .on("click", a => {
+ // Stop this event"s propagation so that it isn't also considered a
+ // graph-select.
+ (<Event>d3.event).stopPropagation();
+ sceneBehavior.fire("annotation-select", {
+ name: a.node.name,
+ hostName: d.node.name
+ });
+ });
+};
+
+/**
+ * Adjust annotation's position.
+ *
+ * @param aGroup selection of a "g.annotation" element.
+ * @param d Host node data.
+ * @param a annotation node data.
+ * @param scene Polymer scene element.
+ */
+function update(aGroup, d: render.RenderNodeInformation, a: render.Annotation,
+ sceneBehavior) {
+ // Annotations that point to embedded nodes (constants,summary)
+ // don't have a render information attached so we don't stylize these.
+ // Also we don't stylize ellipsis annotations (the string "... and X more").
+ if (a.renderNodeInfo &&
+ a.annotationType !== tf.graph.render.AnnotationType.ELLIPSIS) {
+ node.stylize(aGroup, a.renderNodeInfo, sceneBehavior,
+ Class.Annotation.NODE);
+ }
+
+ if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) {
+ // Update the width of the annotation to give space for the image.
+ a.width += 10;
+ }
+
+ // label position
+ aGroup.select("text." + Class.Annotation.LABEL).transition().attr({
+ x: d.x + a.dx + (a.isIn ? -1 : 1) * (a.width / 2 + a.labelOffset),
+ y: d.y + a.dy
+ });
+
+ // Some annotations (such as summary) are represented using a 12x12 image tag.
+ // Purposely ommited units (e.g. pixels) since the images are vector graphics.
+ // If there is an image, we adjust the location of the image to be vertically
+ // centered with the node and horizontally centered between the arrow and the
+ // text label.
+ aGroup.select("image").transition().attr({
+ x: d.x + a.dx - 3,
+ y: d.y + a.dy - 6
+ });
+
+ // Node position (only one of the shape selection will be non-empty.)
+ scene.positionEllipse(aGroup.select("." + Class.Annotation.NODE + " ellipse"),
+ d.x + a.dx, d.y + a.dy, a.width, a.height);
+ scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " rect"),
+ d.x + a.dx, d.y + a.dy, a.width, a.height);
+ scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " use"),
+ d.x + a.dx, d.y + a.dy, a.width, a.height);
+
+ // Edge position
+ aGroup.select("path." + Class.Annotation.EDGE).transition().attr("d", a => {
+ // map relative position to absolute position
+ let points = a.points.map(p => {
+ return {x: p.dx + d.x, y: p.dy + d.y};
+ });
+ return edge.interpolate(points);
+ });
+};
+
+} // close module
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts
new file mode 100644
index 0000000000..e11ec97f80
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts
@@ -0,0 +1,177 @@
+/// <reference path="../graph.ts" />
+/// <reference path="../render.ts" />
+/// <reference path="scene.ts" />
+
+module tf.graph.scene.edge {
+
+let Scene = tf.graph.scene; // Aliased
+
+export function getEdgeKey(edgeObj) {
+ return edgeObj.v + tf.graph.EDGE_KEY_DELIM + edgeObj.w;
+}
+
+/**
+ * Select or Create a "g.edges" group to a given sceneGroup
+ * and builds a number of "g.edge" groups inside the group.
+ *
+ * Structure Pattern:
+ *
+ * <g class="edges">
+ * <g class="edge">
+ * <path class="edgeline"/>
+ * </g>
+ * ...
+ * </g>
+ *
+ *
+ * @param sceneGroup container
+ * @param graph
+ * @param sceneBehavior Parent scene module.
+ * @return selection of the created nodeGroups
+ */
+export function buildGroup(sceneGroup,
+ graph: graphlib.Graph<tf.graph.render.RenderNodeInformation,
+ tf.graph.render.RenderMetaedgeInformation>, sceneBehavior) {
+ let edgeData = _.reduce(graph.edges(), (edges, edgeObj) => {
+ let edgeLabel = graph.edge(edgeObj);
+ edges.push({
+ v: edgeObj.v,
+ w: edgeObj.w,
+ label: edgeLabel
+ });
+ return edges;
+ }, []);
+
+ let container = scene.selectOrCreateChild(sceneGroup, "g",
+ Class.Edge.CONTAINER);
+ let containerNode = container.node();
+
+ // Select all children and join with data.
+ // (Note that all children of g.edges are g.edge)
+ let edgeGroups = container.selectAll(function() {
+ // using d3's selector function
+ // See https://github.com/mbostock/d3/releases/tag/v2.0.0
+ // (It's not listed in the d3 wiki.)
+ return this.childNodes;
+ })
+ .data(edgeData, getEdgeKey);
+
+ // Make edges a group to support rendering multiple lines for metaedge
+ edgeGroups.enter()
+ .append("g")
+ .attr("class", Class.Edge.GROUP)
+ .attr("data-edge", getEdgeKey)
+ .each(function(d) {
+ let edgeGroup = d3.select(this);
+ d.label.edgeGroup = edgeGroup;
+ // index node group for quick highlighting
+ sceneBehavior._edgeGroupIndex[getEdgeKey(d)] = edgeGroup;
+
+ // If any edges are reference edges, add the reference edge class.
+ let extraEdgeClass = d.label.metaedge && d.label.metaedge.numRefEdges
+ ? Class.Edge.REF_LINE + " " + Class.Edge.LINE
+ : undefined;
+ // Add line during enter because we're assuming that type of line
+ // normally does not change.
+ appendEdge(edgeGroup, d, scene, extraEdgeClass);
+ });
+
+ edgeGroups.each(position);
+ edgeGroups.each(function(d) {
+ stylize(d3.select(this), d, sceneBehavior);
+ });
+
+ edgeGroups.exit()
+ .each(d => {
+ delete sceneBehavior._edgeGroupIndex[getEdgeKey(d)];
+ })
+ .remove();
+ return edgeGroups;
+};
+
+/**
+ * For a given d3 selection and data object, create a path to represent the
+ * edge described in d.label.
+ *
+ * If d.label is defined, it will be a RenderMetaedgeInformation instance. It
+ * will sometimes be undefined, for example for some Annotation edges for which
+ * there is no underlying Metaedge in the hierarchical graph.
+ */
+export function appendEdge(edgeGroup, d, sceneBehavior, edgeClass?) {
+ edgeClass = edgeClass || Class.Edge.LINE; // set default type
+
+ if (d.label && d.label.structural) {
+ edgeClass += " " + Class.Edge.STRUCTURAL;
+ }
+
+ edgeGroup.append("path")
+ .attr("class", edgeClass);
+};
+
+/**
+ * Returns a tween interpolator for the endpoint of an edge path.
+ */
+function getEdgePathInterpolator(d, i, a) {
+ let renderMetaedgeInfo = d.label;
+ let adjoiningMetaedge = renderMetaedgeInfo.adjoiningMetaedge;
+ if (!adjoiningMetaedge) {
+ return d3.interpolate(a, interpolate(renderMetaedgeInfo.points));
+ }
+
+ let renderPath = this;
+
+ // Get the adjoining path that matches the adjoining metaedge.
+ let adjoiningPath =
+ <SVGPathElement>((<HTMLElement>adjoiningMetaedge.edgeGroup.node())
+ .firstChild);
+
+ // Find the desired SVGPoint along the adjoining path, then convert those
+ // coordinates into the space of the renderPath using its Current
+ // Transformation Matrix (CTM).
+ let inbound = renderMetaedgeInfo.metaedge.inbound;
+
+ return function(t) {
+ let adjoiningPoint = adjoiningPath
+ .getPointAtLength(inbound ? adjoiningPath.getTotalLength() : 0)
+ .matrixTransform(adjoiningPath.getCTM())
+ .matrixTransform(renderPath.getCTM().inverse());
+
+ // Update the relevant point in the renderMetaedgeInfo's points list, then
+ // re-interpolate the path.
+ let points = renderMetaedgeInfo.points;
+ let index = inbound ? 0 : points.length - 1;
+ points[index].x = adjoiningPoint.x;
+ points[index].y = adjoiningPoint.y;
+ let dPath = interpolate(points);
+ return dPath;
+ };
+}
+
+export let interpolate = d3.svg.line()
+ .interpolate("basis")
+ .x((d: any) => { return d.x; })
+ .y((d: any) => { return d.y; });
+
+function position(d) {
+ d3.select(this).select("path." + Class.Edge.LINE)
+ .each(function(d) {
+ let path = d3.select(this);
+ path.transition().attrTween("d", getEdgePathInterpolator);
+ });
+};
+
+/**
+ * For a given d3 selection and data object, mark the edge as a control
+ * dependency if it contains only control edges.
+ *
+ * d's label property will be a RenderMetaedgeInformation object.
+ */
+function stylize(edgeGroup, d, stylize) {
+ let a;
+ let metaedge = d.label.metaedge;
+ edgeGroup
+ .select("path." + Class.Edge.LINE)
+ .classed("control-dep", metaedge && !metaedge.numRegularEdges);
+};
+
+} // close module
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts
new file mode 100644
index 0000000000..1a34132765
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts
@@ -0,0 +1,269 @@
+/// <reference path="../../../../typings/tsd.d.ts" />
+/// <reference path="../common.ts" />
+
+module tf.scene {
+
+/** Show minimap when the viewpoint area is less than X% of the whole area. */
+const FRAC_VIEWPOINT_AREA: number = 0.8;
+
+export class Minimap {
+ /** The minimap container. */
+ private minimap: HTMLElement;
+ /** The canvas used for drawing the mini version of the svg. */
+ private canvas: HTMLCanvasElement;
+ /** A buffer canvas used for temporary drawing to avoid flickering. */
+ private canvasBuffer: HTMLCanvasElement;
+
+ /** The minimap svg used for holding the viewpoint rectangle. */
+ private minimapSvg: SVGSVGElement;
+ /** The rectangle showing the current viewpoint. */
+ private viewpoint: SVGRectElement;
+ /**
+ * The scale factor for the minimap. The factor is determined automatically
+ * so that the minimap doesn't violate the maximum width/height specified
+ * in the constructor. The minimap maintains the same aspect ratio as the
+ * original svg.
+ */
+ private scaleMinimap: number;
+ /** The main svg element. */
+ private svg: SVGSVGElement;
+ /** The svg group used for panning and zooming the main svg. */
+ private zoomG: SVGGElement;
+ /** The zoom behavior of the main svg. */
+ private mainZoom: d3.behavior.Zoom<any>;
+ /** The maximum width and height for the minimap. */
+ private maxWandH: number;
+ /** The last translation vector used in the main svg. */
+ private translate: [number, number];
+ /** The last scaling factor used in the main svg. */
+ private scaleMain: number;
+ /** The coordinates of the viewpoint rectangle. */
+ private viewpointCoord: {x: number, y: number};
+ /** The current size of the minimap */
+ private minimapSize: {width: number, height: number};
+ /** Padding (px) due to the main labels of the graph. */
+ private labelPadding: number;
+ /**
+ * Constructs a new minimap.
+ *
+ * @param svg The main svg element.
+ * @param zoomG The svg group used for panning and zooming the main svg.
+ * @param mainZoom The main zoom behavior.
+ * @param minimap The minimap container.
+ * @param maxWandH The maximum width/height for the minimap.
+ * @param labelPadding Padding in pixels due to the main graph labels.
+ */
+ constructor(svg: SVGSVGElement, zoomG: SVGGElement,
+ mainZoom: d3.behavior.Zoom<any>, minimap: HTMLElement,
+ maxWandH: number, labelPadding: number) {
+ this.svg = svg;
+ this.labelPadding = labelPadding;
+ this.zoomG = zoomG;
+ this.mainZoom = mainZoom;
+ this.maxWandH = maxWandH;
+ let $minimap = d3.select(minimap);
+ // The minimap will have 2 main components: the canvas showing the content
+ // and an svg showing a rectangle of the currently zoomed/panned viewpoint.
+ let $minimapSvg = $minimap.select("svg");
+
+ // Make the viewpoint rectangle draggable.
+ let $viewpoint = $minimapSvg.select("rect");
+ let dragmove = (d) => {
+ this.viewpointCoord.x = (<DragEvent>d3.event).x;
+ this.viewpointCoord.y = (<DragEvent>d3.event).y;
+ this.updateViewpoint();
+ };
+ this.viewpointCoord = {x: 0, y: 0};
+ let drag = d3.behavior.drag().origin(Object).on("drag", dragmove);
+ $viewpoint.datum(this.viewpointCoord).call(drag);
+
+ // Make the minimap clickable.
+ $minimapSvg.on("click", () => {
+ if ((<Event>d3.event).defaultPrevented) {
+ // This click was part of a drag event, so suppress it.
+ return;
+ }
+ // Update the coordinates of the viewpoint.
+ let width = Number($viewpoint.attr("width"));
+ let height = Number($viewpoint.attr("height"));
+ let clickCoords = d3.mouse($minimapSvg.node());
+ this.viewpointCoord.x = clickCoords[0] - width / 2;
+ this.viewpointCoord.y = clickCoords[1] - height / 2;
+ this.updateViewpoint();
+ });
+ this.viewpoint = <SVGRectElement> $viewpoint.node();
+ this.minimapSvg = <SVGSVGElement> $minimapSvg.node();
+ this.minimap = minimap;
+ this.canvas = <HTMLCanvasElement> $minimap.select("canvas.first").node();
+ this.canvasBuffer =
+ <HTMLCanvasElement> $minimap.select("canvas.second").node();
+ }
+
+ /**
+ * Updates the position and the size of the viewpoint rectangle.
+ * It also notifies the main svg about the new panned position.
+ */
+ private updateViewpoint(): void {
+ // Update the coordinates of the viewpoint rectangle.
+ d3.select(this.viewpoint)
+ .attr("x", this.viewpointCoord.x)
+ .attr("y", this.viewpointCoord.y);
+ // Update the translation vector of the main svg to reflect the
+ // new viewpoint.
+ let mainX = - this.viewpointCoord.x * this.scaleMain / this.scaleMinimap;
+ let mainY = - this.viewpointCoord.y * this.scaleMain / this.scaleMinimap;
+ let zoomEvent = this.mainZoom.translate([mainX, mainY]).event;
+ d3.select(this.zoomG).call(zoomEvent);
+ }
+
+ /**
+ * Redraws the minimap. Should be called whenever the main svg
+ * was updated (e.g. when a node was expanded).
+ */
+ update(): void {
+ let $svg = d3.select(this.svg);
+ // Read all the style rules in the document and embed them into the svg.
+ // The svg needs to be self contained, i.e. all the style rules need to be
+ // embedded so the canvas output matches the origin.
+ let stylesText = "";
+ for (let k = 0; k < document.styleSheets.length; k++) {
+ try {
+ let cssRules = (<any>document.styleSheets[k]).cssRules ||
+ (<any>document.styleSheets[k]).rules;
+ if (cssRules == null) {
+ continue;
+ }
+ for (let i = 0; i < cssRules.length; i++) {
+ stylesText += cssRules[i].cssText + "\n";
+ }
+ } catch (e) {
+ if (e.name !== "SecurityError") {
+ throw e;
+ }
+ }
+ }
+
+ // Temporarily add the css rules to the main svg.
+ let svgStyle = $svg.append("style");
+ svgStyle.text(stylesText);
+
+ // Temporarily remove the zoom/pan transform from the main svg since we
+ // want the minimap to show a zoomed-out and centered view.
+ let $zoomG = d3.select(this.zoomG);
+ let zoomTransform = $zoomG.attr("transform");
+ $zoomG.attr("transform", null);
+
+ // Get the size of the entire scene.
+ let sceneSize = this.zoomG.getBBox();
+ // Since we add padding, account for that here.
+ sceneSize.height += this.labelPadding;
+
+ // Temporarily assign an explicit width/height to the main svg, since
+ // it doesn't have one (uses flex-box), but we need it for the canvas
+ // to work.
+ $svg.attr({
+ width: sceneSize.width,
+ height: sceneSize.height,
+ });
+
+ // Since the content inside the svg changed (e.g. a node was expanded),
+ // the aspect ratio have also changed. Thus, we need to update the scale
+ // factor of the minimap. The scale factor is determined such that both
+ // the width and height of the minimap are <= maximum specified w/h.
+ this.scaleMinimap =
+ this.maxWandH / Math.max(sceneSize.width, sceneSize.height);
+
+ this.minimapSize = {
+ width: sceneSize.width * this.scaleMinimap,
+ height: sceneSize.height * this.scaleMinimap
+ };
+
+ // Update the size of the minimap's svg, the buffer canvas and the
+ // viewpoint rect.
+ d3.select(this.minimapSvg).attr(<any>this.minimapSize);
+ d3.select(this.canvasBuffer).attr(<any>this.minimapSize);
+ if (this.translate != null && this.zoom != null) {
+ // Update the viewpoint rectangle shape since the aspect ratio of the
+ // map has changed.
+ requestAnimationFrame(() => this.zoom());
+ }
+
+ // Serialize the main svg to a string which will be used as the rendering
+ // content for the canvas.
+ let svgXml = (new XMLSerializer()).serializeToString(this.svg);
+
+ // Now that the svg is serialized for rendering, remove the temporarily
+ // assigned styles, explicit width and height and bring back the pan/zoom
+ // transform.
+ svgStyle.remove();
+ $svg.attr({
+ width: null,
+ height: null
+ });
+ $zoomG.attr("transform", zoomTransform);
+ let image = new Image();
+ image.onload = () => {
+ // Draw the svg content onto the buffer canvas.
+ let context = this.canvasBuffer.getContext("2d");
+ context.clearRect(0, 0, this.canvasBuffer.width,
+ this.canvasBuffer.height);
+ context.drawImage(image, 0, 0,
+ this.minimapSize.width, this.minimapSize.height);
+ requestAnimationFrame(() => {
+ // Hide the old canvas and show the new buffer canvas.
+ d3.select(this.canvasBuffer).style("display", null);
+ d3.select(this.canvas).style("display", "none");
+ // Swap the two canvases.
+ [this.canvas, this.canvasBuffer] = [this.canvasBuffer, this.canvas];
+ });
+ };
+ image.src = "data:image/svg+xml;base64," + btoa(svgXml);
+ }
+
+ /**
+ * Handles changes in zooming/panning. Should be called from the main svg
+ * to notify that a zoom/pan was performed and this minimap will update it's
+ * viewpoint rectangle.
+ *
+ * @param translate The translate vector, or none to use the last used one.
+ * @param scale The scaling factor, or none to use the last used one.
+ */
+ zoom(translate?: [number, number], scale?: number): void {
+ // Update the new translate and scale params, only if specified.
+ this.translate = translate || this.translate;
+ this.scaleMain = scale || this.scaleMain;
+ // Update the location of the viewpoint rectangle.
+ let svgRect = this.svg.getBoundingClientRect();
+ let $viewpoint = d3.select(this.viewpoint);
+ this.viewpointCoord.x = -this.translate[0] * this.scaleMinimap /
+ this.scaleMain;
+ this.viewpointCoord.y = -this.translate[1] * this.scaleMinimap /
+ this.scaleMain;
+ let viewpointWidth = svgRect.width * this.scaleMinimap / this.scaleMain;
+ let viewpointHeight = svgRect.height * this.scaleMinimap / this.scaleMain;
+ $viewpoint.attr({
+ x: this.viewpointCoord.x,
+ y: this.viewpointCoord.y,
+ width: viewpointWidth,
+ height: viewpointHeight
+ });
+ // Show/hide the minimap depending on the viewpoint area as fraction of the
+ // whole minimap.
+ let mapWidth = this.minimapSize.width;
+ let mapHeight = this.minimapSize.height;
+ let x = this.viewpointCoord.x;
+ let y = this.viewpointCoord.y;
+ let w = Math.min(Math.max(0, x + viewpointWidth), mapWidth) -
+ Math.min(Math.max(0, x), mapWidth);
+ let h = Math.min(Math.max(0, y + viewpointHeight), mapHeight) -
+ Math.min(Math.max(0, y), mapHeight);
+ let fracIntersect = (w * h) / (mapWidth * mapHeight);
+ if (fracIntersect < FRAC_VIEWPOINT_AREA) {
+ this.minimap.classList.remove("hidden");
+ } else {
+ this.minimap.classList.add("hidden");
+ }
+ }
+}
+
+} // close module tf.scene
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts
new file mode 100644
index 0000000000..8c74b37e07
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts
@@ -0,0 +1,525 @@
+/// <reference path="../graph.ts" />
+/// <reference path="scene.ts" />
+/// <reference path="annotation.ts" />
+
+module tf.graph.scene.node {
+
+/**
+ * Select or Create a "g.nodes" group to a given sceneGroup
+ * and builds a number of "g.node" groups inside the group.
+ *
+ * Structure Pattern:
+ *
+ * <g class="nodes">
+ * <g class="node">
+ * <g class="in-annotations">
+ * ...
+ * </g>
+ * <g class="out-annotations">
+ * ...
+ * </g>
+ * <g class="nodeshape">
+ * <!--
+ * Content of the node shape should be for the node itself. For example a
+ * Metanode would have a <rect> with rounded edges, an op would have an
+ * <ellipse>. More complex nodes like series may contain multiple elements
+ * which are conditionally visible based on whether the node is expanded.
+ * -->
+ * </g>
+ * <text class="label">node name</text>
+ * <g class="subscene">
+ * <!--
+ * Content of the subscene (only for metanode and series node).
+ *
+ * Subscene is a svg group that contains content of the
+ * metanode's metagraph that is recursively generated by Scene.build().
+ *
+ * When the graph is expanded multiple times, a subscene can contain
+ * nested subscenes inside.
+ * -->
+ * </g>
+ * </g>
+ * ...
+ * </g>
+ *
+ *
+ * @param sceneGroup selection of the container
+ * @param nodeData array of render node information to map
+ * @param sceneBehavior parent scene module
+ * @return selection of the created nodeGroups
+ */
+export function buildGroup(sceneGroup,
+ nodeData: render.RenderNodeInformation[], sceneBehavior) {
+ let container = scene.selectOrCreateChild(sceneGroup, "g",
+ Class.Node.CONTAINER);
+ // Select all children and join with data.
+ // (Note that all children of g.nodes are g.node)
+ let nodeGroups = container.selectAll(function() {
+ // using d3's selector function
+ // See https://github.com/mbostock/d3/releases/tag/v2.0.0
+ // (It's not listed in the d3 wiki.)
+ return this.childNodes; // this here refers to container.node()
+ })
+ .data(nodeData, (d: any) => {
+ // make sure that we don't have to swap shape type
+ return d.node.name + ":" + d.node.type;
+ });
+
+ // ENTER
+ nodeGroups.enter()
+ .append("g")
+ .attr("data-name", d => { return d.node.name; })
+ .each(function(d) {
+ let nodeGroup = d3.select(this);
+ // index node group for quick stylizing
+ sceneBehavior.addNodeGroup(d.node.name, nodeGroup);
+ });
+
+ // UPDATE
+ nodeGroups
+ .attr("class", d => {
+ return Class.Node.GROUP + " " + nodeClass(d);
+ })
+ .each(function(d) {
+ let nodeGroup = d3.select(this);
+ // add g.in-annotations (always add -- to keep layer order consistent.)
+ let inAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g",
+ Class.Annotation.INBOX);
+ annotation.buildGroup(inAnnotationBox, d.inAnnotations, d,
+ sceneBehavior);
+
+ // add g.out-annotations (always add -- to keep layer order consistent.)
+ let outAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g",
+ Class.Annotation.OUTBOX);
+ annotation.buildGroup(outAnnotationBox, d.outAnnotations, d,
+ sceneBehavior);
+
+ // label
+ let label = labelBuild(nodeGroup, d, sceneBehavior);
+ // Do not add interaction to metanode labels as they live inside the
+ // metanode shape which already has the same interactions.
+ addInteraction(label, d, sceneBehavior, d.node.type === NodeType.META);
+
+ // build .shape below label
+ let shape = buildShape(nodeGroup, d, Class.Node.SHAPE, label.node());
+ if (d.node.isGroupNode) {
+ addButton(shape, d, sceneBehavior);
+ }
+ addInteraction(shape, d, sceneBehavior);
+
+ // build subscene on the top
+ subsceneBuild(nodeGroup, d, sceneBehavior);
+
+ stylize(nodeGroup, d, sceneBehavior);
+ position(nodeGroup, d, sceneBehavior);
+ });
+
+ // EXIT
+ nodeGroups.exit()
+ .each(function(d) {
+ // remove all indices on remove
+ sceneBehavior.removeNodeGroup(d.node.name);
+
+ let nodeGroup = d3.select(this);
+ if (d.inAnnotations.list.length > 0) {
+ nodeGroup.select("." + Class.Annotation.INBOX)
+ .selectAll("." + Class.Annotation.GROUP)
+ .each(a => {
+ sceneBehavior.removeAnnotationGroup(a, d);
+ });
+ }
+ if (d.outAnnotations.list.length > 0) {
+ nodeGroup.select("." + Class.Annotation.OUTBOX)
+ .selectAll("." + Class.Annotation.GROUP)
+ .each(a => {
+ sceneBehavior.removeAnnotationGroup(a, d);
+ });
+ }
+ })
+ .remove();
+ return nodeGroups;
+};
+
+/**
+ * Update or remove the subscene of a render group node depending on whether it
+ * is a expanded. If the node is not a group node, this method has no effect.
+ *
+ * @param nodeGroup selection of the container
+ * @param renderNodeInfo the render information for the node.
+ * @param sceneBehavior parent scene module
+ * @return Selection of the subscene group, or null if node group does not have
+ * a subscene. Op nodes, bridge nodes and unexpanded group nodes will
+ * not have a subscene.
+ */
+function subsceneBuild(nodeGroup,
+ renderNodeInfo: render.RenderGroupNodeInformation, sceneBehavior) {
+ if (renderNodeInfo.node.isGroupNode) {
+ if (renderNodeInfo.expanded) {
+ // Recursively build the subscene.
+ return scene.buildGroup(nodeGroup, renderNodeInfo, sceneBehavior,
+ Class.Subscene.GROUP);
+ }
+ // Clean out existing subscene if the node is not expanded.
+ scene.selectChild(nodeGroup, "g", Class.Subscene.GROUP).remove();
+ }
+ return null;
+};
+
+/**
+ * Translate the subscene of the given node group
+ */
+function subscenePosition(nodeGroup, d: render.RenderNodeInformation) {
+ let x0 = d.x - d.width / 2.0 + d.paddingLeft;
+ let y0 = d.y - d.height / 2.0 + d.paddingTop;
+
+ let subscene = scene.selectChild(nodeGroup, "g", Class.Subscene.GROUP);
+ scene.translate(subscene, x0, y0);
+};
+
+/**
+ * Add an expand/collapse button to a group node
+ *
+ * @param selection The group node selection.
+ * @param d Info about the node being rendered.
+ * @param sceneBehavior parent scene module.
+ */
+function addButton(selection, d: render.RenderNodeInformation, sceneBehavior) {
+ let group = scene.selectOrCreateChild(
+ selection, "g", Class.Node.BUTTON_CONTAINER);
+ scene.selectOrCreateChild(group, "circle", Class.Node.BUTTON_CIRCLE);
+ scene.selectOrCreateChild(group, "path", Class.Node.EXPAND_BUTTON).attr(
+ "d", "M0,-2.2 V2.2 M-2.2,0 H2.2");
+ scene.selectOrCreateChild(group, "path", Class.Node.COLLAPSE_BUTTON).attr(
+ "d", "M-2.2,0 H2.2");
+ group.on("click", d => {
+ // Stop this event's propagation so that it isn't also considered a
+ // node-select.
+ (<Event>d3.event).stopPropagation();
+ sceneBehavior.fire("node-toggle-expand", { name: d.node.name });
+ });
+ scene.positionButton(group, d);
+};
+
+/**
+ * Fire node-* events when the selection is interacted.
+ *
+ * @param disableInteraction When true, have the provided selection
+ * ignore all pointer events. Used for text labels inside of metanodes, which
+ * don't need interaction as their surrounding shape has interaction, and if
+ * given interaction would cause conflicts with the expand/collapse button.
+ */
+function addInteraction(selection, d: render.RenderNodeInformation,
+ sceneBehavior, disableInteraction?: boolean) {
+ if (disableInteraction) {
+ selection.attr("pointer-events", "none");
+ return;
+ }
+ selection.on("dblclick", d => {
+ sceneBehavior.fire("node-toggle-expand", { name: d.node.name });
+ })
+ .on("mouseover", d => {
+ // don't send mouseover over expanded group,
+ // otherwise it is causing too much glitches
+ if (sceneBehavior.isNodeExpanded(d)) { return; }
+
+ sceneBehavior.fire("node-highlight", { name: d.node.name });
+ })
+ .on("mouseout", d => {
+ // don't send mouseover over expanded group,
+ // otherwise it is causing too much glitches
+ if (sceneBehavior.isNodeExpanded(d)) { return; }
+
+ sceneBehavior.fire("node-unhighlight", { name: d.node.name });
+ })
+ .on("click", d => {
+ // Stop this event's propagation so that it isn't also considered
+ // a graph-select.
+ (<Event>d3.event).stopPropagation();
+ sceneBehavior.fire("node-select", { name: d.node.name });
+ });
+};
+
+/**
+ * Append svg text for label and assign data.
+ * @param nodeGroup
+ * @param renderNodeInfo The render node information for the label.
+ * @param sceneBehavior parent scene module.
+ */
+function labelBuild(nodeGroup, renderNodeInfo: render.RenderNodeInformation,
+ sceneBehavior) {
+ let namePath = renderNodeInfo.node.name.split("/");
+ let text = namePath[namePath.length - 1];
+
+ // Truncate long labels for unexpanded Metanodes.
+ let useFontScale = renderNodeInfo.node.type === NodeType.META &&
+ !renderNodeInfo.expanded;
+
+ let label = scene.selectOrCreateChild(nodeGroup, "text", Class.Node.LABEL);
+ label.attr("dy", ".35em")
+ .attr("text-anchor", "middle");
+ if (useFontScale) {
+ if (text.length > sceneBehavior.maxMetanodeLabelLength) {
+ text = text.substr(0, sceneBehavior.maxMetanodeLabelLength - 2) + "...";
+ }
+ let scale = getLabelFontScale(sceneBehavior);
+ label.attr("font-size", scale(text.length) + "px");
+ }
+ label.text(text);
+ return label;
+};
+
+/**
+ * d3 scale used for sizing font of labels, used by labelBuild,
+ * initialized once by getLabelFontScale.
+ */
+let fontScale = null;
+function getLabelFontScale(sceneBehavior) {
+ if (!fontScale) {
+ fontScale = d3.scale.linear()
+ .domain([sceneBehavior.maxMetanodeLabelLengthLargeFont,
+ sceneBehavior.maxMetanodeLabelLength])
+ .range([sceneBehavior.maxMetanodeLabelLengthFontSize,
+ sceneBehavior.minMetanodeLabelLengthFontSize]).clamp(true);
+ }
+ return fontScale;
+}
+/**
+ * Set label position of a given node group
+ */
+function labelPosition(nodeGroup, d: render.RenderNodeInformation,
+ yOffset: number) {
+ scene.selectChild(nodeGroup, "text", Class.Node.LABEL).transition()
+ .attr("x", d.x)
+ .attr("y", d.y + yOffset);
+};
+
+/**
+ * Select or append/insert shape for a node and assign renderNode
+ * as the shape's data.
+ *
+ * @param nodeGroup
+ * @param d RenderNodeInformation
+ * @param nodeClass class for the element.
+ * @param before Reference DOM node for insertion.
+ * @return Selection of the shape.
+ */
+export function buildShape(nodeGroup, d, nodeClass: string, before?) {
+ // Create a group to house the underlying visual elements.
+ let shapeGroup = scene.selectOrCreateChild(nodeGroup, "g", nodeClass,
+ before);
+ // TODO(jimbo): DOM structure should be templated in HTML somewhere, not JS.
+ switch (d.node.type) {
+ case NodeType.OP:
+ scene.selectOrCreateChild(shapeGroup, "ellipse",
+ Class.Node.COLOR_TARGET);
+ break;
+ case NodeType.SERIES:
+ // Choose the correct stamp to use to represent this series.
+ let stampType = "annotation";
+ let groupNodeInfo = <render.RenderGroupNodeInformation>d;
+ if (groupNodeInfo.coreGraph) {
+ stampType = groupNodeInfo.node.hasNonControlEdges
+ ? "vertical" : "horizontal";
+ }
+ scene.selectOrCreateChild(shapeGroup, "use", Class.Node.COLOR_TARGET)
+ .attr("xlink:href", "#op-series-" + stampType + "-stamp");
+ scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ case NodeType.BRIDGE:
+ scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ case NodeType.META:
+ scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ default:
+ throw Error("Unrecognized node type: " + d.node.type);
+ }
+ return shapeGroup;
+};
+
+export function nodeClass(d: render.RenderNodeInformation) {
+ switch (d.node.type) {
+ case NodeType.OP:
+ return Class.OPNODE;
+ case NodeType.META:
+ return Class.METANODE;
+ case NodeType.SERIES:
+ return Class.SERIESNODE;
+ case NodeType.BRIDGE:
+ return Class.BRIDGENODE;
+ case NodeType.ELLIPSIS:
+ return Class.ELLIPSISNODE;
+ };
+ throw Error("Unrecognized node type: " + d.node.type);
+};
+
+/** Modify node and its subscene and its label's positional attributes */
+function position(nodeGroup, d: render.RenderNodeInformation, sceneBehavior) {
+ let shapeGroup = scene.selectChild(nodeGroup, "g", Class.Node.SHAPE);
+ switch (d.node.type) {
+ case NodeType.OP: {
+ // position shape
+ let shape = scene.selectChild(shapeGroup, "ellipse");
+ scene.positionEllipse(shape, d.x, d.y, d.width, d.height);
+ labelPosition(nodeGroup, d, d.labelOffset);
+ break;
+ }
+ case NodeType.META: {
+ // position shape
+ let shape = scene.selectChild(shapeGroup, "rect");
+ scene.positionRect(shape, d.x, d.y, d.width, d.height);
+
+ if (d.expanded) {
+ subscenePosition(nodeGroup, d);
+
+ // put label on top
+ labelPosition(nodeGroup, d,
+ - d.height / 2 + d.labelHeight / 2);
+ } else {
+ labelPosition(nodeGroup, d, 0);
+ }
+ break;
+ }
+ case NodeType.SERIES: {
+ let shape = scene.selectChild(shapeGroup, "use");
+ scene.positionRect(shape, d.x, d.y, d.width, d.height);
+ if (d.expanded) {
+ subscenePosition(nodeGroup, d);
+
+ // put label on top
+ labelPosition(nodeGroup, d,
+ - d.height / 2 + d.labelHeight / 2);
+ } else {
+ labelPosition(nodeGroup, d, d.labelOffset);
+ }
+ }
+ case NodeType.BRIDGE: {
+ // position shape
+ // NOTE: In reality, these will not be visible, but it helps to put them
+ // in the correct position for debugging purposes.
+ let shape = scene.selectChild(shapeGroup, "rect");
+ scene.positionRect(shape, d.x, d.y, d.width, d.height);
+ break;
+ }
+ default: {
+ throw Error("Unrecognized node type: " + d.node.type);
+ }
+ }
+};
+
+/** Enum specifying the options to color nodes by */
+let ColorBy = {
+ STRUCTURE: 0,
+ DEVICE: 1,
+ COMPUTE_TIME: 2,
+ MEMORY: 3
+};
+
+/**
+ * Returns the fill color for the node given its state and the "color by"
+ * option.
+ */
+function getFillForNode(sceneBehavior, colorBy,
+ renderInfo: render.RenderNodeInformation, isExpanded: boolean): string {
+ let colorParams = tf.graph.render.MetanodeColors;
+ switch (colorBy) {
+ case ColorBy.STRUCTURE:
+ if (renderInfo.node.type === tf.graph.NodeType.META) {
+ let tid = (<Metanode>renderInfo.node).templateId;
+ return tid === null ? colorParams.UNKNOWN : colorParams.STRUCTURE_PALETTE(
+ sceneBehavior.templateIndex(tid), renderInfo.expanded);
+ } else if (renderInfo.node.type === tf.graph.NodeType.SERIES) {
+ // If expanded, we're showing the background rect, which we want to
+ // appear gray. Otherwise we're showing a stack of ellipses which we
+ // want to show white.
+ return renderInfo.expanded ? colorParams.EXPANDED_COLOR : "white";
+ } else if (renderInfo.node.type === NodeType.BRIDGE) {
+ return renderInfo.structural ? "#f0e" :
+ (<BridgeNode>renderInfo.node).inbound ? "#0ef" : "#fe0";
+ } else {
+ // Op nodes are white.
+ return "white";
+ }
+ case ColorBy.DEVICE:
+ if (renderInfo.deviceColors == null) {
+ // Return the hue for unknown device.
+ return colorParams.UNKNOWN;
+ }
+ let id = renderInfo.node.name;
+ let escapedId = tf.escapeQuerySelector(id);
+ let gradientDefs = d3.select("svg#svg defs #linearGradients");
+ let linearGradient =
+ gradientDefs.select("linearGradient#" + escapedId);
+ // If the linear gradient is not there yet, create it.
+ if (linearGradient.size() === 0) {
+ linearGradient = gradientDefs.append("linearGradient").attr("id", id);
+ // Re-create the stops of the linear gradient.
+ linearGradient.selectAll("*").remove();
+ let cumulativeProportion = 0;
+ // For each device, create a stop using the proportion of that device.
+ _.each(renderInfo.deviceColors, (d: any) => {
+ let color = d.color;
+ linearGradient.append("stop")
+ .attr("offset", cumulativeProportion)
+ .attr("stop-color", color);
+ linearGradient.append("stop")
+ .attr("offset", cumulativeProportion + d.proportion)
+ .attr("stop-color", color);
+ cumulativeProportion += d.proportion;
+ });
+ }
+ return isExpanded ? colorParams.EXPANDED_COLOR : `url(#${escapedId})`;
+ case ColorBy.COMPUTE_TIME:
+ return isExpanded ?
+ colorParams.EXPANDED_COLOR : renderInfo.computeTimeColor ||
+ colorParams.UNKNOWN;
+ case ColorBy.MEMORY:
+ return isExpanded ?
+ colorParams.EXPANDED_COLOR : renderInfo.memoryColor ||
+ colorParams.UNKNOWN;
+ default:
+ throw new Error("Unknown case to color nodes by");
+ }
+}
+
+/**
+ * Modify node style by toggling class and assign attributes (only for things
+ * that can't be done in css).
+ */
+export function stylize(nodeGroup, renderInfo: render.RenderNodeInformation,
+ sceneBehavior, nodeClass?) {
+ nodeClass = nodeClass || Class.Node.SHAPE;
+ let isHighlighted = sceneBehavior.isNodeHighlighted(renderInfo.node.name);
+ let isSelected = sceneBehavior.isNodeSelected(renderInfo.node.name);
+ let isExtract = renderInfo.isInExtract || renderInfo.isOutExtract;
+ let isExpanded = renderInfo.expanded;
+ nodeGroup.classed("highlighted", isHighlighted);
+ nodeGroup.classed("selected", isSelected);
+ nodeGroup.classed("extract", isExtract);
+ nodeGroup.classed("expanded", isExpanded);
+
+ // Main node always exists here and it will be reached before subscene,
+ // so d3 selection is fine here.
+ let node = nodeGroup.select("." + nodeClass + " ." + Class.Node.COLOR_TARGET);
+ let fillColor = getFillForNode(sceneBehavior,
+ ColorBy[sceneBehavior.colorBy.toUpperCase()],
+ renderInfo, isExpanded);
+ node.style("fill", fillColor);
+
+ // Choose outline to be darker version of node color if the node is a single
+ // color and is not selected.
+ if (isSelected) {
+ node.style("stroke", null);
+ } else {
+ // If node is colored by a gradient, then use a dark gray outline.
+ let outlineColor = fillColor.substring(0, 3) === "url" ?
+ tf.graph.render.MetanodeColors.GRADIENT_OUTLINE :
+ d3.rgb(fillColor).darker().toString();
+ node.style("stroke", outlineColor);
+ }
+};
+
+} // close module
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts
new file mode 100644
index 0000000000..2e2467f039
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts
@@ -0,0 +1,409 @@
+/// <reference path="../graph.ts" />
+/// <reference path="edge.ts" />
+/// <reference path="node.ts" />
+/// <reference path="../layout.ts" />
+
+module tf.graph.scene {
+
+/** Enums element class of objects in the scene */
+export let Class = {
+ Node: {
+ // <g> element that contains nodes.
+ CONTAINER: "nodes",
+ // <g> element that contains detail about a node.
+ GROUP: "node",
+ // <g> element that contains visual elements (like rect, ellipse).
+ SHAPE: "nodeshape",
+ // <*> element(s) under SHAPE that should receive color updates.
+ COLOR_TARGET: "nodecolortarget",
+ // <text> element showing the node's label.
+ LABEL: "nodelabel",
+ // <g> element that contains all visuals for the expand/collapse
+ // button for expandable group nodes.
+ BUTTON_CONTAINER: "buttoncontainer",
+ // <circle> element that surrounds expand/collapse buttons.
+ BUTTON_CIRCLE: "buttoncircle",
+ // <path> element of the expand button.
+ EXPAND_BUTTON: "expandbutton",
+ // <path> element of the collapse button.
+ COLLAPSE_BUTTON: "collapsebutton"
+ },
+ Edge: {
+ CONTAINER: "edges",
+ GROUP: "edge",
+ LINE: "edgeline",
+ REF_LINE: "refline",
+ STRUCTURAL: "structural"
+ },
+ Annotation: {
+ OUTBOX: "out-annotations",
+ INBOX: "in-annotations",
+ GROUP: "annotation",
+ NODE: "annotation-node",
+ EDGE: "annotation-edge",
+ CONTROL_EDGE: "annotation-control-edge",
+ LABEL: "annotation-label",
+ ELLIPSIS: "annotation-ellipsis"
+ },
+ Scene: {
+ GROUP: "scene",
+ CORE: "core",
+ INEXTRACT: "in-extract",
+ OUTEXTRACT: "out-extract"
+ },
+ Subscene: {
+ GROUP: "subscene"
+ },
+ OPNODE: "op",
+ METANODE: "meta",
+ SERIESNODE: "series",
+ BRIDGENODE: "bridge",
+ ELLIPSISNODE: "ellipsis"
+};
+
+/**
+ * Helper method for fitting the graph in the svg view.
+ *
+ * @param svg The main svg.
+ * @param zoomG The svg group used for panning and zooming.
+ * @param d3zoom The zoom behavior.
+ * @param callback Called when the fitting is done.
+ */
+export function fit(svg, zoomG, d3zoom, callback) {
+ let svgRect = svg.getBoundingClientRect();
+ let sceneSize = zoomG.getBBox();
+ let scale = 0.9 * Math.min(
+ svgRect.width / sceneSize.width,
+ svgRect.height / sceneSize.height,
+ 2
+ );
+ let params = layout.PARAMS.graph;
+ let zoomEvent = d3zoom.scale(scale)
+ .on("zoomend.fitted", () => {
+ // Remove the listener for the zoomend event,
+ // so we don't get called at the end of regular zoom events,
+ // just those that fit the graph to screen.
+ d3zoom.on("zoomend.fitted", null);
+ callback();
+ })
+ .translate([params.padding.paddingLeft, params.padding.paddingTop])
+ .event;
+ d3.select(zoomG).transition().duration(500).call(zoomEvent);
+};
+
+/**
+ * Helper method for panning the graph to center on the provided node,
+ * if the node is currently off-screen.
+ *
+ * @param nodeName The node to center the graph on
+ * @param svg The root SVG element for the graph
+ * @param zoomG The svg group used for panning and zooming.
+ * @param d3zoom The zoom behavior.
+ * @return True if the graph had to be panned to display the
+ * provided node.
+ */
+export function panToNode(nodeName: String, svg, zoomG, d3zoom): boolean {
+ let node: any = d3.selectAll("[data-name='" + nodeName + "']."
+ + Class.Node.GROUP)[0][0];
+ if (!node) {
+ return false;
+ }
+ let translate = d3zoom.translate();
+ // Check if the selected node is off-screen in either
+ // X or Y dimension in either direction.
+ let nodeBox = node.getBBox();
+ let nodeCtm = node.getScreenCTM();
+ let pointTL = svg.createSVGPoint();
+ let pointBR = svg.createSVGPoint();
+ pointTL.x = nodeBox.x;
+ pointTL.y = nodeBox.y;
+ pointBR.x = nodeBox.x + nodeBox.width;
+ pointBR.y = nodeBox.y + nodeBox.height;
+ pointTL = pointTL.matrixTransform(nodeCtm);
+ pointBR = pointBR.matrixTransform(nodeCtm);
+ let isOutsideOfBounds = (start, end, bound) => {
+ return end < 0 || start > bound;
+ };
+ let svgRect = svg.getBoundingClientRect();
+ if (isOutsideOfBounds(pointTL.x, pointBR.x, svgRect.width) ||
+ isOutsideOfBounds(pointTL.y, pointBR.y, svgRect.height)) {
+ // Determine the amount to transform the graph in both X and Y
+ // dimensions in order to center the selected node. This takes into
+ // acount the position of the node, the size of the svg scene, the
+ // amount the scene has been scaled by through zooming, and any previous
+ // transform already performed by this logic.
+ let centerX = (pointTL.x + pointBR.x) / 2;
+ let centerY = (pointTL.y + pointBR.y) / 2;
+ let dx = ((svgRect.width / 2) - centerX);
+ let dy = ((svgRect.height / 2) - centerY);
+ let zoomEvent = d3zoom.translate([translate[0] + dx, translate[1] + dy])
+ .event;
+ d3.select(zoomG).transition().duration(500).call(zoomEvent);
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Given a container d3 selection, select a child svg element of a given tag
+ * and class if exists or append / insert one otherwise. If multiple children
+ * matches the tag and class name, returns only the first one.
+ *
+ * @param container
+ * @param tagName tag name.
+ * @param className (optional) Class name.
+ * @param before (optional) reference DOM node for insertion.
+ * @return selection of the element
+ */
+export function selectOrCreateChild(container, tagName: string,
+ className?: string, before?) {
+ let child = selectChild(container, tagName, className);
+ if (!child.empty()) {
+ return child;
+ }
+ let newElement = document.createElementNS("http://www.w3.org/2000/svg",
+ tagName);
+ if (className) {
+ newElement.classList.add(className);
+ }
+
+ if (before) { // if before exists, insert
+ container.node().insertBefore(newElement, before);
+ } else { // otherwise, append
+ container.node().appendChild(newElement);
+ }
+ return d3.select(newElement)
+ // need to bind data to emulate d3_selection.append
+ .datum(container.datum());
+};
+
+/**
+ * Given a container d3 selection, select a child element of a given tag and
+ * class. If multiple children matches the tag and class name, returns only
+ * the first one.
+ *
+ * @param container
+ * @param tagName tag name.
+ * @param className (optional) Class name.
+ * @return selection of the element, or an empty selection
+ */
+export function selectChild(container, tagName: string, className?: string) {
+ let children = container.node().childNodes;
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ if (child.tagName === tagName &&
+ (!className || child.classList.contains(className))
+ ) {
+ return d3.select(child);
+ }
+ }
+ return d3.select(null);
+};
+
+/**
+ * Select or create a sceneGroup and build/update its nodes and edges.
+ *
+ * Structure Pattern:
+ *
+ * <g class="scene">
+ * <g class="core">
+ * <g class="edges">
+ * ... stuff from tf.graph.scene.edges.build ...
+ * </g>
+ * <g class="nodes">
+ * ... stuff from tf.graph.scene.nodes.build ...
+ * </g>
+ * </g>
+ * <g class="in-extract">
+ * <g class="nodes">
+ * ... stuff from tf.graph.scene.nodes.build ...
+ * </g>
+ * </g>
+ * <g class="out-extract">
+ * <g class="nodes">
+ * ... stuff from tf.graph.scene.nodes.build ...
+ * </g>
+ * </g>
+ * </g>
+ *
+ * @param container D3 selection of the parent.
+ * @param renderNode render node of a metanode or series node.
+ * @param sceneBehavior Parent scene module.
+ * @param sceneClass class attribute of the scene (default="scene").
+ */
+export function buildGroup(container,
+ renderNode: render.RenderGroupNodeInformation,
+ sceneBehavior,
+ sceneClass: string) {
+ sceneClass = sceneClass || Class.Scene.GROUP;
+ let isNewSceneGroup = selectChild(container, "g", sceneClass).empty();
+ let sceneGroup = selectOrCreateChild(container, "g", sceneClass);
+
+ // core
+ let coreGroup = selectOrCreateChild(sceneGroup, "g", Class.Scene.CORE);
+ let coreNodes = _.reduce(renderNode.coreGraph.nodes(), (nodes, name) => {
+ let node = renderNode.coreGraph.node(name);
+ if (!node.excluded) {
+ nodes.push(node);
+ }
+ return nodes;
+ }, []);
+
+ if (renderNode.node.type === NodeType.SERIES) {
+ // For series, we want the first item on top, so reverse the array so
+ // the first item in the series becomes last item in the top, and thus
+ // is rendered on the top.
+ coreNodes.reverse();
+ }
+
+ // Create the layer of edges for this scene (paths).
+ edge.buildGroup(coreGroup, renderNode.coreGraph, sceneBehavior);
+
+ // Create the layer of nodes for this scene (ellipses, rects etc).
+ node.buildGroup(coreGroup, coreNodes, sceneBehavior);
+
+ // In-extract
+ if (renderNode.isolatedInExtract.length > 0) {
+ let inExtractGroup = selectOrCreateChild(sceneGroup, "g",
+ Class.Scene.INEXTRACT);
+ node.buildGroup(inExtractGroup, renderNode.isolatedInExtract,
+ sceneBehavior);
+ } else {
+ selectChild(sceneGroup, "g", Class.Scene.INEXTRACT).remove();
+ }
+
+ // Out-extract
+ if (renderNode.isolatedOutExtract.length > 0) {
+ let outExtractGroup = selectOrCreateChild(sceneGroup, "g",
+ Class.Scene.OUTEXTRACT);
+ node.buildGroup(outExtractGroup, renderNode.isolatedOutExtract,
+ sceneBehavior);
+ } else {
+ selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT).remove();
+ }
+
+ position(sceneGroup, renderNode);
+
+ // Fade in the scene group if it didn't already exist.
+ if (isNewSceneGroup) {
+ sceneGroup.attr("opacity", 0)
+ .transition().attr("opacity", 1);
+ }
+
+ return sceneGroup;
+};
+
+/**
+ * Given a scene's svg group, set g.in-extract, g.coreGraph, g.out-extract svg
+ * groups' position relative to the scene.
+ *
+ * @param sceneGroup
+ * @param renderNode render node of a metanode or series node.
+ */
+function position(sceneGroup, renderNode: render.RenderGroupNodeInformation) {
+ // Translate scenes down by the label height so that when showing graphs in
+ // expanded metanodes, the graphs are below the labels. Do not shift them
+ // down for series nodes as series nodes don't have labels inside of their
+ // bounding boxes.
+ let yTranslate = renderNode.node.type === NodeType.SERIES ?
+ 0 : layout.PARAMS.subscene.meta.labelHeight;
+
+ // core
+ translate(selectChild(sceneGroup, "g", Class.Scene.CORE),
+ 0, yTranslate);
+
+ // in-extract
+ let inExtractX = renderNode.coreBox.width === 0 ?
+ 0 : renderNode.coreBox.width;
+ let hasInExtract = renderNode.isolatedInExtract.length > 0;
+ if (hasInExtract) {
+ translate(selectChild(sceneGroup, "g", Class.Scene.INEXTRACT),
+ inExtractX, yTranslate);
+ }
+
+ // out-extract
+ let hasOutExtract = renderNode.isolatedOutExtract.length > 0;
+ if (hasOutExtract) {
+ let outExtractX = inExtractX + renderNode.inExtractBox.width
+ + renderNode.extractXOffset;
+ translate(selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT),
+ outExtractX, yTranslate);
+ }
+};
+
+/** Adds a click listener to a group that fires a graph-select event */
+export function addGraphClickListener(graphGroup, sceneBehavior) {
+ d3.select(graphGroup).on("click", () => {
+ sceneBehavior.fire("graph-select");
+ });
+};
+
+/** Helper for adding transform: translate(x0, y0) */
+export function translate(selection, x0: number, y0: number) {
+ selection.attr("transform", "translate(" + x0 + "," + y0 + ")");
+};
+
+/**
+ * Helper for setting position of a svg rect
+ * @param rect rect to set position of.
+ * @param cx Center x.
+ * @param cy Center x.
+ * @param width Width to set.
+ * @param height Height to set.
+ */
+export function positionRect(rect, cx: number, cy: number, width: number,
+ height: number) {
+ rect.transition().attr({
+ x: cx - width / 2,
+ y: cy - height / 2,
+ width: width,
+ height: height
+ });
+};
+
+/**
+ * Helper for setting position of a svg expand/collapse button
+ * @param button container group
+ * @param renderNode the render node of the group node to position
+ * the button on.
+ */
+export function positionButton(button,
+ renderNode: render.RenderNodeInformation) {
+ // Position the button in the top-right corner of the group node,
+ // with space given the draw the button inside of the corner.
+ let x = renderNode.x + renderNode.width / 2 - 6;
+ let y = renderNode.y - renderNode.height / 2 + 6;
+ // For unexpanded series nodes, the button has special placement due
+ // to the unique visuals of this group node.
+ if (renderNode.node.type === NodeType.SERIES && !renderNode.expanded) {
+ x += 10;
+ y -= 2;
+ }
+ let translateStr = "translate(" + x + "," + y + ")";
+ button.selectAll("path").transition().attr("transform", translateStr);
+ button.select("circle").transition().attr({
+ cx: x,
+ cy: y,
+ r: layout.PARAMS.nodeSize.meta.expandButtonRadius
+ });
+};
+
+/**
+ * Helper for setting position of a svg ellipse
+ * @param ellipse ellipse to set position of.
+ * @param cx Center x.
+ * @param cy Center x.
+ * @param width Width to set.
+ * @param height Height to set.
+ */
+export function positionEllipse(ellipse, cx: number, cy: number,
+ width: number, height: number) {
+ ellipse.transition().attr({
+ cx: cx,
+ cy: cy,
+ rx: width / 2,
+ ry: height / 2
+ });
+};
+
+} // close module