diff options
Diffstat (limited to 'tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts')
-rw-r--r-- | tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts | 409 |
1 files changed, 409 insertions, 0 deletions
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 |