aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts')
-rw-r--r--tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts628
1 files changed, 628 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts
new file mode 100644
index 0000000000..4eb3cab011
--- /dev/null
+++ b/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts
@@ -0,0 +1,628 @@
+/// <reference path="graph.ts" />
+/// <reference path="render.ts" />
+
+module tf.graph.layout {
+
+/** Set of parameters that define the look and feel of the graph. */
+export const PARAMS = {
+ animation: {
+ /** Default duration for graph animations in ms. */
+ duration: 250
+ },
+ graph: {
+ /** Graph parameter for metanode. */
+ meta: {
+ /**
+ * Dagre's nodesep param - number of pixels that
+ * separate nodes horizontally in the layout.
+ *
+ * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout
+ */
+ nodeSep: 110,
+ /**
+ * Dagre's ranksep param - number of pixels
+ * between each rank in the layout.
+ *
+ * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout
+ */
+ rankSep: 25
+ },
+ /** Graph parameter for metanode. */
+ series: {
+ /**
+ * Dagre's nodesep param - number of pixels that
+ * separate nodes horizontally in the layout.
+ *
+ * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout
+ */
+ nodeSep: 90,
+ /**
+ * Dagre's ranksep param - number of pixels
+ * between each rank in the layout.
+ *
+ * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout
+ */
+ rankSep: 25,
+ },
+ /**
+ * Padding is used to correctly position the graph SVG inside of its parent
+ * element. The padding amounts are applied using an SVG transform of X and
+ * Y coordinates.
+ */
+ padding: {
+ paddingTop: 40,
+ paddingLeft: 20
+ }
+ },
+ subscene: {
+ meta: {
+ paddingTop: 10,
+ paddingBottom: 10,
+ paddingLeft: 10,
+ paddingRight: 10,
+ /**
+ * Used to leave room for the label on top of the highest node in
+ * the core graph.
+ */
+ labelHeight: 20,
+ /** X-space between each extracted node and the core graph. */
+ extractXOffset: 50,
+ /** Y-space between each extracted node. */
+ extractYOffset: 20
+ },
+ series: {
+ paddingTop: 10,
+ paddingBottom: 10,
+ paddingLeft: 10,
+ paddingRight: 10,
+ labelHeight: 10
+ }
+ },
+ nodeSize: {
+ /** Size of meta nodes. */
+ meta: {
+ radius: 5,
+ width: 60,
+ /** A scale for the node's height based on number of nodes inside */
+ height: d3.scale.linear().domain([1, 200]).range([15, 60]).clamp(true),
+ /** The radius of the circle denoting the expand button. */
+ expandButtonRadius: 3
+ },
+ /** Size of op nodes. */
+ op: {
+ width: 15,
+ height: 6,
+ radius: 3, // for making annotation touching ellipse
+ labelOffset: -8
+ },
+ /** Size of series nodes. */
+ series: {
+ expanded: {
+ // For expanded series nodes, width and height will be
+ // computed to account for the subscene.
+ radius: 10,
+ labelOffset: 0,
+ },
+ vertical: {
+ // When unexpanded, series whose underlying metagraphs contain
+ // one or more non-control edges will show as a vertical stack
+ // of ellipses.
+ width: 16,
+ height: 13,
+ labelOffset: -13,
+ },
+ horizontal: {
+ // When unexpanded, series whose underlying metagraphs contain
+ // no non-control edges will show as a horizontal stack of
+ // ellipses.
+ width: 24,
+ height: 8,
+ radius: 10, // Forces annotations to center line.
+ labelOffset: -10,
+ },
+ },
+ /** Size of bridge nodes. */
+ bridge: {
+ // NOTE: bridge nodes will normally be invisible, but they must
+ // take up some space so that the layout step leaves room for
+ // their edges.
+ width: 20,
+ height: 20,
+ radius: 2,
+ labelOffset: 0
+ }
+ },
+ shortcutSize: {
+ /** Size of shortcuts for op nodes */
+ op: {
+ width: 10,
+ height: 4
+ },
+ /** Size of shortcuts for meta nodes */
+ meta: {
+ width: 12,
+ height: 4,
+ radius: 1
+ },
+ /** Size of shortcuts for series nodes */
+ series: {
+ width: 14,
+ height: 4,
+ }
+ },
+ annotations: {
+ /** X-space between the shape and each annotation-node. */
+ xOffset: 10,
+ /** Y-space between each annotation-node. */
+ yOffset: 3,
+ /** X-space between each annotation-node and its label. */
+ labelOffset: 2,
+ /** Estimate max width for annotation label */
+ labelWidth: 35
+ },
+ constant: {
+ size: {
+ width: 4,
+ height: 4
+ }
+ },
+ series: {
+ /** Maximum number of repeated item for unexpanded series node. */
+ maxStackCount: 3,
+ /**
+ * Positioning offset ratio for collapsed stack
+ * of parallel series (series without edges between its members).
+ */
+ parallelStackOffsetRatio: 0.2,
+ /**
+ * Positioning offset ratio for collapsed stack
+ * of tower series (series with edges between its members).
+ */
+ towerStackOffsetRatio: 0.5
+ },
+ minimap : {
+ /** The maximum width/height the minimap can have. */
+ size: 150
+ }
+};
+
+/** Calculate layout for a scene of a group node. */
+export function scene(renderNodeInfo: render.RenderGroupNodeInformation)
+ : void {
+ // Update layout, size, and annotations of its children nodes and edges.
+ if (renderNodeInfo.node.isGroupNode) {
+ layoutChildren(renderNodeInfo);
+ }
+
+ // Update position of its children nodes and edges
+ if (renderNodeInfo.node.type === NodeType.META) {
+ layoutMetanode(renderNodeInfo);
+ } else if (renderNodeInfo.node.type === NodeType.SERIES) {
+ layoutSeriesNode(renderNodeInfo);
+ }
+};
+
+/**
+ * Update layout, size, and annotations of its children nodes and edges.
+ */
+function layoutChildren(renderNodeInfo: render.RenderGroupNodeInformation)
+ : void {
+ let children = renderNodeInfo.coreGraph.nodes().map(n => {
+ return renderNodeInfo.coreGraph.node(n);
+ }).concat(renderNodeInfo.isolatedInExtract,
+ renderNodeInfo.isolatedOutExtract);
+
+ _.each(children, childNodeInfo => {
+ // Set size of each child
+ switch (childNodeInfo.node.type) {
+ case NodeType.OP:
+ _.extend(childNodeInfo, PARAMS.nodeSize.op);
+ break;
+ case NodeType.BRIDGE:
+ _.extend(childNodeInfo, PARAMS.nodeSize.bridge);
+ break;
+ case NodeType.META:
+ if (!childNodeInfo.expanded) {
+ // set fixed width and scalable height based on cardinality
+ _.extend(childNodeInfo, PARAMS.nodeSize.meta);
+ childNodeInfo.height =
+ PARAMS.nodeSize.meta.height(childNodeInfo.node.cardinality);
+ } else {
+ let childGroupNodeInfo =
+ <render.RenderGroupNodeInformation>childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ }
+ break;
+ case NodeType.SERIES:
+ if (childNodeInfo.expanded) {
+ _.extend(childNodeInfo, PARAMS.nodeSize.series.expanded);
+ let childGroupNodeInfo =
+ <render.RenderGroupNodeInformation>childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ } else {
+ let childGroupNodeInfo =
+ <render.RenderGroupNodeInformation>childNodeInfo;
+ let seriesParams =
+ childGroupNodeInfo.node.hasNonControlEdges ?
+ PARAMS.nodeSize.series.vertical :
+ PARAMS.nodeSize.series.horizontal;
+ _.extend(childNodeInfo, seriesParams);
+ }
+ break;
+ default:
+ throw Error("Unrecognized node type: " + childNodeInfo.node.type);
+ }
+
+ // Layout each child's annotations
+ layoutAnnotation(childNodeInfo);
+ });
+}
+
+/**
+ * Calculate layout for a graph using dagre
+ * @param graph the graph to be laid out
+ * @param params layout parameters
+ * @return width and height of the core graph
+ */
+function dagreLayout(graph: graphlib.Graph<any, any>, params)
+ : {height: number, width: number} {
+ _.extend(graph.graph(), {
+ nodeSep: params.nodeSep,
+ rankSep: params.rankSep
+ });
+
+ let bridgeNodeNames = [];
+ let nonBridgeNodeNames = [];
+
+ // Split out nodes into bridge and non-bridge nodes, and calculate the total
+ // width we should use for bridge nodes.
+ _.each(graph.nodes(), nodeName => {
+ let nodeInfo = graph.node(nodeName);
+ if (nodeInfo.node.type === NodeType.BRIDGE) {
+ bridgeNodeNames.push(nodeName);
+ } else {
+ nonBridgeNodeNames.push(nodeName);
+ }
+ });
+
+ // If there are no non-bridge nodes, then the graph has zero size.
+ if (!nonBridgeNodeNames.length) {
+ return {
+ width: 0,
+ height: 0,
+ };
+ }
+
+ dagre.layout(graph);
+
+ let graphLabel = graph.graph();
+
+ // Calculate the true bounding box of the graph by iterating over nodes and
+ // edges rather than accepting dagre's word for it. In particular, we should
+ // ignore the extra-wide bridge nodes and bridge edges, and allow for
+ // annotation boxes and labels.
+ let minX = Infinity;
+ let minY = Infinity;
+ let maxX = -Infinity;
+ let maxY = -Infinity;
+ _.each(nonBridgeNodeNames, nodeName => {
+ let nodeInfo = graph.node(nodeName);
+ let w = 0.5 * nodeInfo.width;
+ let x1 = nodeInfo.x - w - nodeInfo.inboxWidth;
+ let x2 = nodeInfo.x + w + nodeInfo.outboxWidth;
+ minX = x1 < minX ? x1 : minX;
+ maxX = x2 > maxX ? x2 : maxX;
+ let labelLength =
+ nodeName.length - nodeName.lastIndexOf(NAMESPACE_DELIM);
+ // TODO(jimbo): Account for font width rather than using a magic number.
+ let charWidth = 3; // 3 pixels per character.
+ let lw = 0.5 * labelLength * charWidth;
+ let lx1 = nodeInfo.x - lw;
+ let lx2 = nodeInfo.x + lw;
+ minX = lx1 < minX ? lx1 : minX;
+ maxX = lx2 > maxX ? lx2 : maxX;
+ // TODO(jimbo): Account for the height of labels above op nodes here.
+ let h = 0.5 * nodeInfo.outerHeight;
+ let y1 = nodeInfo.y - h;
+ let y2 = nodeInfo.y + h;
+ minY = y1 < minY ? y1 : minY;
+ maxY = y2 > maxY ? y2 : maxY;
+ });
+ _.each(graph.edges(), edgeObj => {
+ let renderMetaedgeInfo = graph.edge(edgeObj);
+ if (renderMetaedgeInfo.structural) {
+ return; // Skip structural edges from min/max calculations.
+ }
+ _.each(renderMetaedgeInfo.points,
+ (point: { x: number, y: number }) => {
+ minX = point.x < minX ? point.x : minX;
+ maxX = point.x > maxX ? point.x : maxX;
+ minY = point.y < minY ? point.y : minY;
+ maxY = point.y > maxY ? point.y : maxY;
+ });
+ });
+
+ // Shift all nodes and edge points to account for the left-padding amount,
+ // and the invisble bridge nodes.
+ _.each(graph.nodes(), nodeName => {
+ let nodeInfo = graph.node(nodeName);
+ nodeInfo.x -= minX;
+ nodeInfo.y -= minY;
+ });
+ _.each(graph.edges(), edgeObj => {
+ _.each(graph.edge(edgeObj).points,
+ (point: { x: number, y: number }) => {
+ point.x -= minX;
+ point.y -= minY;
+ });
+ });
+
+ return {
+ width: maxX - minX,
+ height: maxY - minY,
+ };
+}
+
+/** Layout a metanode. */
+function layoutMetanode(renderNodeInfo): void {
+ // First, copy params specific to meta nodes onto this render info object.
+ let params = PARAMS.subscene.meta;
+ renderNodeInfo = _.extend(renderNodeInfo, params);
+
+ // Invoke dagre.layout() on the core graph and record the bounding box
+ // dimensions.
+ _.extend(renderNodeInfo.coreBox,
+ dagreLayout(renderNodeInfo.coreGraph, PARAMS.graph.meta));
+
+ // Calculate the position of nodes in isolatedInExtract relative to the
+ // top-left corner of inExtractBox (the bounding box for all inExtract nodes)
+ // and calculate the size of the inExtractBox.
+ let hasInExtract = renderNodeInfo.isolatedInExtract.length > 0;
+
+ renderNodeInfo.inExtractBox.width = hasInExtract ?
+ _(renderNodeInfo.isolatedInExtract).pluck("outerWidth").max() : 0;
+
+ renderNodeInfo.inExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedInExtract, (height, child: any, i) => {
+ let yOffset = i > 0 ? params.extractYOffset : 0;
+ // use outerWidth/Height here to avoid overlaps between extracts
+ child.x = renderNodeInfo.inExtractBox.width / 2;
+ child.y = height + yOffset + child.outerHeight / 2;
+ return height + yOffset + child.outerHeight;
+ }, 0);
+
+ // Calculate the position of nodes in isolatedOutExtract relative to the
+ // top-left corner of outExtractBox (the bounding box for all outExtract
+ // nodes) and calculate the size of the outExtractBox.
+ let hasOutExtract = renderNodeInfo.isolatedOutExtract.length > 0;
+ renderNodeInfo.outExtractBox.width = hasOutExtract ?
+ _(renderNodeInfo.isolatedOutExtract).pluck("outerWidth").max() : 0;
+
+ renderNodeInfo.outExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedOutExtract, (height, child: any, i) => {
+ let yOffset = i > 0 ? params.extractYOffset : 0;
+ // use outerWidth/Height here to avoid overlaps between extracts
+ child.x = renderNodeInfo.outExtractBox.width / 2;
+ child.y = height + yOffset + child.outerHeight / 2;
+ return height + yOffset + child.outerHeight;
+ }, 0);
+
+ // Determine the whole metanode's width (from left to right).
+ renderNodeInfo.width =
+ params.paddingLeft + renderNodeInfo.coreBox.width + params.paddingRight +
+ (hasInExtract ?
+ renderNodeInfo.inExtractBox.width + params.extractXOffset : 0) +
+ (hasOutExtract ?
+ params.extractXOffset + renderNodeInfo.outExtractBox.width : 0);
+
+ // TODO(jimbo): Remove labelHeight and instead incorporate into box sizes.
+ // Determine the whole metanode's height (from top to bottom).
+ renderNodeInfo.height =
+ renderNodeInfo.labelHeight +
+ params.paddingTop +
+ Math.max(
+ renderNodeInfo.inExtractBox.height,
+ renderNodeInfo.coreBox.height,
+ renderNodeInfo.outExtractBox.height
+ ) +
+ params.paddingBottom;
+}
+
+/**
+ * Calculate layout for series node's core graph. Only called for an expanded
+ * series.
+ */
+function layoutSeriesNode(node: render.RenderGroupNodeInformation): void {
+ let graph = node.coreGraph;
+
+ let params = PARAMS.subscene.series;
+ _.extend(node, params);
+
+ // Layout the core.
+ _.extend(node.coreBox,
+ dagreLayout(node.coreGraph, PARAMS.graph.series));
+
+ _.each(graph.nodes(), nodeName => {
+ graph.node(nodeName).excluded = false;
+ });
+
+ // Series do not have in/outExtractBox so no need to include them here.
+ node.width = node.coreBox.width + params.paddingLeft + params.paddingRight;
+ node.height = node.coreBox.height + params.paddingTop + params.paddingBottom;
+}
+
+/**
+ * Calculate layout for annotations of a given node.
+ * This will modify positions of the the given node and its annotations.
+ *
+ * @see tf.graph.render.Node and tf.graph.render.Annotation
+ * for description of each property of each render node.
+ *
+ */
+ function layoutAnnotation(renderNodeInfo: render.RenderNodeInformation): void {
+ // If the render node is an expanded metanode, then its annotations will not
+ // be visible and we should skip the annotation calculations.
+ if (renderNodeInfo.expanded) {
+ _.extend(renderNodeInfo, {
+ inboxWidth: 0,
+ inboxHeight: 0,
+ outboxWidth: 0,
+ outboxHeight: 0,
+ outerWidth: renderNodeInfo.width,
+ outerHeight: renderNodeInfo.height
+ });
+ return;
+ }
+
+ let inAnnotations = renderNodeInfo.inAnnotations.list;
+ let outAnnotations = renderNodeInfo.outAnnotations.list;
+
+ // Calculate size for in-annotations
+ _.each(inAnnotations, a => sizeAnnotation(a));
+
+ // Calculate size for out-annotations
+ _.each(outAnnotations, a => sizeAnnotation(a));
+
+ let params = PARAMS.annotations;
+ renderNodeInfo.inboxWidth =
+ inAnnotations.length > 0 ?
+ (<any>_(inAnnotations).pluck("width").max()) +
+ params.xOffset + params.labelWidth + params.labelOffset :
+ 0;
+
+ renderNodeInfo.outboxWidth =
+ outAnnotations.length > 0 ?
+ (<any>_(outAnnotations).pluck("width").max()) +
+ params.xOffset + params.labelWidth + params.labelOffset :
+ 0;
+
+ // Calculate annotation node position (a.dx, a.dy)
+ // and total height for in-annotations
+ // After this chunk of code:
+ // inboxHeight = sum of annotation heights+ (annotation.length - 1 * yOffset)
+ let inboxHeight = _.reduce(inAnnotations,
+ (height, a: any, i) => {
+ let yOffset = i > 0 ? params.yOffset : 0;
+ a.dx = -(renderNodeInfo.width + a.width) / 2 - params.xOffset;
+ a.dy = height + yOffset + a.height / 2;
+ return height + yOffset + a.height;
+ }, 0);
+
+ _.each(inAnnotations, (a: any) => {
+ a.dy -= inboxHeight / 2;
+
+ a.labelOffset = params.labelOffset;
+ });
+
+ // Calculate annotation node position position (a.dx, a.dy)
+ // and total height for out-annotations
+ // After this chunk of code:
+ // outboxHeight = sum of annotation heights +
+ // (annotation.length - 1 * yOffset)
+ let outboxHeight = _.reduce(outAnnotations,
+ (height, a: any, i) => {
+ let yOffset = i > 0 ? params.yOffset : 0;
+ a.dx = (renderNodeInfo.width + a.width) / 2 + params.xOffset;
+ a.dy = height + yOffset + a.height / 2;
+ return height + yOffset + a.height;
+ }, 0);
+
+ _.each(outAnnotations, (a: any) => {
+ // adjust by (half of ) the total height
+ // so dy is relative to the host node's center.
+ a.dy -= outboxHeight / 2;
+
+ a.labelOffset = params.labelOffset;
+ });
+
+ // Creating scales for touch point between the in-annotation edges
+ // and their hosts.
+
+ let inTouchHeight =
+ Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius,
+ inboxHeight / 2);
+ inTouchHeight = inTouchHeight < 0 ? 0 : inTouchHeight;
+
+ let inY = d3.scale.linear()
+ .domain([0, inAnnotations.length - 1])
+ .range([-inTouchHeight, inTouchHeight]);
+
+ // Calculate annotation edge position
+ _.each(inAnnotations, (a: any, i) => {
+ a.points = [
+ // The annotation node end
+ {
+ dx: a.dx + a.width / 2,
+ dy: a.dy
+ },
+
+ // The host node end
+ {
+ dx: - renderNodeInfo.width / 2,
+ // only use scale if there are more than one,
+ // otherwise center it vertically
+ dy: inAnnotations.length > 1 ? inY(i) : 0
+ }
+ ];
+ });
+
+ // Creating scales for touch point between the out-annotation edges
+ // and their hosts.
+ let outTouchHeight =
+ Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius,
+ outboxHeight / 2);
+ outTouchHeight = outTouchHeight < 0 ? 0 : outTouchHeight;
+ let outY = d3.scale.linear()
+ .domain([0, outAnnotations.length - 1])
+ .range([-outTouchHeight, outTouchHeight]);
+
+ _.each(outAnnotations, (a: any, i) => {
+ // Add point from the border of the annotation node
+ a.points = [
+ // The host node end
+ {
+ dx: renderNodeInfo.width / 2,
+ // only use scale if there are more than one,
+ // otherwise center it vertically
+ dy: outAnnotations.length > 1 ? outY(i) : 0
+ },
+ // The annotation node end
+ {
+ dx: a.dx - a.width / 2,
+ dy: a.dy
+ }
+ ];
+ });
+
+ renderNodeInfo.outerWidth = renderNodeInfo.width + renderNodeInfo.inboxWidth +
+ renderNodeInfo.outboxWidth;
+ renderNodeInfo.outerHeight =
+ Math.max(renderNodeInfo.height, inboxHeight, outboxHeight);
+}
+
+/**
+ * Set size of an annotation node.
+ */
+function sizeAnnotation(a: render.Annotation): void {
+ switch (a.annotationType) {
+ case render.AnnotationType.CONSTANT:
+ _.extend(a, PARAMS.constant.size);
+ break;
+ case render.AnnotationType.SHORTCUT:
+ if (a.node.type === NodeType.OP) {
+ _.extend(a, PARAMS.shortcutSize.op);
+ } else if (a.node.type === NodeType.META) {
+ _.extend(a, PARAMS.shortcutSize.meta);
+ } else if (a.node.type === NodeType.SERIES) {
+ _.extend(a, PARAMS.shortcutSize.series);
+ } else {
+ throw Error("Invalid node type: " + a.node.type);
+ }
+ break;
+ case render.AnnotationType.SUMMARY:
+ _.extend(a, PARAMS.constant.size);
+ break;
+ }
+}
+
+} // close module