aboutsummaryrefslogtreecommitdiffhomepage
path: root/tensorflow/tensorboard/dist/tf-tensorboard.html
diff options
context:
space:
mode:
Diffstat (limited to 'tensorflow/tensorboard/dist/tf-tensorboard.html')
-rw-r--r--tensorflow/tensorboard/dist/tf-tensorboard.html10484
1 files changed, 10484 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/dist/tf-tensorboard.html b/tensorflow/tensorboard/dist/tf-tensorboard.html
new file mode 100644
index 0000000000..2aa1e46ca3
--- /dev/null
+++ b/tensorflow/tensorboard/dist/tf-tensorboard.html
@@ -0,0 +1,10484 @@
+<html><head><meta charset="UTF-8">
+
+
+
+
+
+<style is="custom-style">
+
+ :root {
+ --tb-orange-weak: #fcb938;
+ --tb-orange-strong: #f3913e;
+ --tb-grey-darker: #e2e2e2;
+ --tb-grey-lighter: #f3f3f3;
+ }
+
+</style>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<script>/// <reference path="../../../typings/tsd.d.ts" />
+var tf;
+(function (tf) {
+ /**
+ * Recommended delay (ms) when running an expensive task asynchronously
+ * that gives enough time for the progress bar to update its UI.
+ */
+ var ASYNC_TASK_DELAY = 20;
+ function time(msg, task) {
+ var start = Date.now();
+ var result = task();
+ /* tslint:disable */
+ console.log(msg, ":", Date.now() - start, "ms");
+ /* tslint:enable */
+ return result;
+ }
+ tf.time = time;
+ /**
+ * Creates a tracker for a subtask given the parent tracker, the total progress
+ * of the subtask and the subtask message. The parent task should pass a
+ * subtracker to its subtasks. The subtask reports its own progress which
+ * becames relative to the main task.
+ */
+ function getSubtaskTracker(parentTracker, impactOnTotalProgress, subtaskMsg) {
+ return {
+ setMessage: function (progressMsg) {
+ // The parent should show a concatenation of its message along with
+ // its subtask tracker message.
+ parentTracker.setMessage(subtaskMsg + " : " + progressMsg);
+ },
+ updateProgress: function (incrementValue) {
+ // Update the parent progress relative to the child progress.
+ // For example, if the sub-task progresses by 30%, and the impact on the
+ // total progress is 50%, then the task progresses by 30% * 50% = 15%.
+ parentTracker
+ .updateProgress(incrementValue * impactOnTotalProgress / 100);
+ },
+ reportError: function (errorMsg) {
+ // The parent should show a concatenation of its message along with
+ // its subtask error message.
+ parentTracker.reportError(subtaskMsg + " : " + errorMsg);
+ }
+ };
+ }
+ tf.getSubtaskTracker = getSubtaskTracker;
+ /**
+ * Runs an expensive task asynchronously and returns a promise of the result.
+ */
+ function runAsyncTask(msg, incProgressValue, task, tracker) {
+ return new Promise(function (resolve, reject) {
+ // Update the progress message to say the current running task.
+ tracker.setMessage(msg);
+ // Run the expensive task with a delay that gives enough time for the
+ // UI to update.
+ setTimeout(function () {
+ try {
+ var result = tf.time(msg, task);
+ // Update the progress value.
+ tracker.updateProgress(incProgressValue);
+ // Return the result to be used by other tasks.
+ resolve(result);
+ }
+ catch (e) {
+ reject(result);
+ }
+ }, ASYNC_TASK_DELAY);
+ });
+ }
+ tf.runAsyncTask = runAsyncTask;
+ /**
+ * Returns a query selector with escaped special characters that are not
+ * allowed in a query selector.
+ */
+ function escapeQuerySelector(querySelector) {
+ return querySelector.replace(/([:.\[\],/\\\(\)])/g, "\\$1");
+ }
+ tf.escapeQuerySelector = escapeQuerySelector;
+})(tf || (tf = {})); // close module tf
+</script>
+<script>/// <reference path="../../../typings/tsd.d.ts" />
+/// <reference path="common.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ /** Delimiter used in node names to denote namespaces. */
+ graph_1.NAMESPACE_DELIM = "/";
+ var FULL_GRAPH_NAME = "fullGraph";
+ graph_1.ROOT_NAME = "__root__";
+ // Separator between the source and the destination name of the edge.
+ graph_1.EDGE_KEY_DELIM = "--";
+ (function (GraphType) {
+ GraphType[GraphType["FULL"] = 0] = "FULL";
+ GraphType[GraphType["EMBEDDED"] = 1] = "EMBEDDED";
+ GraphType[GraphType["META"] = 2] = "META";
+ GraphType[GraphType["SERIES"] = 3] = "SERIES";
+ GraphType[GraphType["CORE"] = 4] = "CORE";
+ GraphType[GraphType["SHADOW"] = 5] = "SHADOW";
+ GraphType[GraphType["BRIDGE"] = 6] = "BRIDGE";
+ GraphType[GraphType["EDGE"] = 7] = "EDGE";
+ })(graph_1.GraphType || (graph_1.GraphType = {}));
+ var GraphType = graph_1.GraphType;
+ ;
+ (function (NodeType) {
+ NodeType[NodeType["META"] = 0] = "META";
+ NodeType[NodeType["OP"] = 1] = "OP";
+ NodeType[NodeType["SERIES"] = 2] = "SERIES";
+ NodeType[NodeType["BRIDGE"] = 3] = "BRIDGE";
+ NodeType[NodeType["ELLIPSIS"] = 4] = "ELLIPSIS";
+ })(graph_1.NodeType || (graph_1.NodeType = {}));
+ var NodeType = graph_1.NodeType;
+ ;
+ /**
+ * A SlimGraph is inspired by graphlib.Graph, but having only the functionality
+ * that we need.
+ */
+ var SlimGraph = (function () {
+ function SlimGraph() {
+ this.nodes = {};
+ this.edges = [];
+ }
+ return SlimGraph;
+ })();
+ graph_1.SlimGraph = SlimGraph;
+ var EllipsisNodeImpl = (function () {
+ /**
+ * Constructs a new ellipsis annotation node.
+ *
+ * @param numNodes The number of additional annotations this node represents.
+ */
+ function EllipsisNodeImpl(numNodes) {
+ this.type = NodeType.ELLIPSIS;
+ this.isGroupNode = false;
+ this.cardinality = 1;
+ this.parentNode = null;
+ this.stats = null;
+ this.setNumMoreNodes(numNodes);
+ }
+ EllipsisNodeImpl.prototype.setNumMoreNodes = function (numNodes) {
+ this.numMoreNodes = numNodes;
+ this.name = "... " + numNodes + " more";
+ };
+ return EllipsisNodeImpl;
+ })();
+ graph_1.EllipsisNodeImpl = EllipsisNodeImpl;
+ ;
+ /**
+ * A label object for nodes in the full graph and leaf nodes in the render
+ * graph.
+ */
+ var OpNodeImpl = (function () {
+ /**
+ * Constructs a new Op node.
+ *
+ * @param rawNode The raw node.
+ * @param normalizedInputs An array of normalized
+ * inputs that denote the incoming edges to the current node. Each input
+ * contains the normalized name of the source node, whether it has a number
+ * part and whether it is a control dependency.
+ */
+ function OpNodeImpl(rawNode, normalizedInputs) {
+ this.op = rawNode.op;
+ this.name = rawNode.name;
+ this.device = rawNode.device;
+ this.attr = rawNode.attr;
+ this.inputs = normalizedInputs;
+ // additional properties
+ this.type = NodeType.OP;
+ this.isGroupNode = false;
+ this.cardinality = 1;
+ this.inEmbeddings = [];
+ this.outEmbeddings = [];
+ this.parentNode = null;
+ }
+ return OpNodeImpl;
+ })();
+ ;
+ function createMetanode(name, opt) {
+ if (opt === void 0) { opt = {}; }
+ return new MetanodeImpl(name, opt);
+ }
+ graph_1.createMetanode = createMetanode;
+ /**
+ * Joins the information from the stats file (memory, compute time) with the graph
+ * information.
+ */
+ function joinStatsInfoWithGraph(graph, statsJson) {
+ _.each(statsJson.devStats, function (stats) {
+ _.each(stats.nodeStats, function (nodeStats) {
+ // Lookup the node in the graph by its original name, e.g. A. If not
+ // found, lookup by the rewritten name A/(A) in case the name is both
+ // a namespace and a node name.
+ var nodeName = nodeStats.nodeName in graph.nodes ?
+ nodeStats.nodeName :
+ nodeStats.nodeName + graph_1.NAMESPACE_DELIM + "(" + nodeStats.nodeName + ")";
+ if (nodeName in graph.nodes) {
+ // Compute the total bytes used.
+ var totalBytes = 0;
+ if (nodeStats.memory) {
+ _.each(nodeStats.memory, function (alloc) {
+ if (alloc.totalBytes) {
+ totalBytes += Number(alloc.totalBytes);
+ }
+ });
+ }
+ var outputSize = null;
+ if (nodeStats.output) {
+ outputSize = _.map(nodeStats.output, function (output) {
+ return _.map(output.tensorDescription.shape.dim, function (dim) { return Number(dim.size); });
+ });
+ }
+ graph.nodes[nodeName].stats = new NodeStats(totalBytes, Number(nodeStats.allEndRelMicros), outputSize);
+ }
+ });
+ });
+ }
+ graph_1.joinStatsInfoWithGraph = joinStatsInfoWithGraph;
+ /**
+ * Execution stats for the node.
+ */
+ var NodeStats = (function () {
+ function NodeStats(totalBytes, totalMicros, outputSize) {
+ this.totalBytes = totalBytes;
+ this.totalMicros = totalMicros;
+ this.outputSize = outputSize;
+ }
+ /**
+ * Combines the specified stats with the current stats.
+ * Modifies the current object. This methos is used to
+ * compute aggregate stats for group nodes.
+ */
+ NodeStats.prototype.combine = function (stats) {
+ if (stats.totalBytes != null) {
+ this.totalBytes += stats.totalBytes;
+ }
+ if (stats.totalMicros != null) {
+ this.totalMicros += stats.totalMicros;
+ }
+ };
+ return NodeStats;
+ })();
+ var MetanodeImpl = (function () {
+ /** A label object for meta-nodes in the graph hierarchy */
+ function MetanodeImpl(name, opt) {
+ if (opt === void 0) { opt = {}; }
+ this.name = name;
+ this.type = NodeType.META;
+ /** number of levels under this group */
+ this.depth = 1;
+ this.isGroupNode = true;
+ /** # of leaf nodes (including embedded ones) */
+ this.cardinality = 0;
+ /** graph contains metanodes, nodes, edges
+ * and metaedges for main items within this metanode
+ */
+ this.metagraph =
+ createGraph(name, GraphType.META, opt);
+ /** bridgegraph must be constructed lazily-see hierarchy.getBridgegraph() */
+ this.bridgegraph = null;
+ /**
+ * A dictionary that count ops type of nodes in this metanode
+ * (op type => count).
+ */
+ this.opHistogram = {};
+ this.deviceHistogram = {};
+ /** unique id for a metanode of similar subgraph */
+ this.templateId = null;
+ /** Metanode which contains this node, if any */
+ this.parentNode = null;
+ this.stats = new NodeStats(0, 0, null);
+ this.hasNonControlEdges = false;
+ }
+ MetanodeImpl.prototype.getFirstChild = function () {
+ return this.metagraph.node(this.metagraph.nodes()[0]);
+ };
+ /**
+ * Returns the op node associated with the metanode.
+ * For example, if the metanode is "sgd", the associated
+ * op node is sgd/(sgd).
+ */
+ MetanodeImpl.prototype.getRootOp = function () {
+ var nameSplit = this.name.split("/");
+ var rootOpName = this.name + "/(" + nameSplit[nameSplit.length - 1] + ")";
+ return this.metagraph.node(rootOpName);
+ };
+ /**
+ * Return an array of the names of all the leaves (non-GroupNodes) inside
+ * this metanode. This performs a breadth-first search of the tree, so
+ * immediate child leaves will appear earlier in the output array than
+ * descendant leaves.
+ */
+ MetanodeImpl.prototype.leaves = function () {
+ var leaves = [];
+ var queue = [this];
+ var metagraph; // Defined here due to a limitation of ES6->5 compilation.
+ while (queue.length) {
+ var node = queue.shift();
+ if (node.isGroupNode) {
+ metagraph = node.metagraph;
+ _.each(metagraph.nodes(), function (name) { return queue.push(metagraph.node(name)); });
+ }
+ else {
+ leaves.push(node.name);
+ }
+ }
+ return leaves;
+ };
+ return MetanodeImpl;
+ })();
+ ;
+ function createMetaedge(v, w) {
+ return new MetaedgeImpl(v, w);
+ }
+ graph_1.createMetaedge = createMetaedge;
+ /**
+ * A label object for edges between metanodes of subgraphs in the render graph.
+ */
+ var MetaedgeImpl = (function () {
+ function MetaedgeImpl(v, w) {
+ this.v = v;
+ this.w = w;
+ this.baseEdgeList = [];
+ this.inbound = null;
+ this.numRegularEdges = 0;
+ this.numControlEdges = 0;
+ this.numRefEdges = 0;
+ }
+ MetaedgeImpl.prototype.addBaseEdge = function (edge) {
+ this.baseEdgeList.push(edge);
+ if (edge.isControlDependency) {
+ this.numControlEdges += 1;
+ }
+ else {
+ this.numRegularEdges += 1;
+ }
+ if (edge.isReferenceEdge) {
+ this.numRefEdges += 1;
+ }
+ };
+ return MetaedgeImpl;
+ })();
+ function createSeriesNode(prefix, suffix, parent, clusterId, name) {
+ return new SeriesNodeImpl(prefix, suffix, parent, clusterId, name);
+ }
+ graph_1.createSeriesNode = createSeriesNode;
+ function getSeriesNodeName(prefix, suffix, parent, startId, endId) {
+ var numRepresentation = (typeof startId !== "undefined" && typeof endId !== "undefined") ?
+ "[" + startId + "-" + endId + "]" : "#";
+ var pattern = prefix + numRepresentation + suffix;
+ return (parent ? parent + "/" : "") + pattern;
+ }
+ graph_1.getSeriesNodeName = getSeriesNodeName;
+ var SeriesNodeImpl = (function () {
+ function SeriesNodeImpl(prefix, suffix, parent, clusterId, name) {
+ this.name = name || getSeriesNodeName(prefix, suffix, parent);
+ this.type = NodeType.SERIES;
+ this.hasLoop = false;
+ this.prefix = prefix;
+ this.suffix = suffix;
+ this.clusterId = clusterId;
+ this.ids = [];
+ this.parent = parent;
+ this.isGroupNode = true;
+ this.cardinality = 0;
+ this.metagraph = createGraph(name, GraphType.SERIES);
+ // bridgegraph must be constructed lazily-see hierarchy.getBridgegraph()
+ this.bridgegraph = null;
+ this.parentNode = null;
+ this.deviceHistogram = {};
+ this.hasNonControlEdges = false;
+ this.stats = new NodeStats(0, 0, null);
+ }
+ return SeriesNodeImpl;
+ })();
+ /**
+ * Normalizes the inputs and extracts associated metadata:
+ * 1) Inputs can contain a colon followed by a number at the end
+ * (e.g. inputName:1) and we remove this from the input name, and take note
+ * that the input was numbered.
+ * 2) Control dependency inputs contain caret at the beginning and we
+ * remove this and annotate the edge as a control dependency.
+ * @param inputs Array of unnormalized names of input nodes.
+ */
+ function normalizeInputs(inputs) {
+ return _.reduce(inputs, function (normalizedInputs, inputName) {
+ var start = inputName[0] === "^";
+ var colon = inputName.lastIndexOf(":");
+ var end = colon !== -1 &&
+ inputName.length - colon > 1 &&
+ !(/\D/).test(inputName.substring(colon + 1)) ?
+ colon : inputName.length;
+ var name = inputName.substring(start ? 1 : 0, end);
+ if (normalizedInputs.length === 0 ||
+ name !== normalizedInputs[normalizedInputs.length - 1].name) {
+ normalizedInputs.push({
+ name: name,
+ hasNumberPart: end !== inputName.length,
+ isControlDependency: start
+ });
+ }
+ return normalizedInputs;
+ }, []);
+ }
+ function build(rawNodes, params, tracker) {
+ /**
+ * A dictionary that maps each in-embedding node name to its host node label
+ * object.
+ */
+ var inEmbedding = {};
+ /**
+ * A dictionary that maps each node name to an array of the node's
+ * out-embedding node label objects.
+ */
+ var outEmbeddings = {};
+ var isInEmbeddedPred = getEmbedPredicate(params.inEmbeddingTypes);
+ var isOutEmbeddedPred = getEmbedPredicate(params.outEmbeddingTypes);
+ var embeddingNodeNames = [];
+ /**
+ * A list of all the non-embedding node names which appear in the processed
+ * list of raw nodes. Here we pre-allocate enough room for all the rawNodes,
+ * even though there will some number of embeddings. The excess array length
+ * is spliced off later.
+ *
+ * Experimentation shows that around 30% of the array will go unused, and
+ * even for very large networks that amounts to less than 10k spaces.
+ */
+ var nodeNames = new Array(rawNodes.length);
+ return tf.runAsyncTask("Normalizing names", 30, function () {
+ var opNodes = new Array(rawNodes.length);
+ var index = 0;
+ _.each(rawNodes, function (rawNode) {
+ var normalizedInputs = normalizeInputs(rawNode.input);
+ var opNode = new OpNodeImpl(rawNode, normalizedInputs);
+ if (isInEmbeddedPred(opNode)) {
+ embeddingNodeNames.push(opNode.name);
+ inEmbedding[opNode.name] = opNode;
+ return;
+ }
+ if (isOutEmbeddedPred(opNode)) {
+ embeddingNodeNames.push(opNode.name);
+ _.each(opNode.inputs, function (input) {
+ var inputName = input.name;
+ outEmbeddings[inputName] = outEmbeddings[inputName] || [];
+ outEmbeddings[inputName].push(opNode);
+ });
+ return;
+ }
+ // The node is not an embedding, so add it to the names and nodes lists.
+ opNodes[index] = opNode;
+ nodeNames[index] = opNode.name;
+ index++;
+ });
+ opNodes.splice(index);
+ nodeNames.splice(index);
+ return opNodes;
+ }, tracker)
+ .then(function (opNodes) {
+ // Create the graph data structure from the graphlib library.
+ return tf.runAsyncTask("Building the data structure", 70, function () {
+ var normalizedNameDict = mapStrictHierarchy(nodeNames, embeddingNodeNames);
+ var graph = new SlimGraph;
+ // Add the nodes to the graph.
+ _.each(opNodes, function (opNode) {
+ var normalizedName = normalizedNameDict[opNode.name] || opNode.name;
+ graph.nodes[normalizedName] = opNode;
+ // Check if the node has out-embeddings. If yes, add them to to the
+ // node.
+ if (opNode.name in outEmbeddings) {
+ opNode.outEmbeddings = outEmbeddings[opNode.name];
+ // Normalize the names of the out-embeddings.
+ _.each(opNode.outEmbeddings, function (node) {
+ node.name = normalizedNameDict[node.name] || node.name;
+ });
+ }
+ // Update the name of the node.
+ opNode.name = normalizedName;
+ });
+ // Visit each node's inputs to add the edges to the graph. If the input
+ // is an in-embedding, then add it to the node's in-embeddings instead.
+ _.each(opNodes, function (opNode) {
+ _.each(opNode.inputs, function (input, i) {
+ var inputName = input.name;
+ if (inputName in inEmbedding) {
+ opNode.inEmbeddings.push(inEmbedding[inputName]);
+ }
+ else {
+ graph.edges.push({
+ v: normalizedNameDict[inputName] || inputName,
+ w: opNode.name,
+ isControlDependency: input.isControlDependency,
+ // Check if this op type and input number corresponds to a
+ // reference edge using the refEdges dictionary in the params.
+ isReferenceEdge: (params.refEdges[opNode.op + " " + i] === true)
+ });
+ }
+ });
+ });
+ // Normalize the names of in-embeddings.
+ _.each(inEmbedding, function (node, name) {
+ node.name = normalizedNameDict[node.name] || node.name;
+ });
+ return graph;
+ }, tracker);
+ })
+ .catch(function (reason) {
+ throw new Error("Failure creating graph");
+ });
+ }
+ graph_1.build = build;
+ ;
+ /**
+ * Create a new graphlib.Graph() instance with default parameters
+ */
+ function createGraph(name, type, opt) {
+ if (opt === void 0) { opt = {}; }
+ var graph = new graphlib.Graph(opt);
+ graph.setGraph({
+ name: name,
+ rankdir: "BT",
+ type: type
+ });
+ return graph;
+ }
+ graph_1.createGraph = createGraph;
+ ;
+ /**
+ * Create a predicate for checking whether a node should be embedded based on
+ * the specified types.
+ */
+ function getEmbedPredicate(types) {
+ return function (node) {
+ // check types
+ for (var i = 0; i < types.length; i++) {
+ var regExp = new RegExp(types[i]);
+ if (node.op.match(regExp)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ }
+ ;
+ /**
+ * Returns a strict node name (name => name/(name)) to avoid conflicts
+ * where the node name is also a namespace.
+ */
+ function getStrictName(name) {
+ var parts = name.split(graph_1.NAMESPACE_DELIM);
+ return name + graph_1.NAMESPACE_DELIM + "(" + parts[parts.length - 1] + ")";
+ }
+ /**
+ * For each op node (embedding or non-embedding), rename it if there is a
+ * non-embedding node under its namespace. For example, assume node name "A".
+ * If there is a non-embedding node under its namespace (e.g. "A/B"), "A" will
+ * be renamed to "A/(A)". Then the namespace "A" will contain 2 nodes: "(A)"
+ * and "B". If all the nodes under "A" are embedding nodes (e.g. constant and
+ * summary), keep "A" as an Op node and don't create a namespace.
+ *
+ * @param nodeNames An array of regular (non-embedding) node names.
+ * @param embeddingNodeNames An array of embedding node names.
+ * @return Dictionary object mapping names that need to be renamed to
+ * new names.
+ */
+ function mapStrictHierarchy(nodeNames, embeddingNodeNames) {
+ /** Dictionary that maps the old new to the new name */
+ var newNameDictionary = {};
+ /** Set used to store all namespaces. */
+ var namespaceSet = {};
+ // sort the nodes to make prefix check faster
+ nodeNames.sort();
+ // look for nodes with a prefix a,a/b -> a/(a),a/b
+ for (var i = 0; i < nodeNames.length - 1; ++i) {
+ var a = nodeNames[i];
+ // Get all the parent namespaces of the current node
+ // and add them in the namespace set.
+ _.each(getHierarchicalPath(a).slice(0, -1), function (ns) {
+ namespaceSet[ns] = true;
+ });
+ var b = nodeNames[i + 1];
+ if (_.startsWith(b, a + graph_1.NAMESPACE_DELIM)) {
+ newNameDictionary[a] = getStrictName(a);
+ }
+ }
+ // Go through all the embedding node names and rename them in case they
+ // collide with namespaces.
+ _.each(embeddingNodeNames, function (embeddingName) {
+ if (embeddingName in namespaceSet) {
+ // Rename to follow strict hierarchy.
+ newNameDictionary[embeddingName] = getStrictName(embeddingName);
+ }
+ });
+ return newNameDictionary;
+ }
+ ;
+ /**
+ * Returns a list of the degrees of each node in the graph.
+ */
+ function degreeSequence(graph) {
+ var degrees = graph.nodes().map(function (name) {
+ return graph.neighbors(name).length;
+ });
+ degrees.sort();
+ return degrees;
+ }
+ ;
+ /**
+ * Returns if the degree sequence of the two graphs is the same.
+ */
+ function hasSimilarDegreeSequence(graph1, graph2) {
+ var dg1 = degreeSequence(graph1);
+ var dg2 = degreeSequence(graph2);
+ for (var i = 0; i < dg1.length; i++) {
+ if (dg1[i] !== dg2[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ graph_1.hasSimilarDegreeSequence = hasSimilarDegreeSequence;
+ ;
+ /**
+ * Returns the hierarchical path of the current node, based on the node's name.
+ * For example, if the name is 'a/b/c', the returned path is ['a', 'a/b', 'a/b/c'].
+ */
+ function getHierarchicalPath(name, seriesNames) {
+ var path = [];
+ var i = name.indexOf(graph_1.NAMESPACE_DELIM);
+ // Push all parent portions of the path.
+ while (i >= 0) {
+ path.push(name.substring(0, i));
+ i = name.indexOf(graph_1.NAMESPACE_DELIM, i + 1);
+ }
+ // If the node's path is under a series, then add the series node name to the
+ // hierarchical path as the parent of the leaf.
+ if (seriesNames) {
+ var seriesName = seriesNames[name];
+ if (seriesName) {
+ path.push(seriesName);
+ }
+ }
+ // Push the leaf of the path.
+ path.push(name);
+ return path;
+ }
+ graph_1.getHierarchicalPath = getHierarchicalPath;
+ ;
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module tf.graph
+</script>
+<script>/// <reference path="../../../typings/tsd.d.ts" />
+/// <reference path="common.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph) {
+ var parser;
+ (function (parser) {
+ /**
+ * Parses a native js value, which can be either a string, boolean or number.
+ *
+ * @param value The value to be parsed.
+ */
+ function parseValue(value) {
+ if (value === "true") {
+ return true;
+ }
+ if (value === "false") {
+ return false;
+ }
+ var firstChar = value[0];
+ if (firstChar === "\"") {
+ return value.substring(1, value.length - 1);
+ }
+ var num = parseFloat(value);
+ return isNaN(num) ? value : num;
+ }
+ /**
+ * Fetches a text file and returns a promise of the result.
+ */
+ function readPbTxt(filepath) {
+ return new Promise(function (resolve, reject) {
+ d3.text(filepath, function (error, text) {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve(text);
+ });
+ });
+ }
+ parser.readPbTxt = readPbTxt;
+ /**
+ * Fetches and parses a json file and returns a promise of the result.
+ */
+ function readJson(filepath) {
+ return new Promise(function (resolve, reject) {
+ d3.json(filepath, function (error, text) {
+ if (error) {
+ reject(error);
+ return;
+ }
+ resolve(text);
+ });
+ });
+ }
+ parser.readJson = readJson;
+ /**
+ * Reads the graph and stats file (if available), parses them and returns a
+ * promise of the result.
+ */
+ function readAndParseData(dataset, pbTxtContent, tracker) {
+ var graphPbTxt;
+ var statsJson;
+ return tf.runAsyncTask("Reading graph.pbtxt", 20, function () {
+ return pbTxtContent || readPbTxt(dataset.path);
+ }, tracker)
+ .then(function (text) {
+ graphPbTxt = text;
+ return tf.runAsyncTask("Reading stats.pbtxt", 20, function () {
+ return (dataset != null && dataset.statsPath != null) ?
+ readJson(dataset.statsPath) : null;
+ }, tracker);
+ })
+ .then(function (json) {
+ statsJson = json;
+ return tf.runAsyncTask("Parsing graph.pbtxt", 60, function () {
+ return parsePbtxt(graphPbTxt);
+ }, tracker);
+ })
+ .then(function (nodes) {
+ return {
+ nodes: nodes,
+ statsJson: statsJson
+ };
+ })
+ .catch(function (reason) {
+ throw new Error("Failure parsing graph definition");
+ });
+ }
+ parser.readAndParseData = readAndParseData;
+ /**
+ * Parses a proto txt file into a javascript object.
+ *
+ * @param input The string contents of the proto txt file.
+ * @return The parsed object.
+ */
+ function parsePbtxt(input) {
+ var output = { node: [] };
+ var stack = [];
+ var path = [];
+ var current = output;
+ function splitNameAndValueInAttribute(line) {
+ var colonIndex = line.indexOf(":");
+ var name = line.substring(0, colonIndex).trim();
+ var value = parseValue(line.substring(colonIndex + 2).trim());
+ return {
+ name: name,
+ value: value
+ };
+ }
+ /**
+ * Since proto-txt doesn't explicitly say whether an attribute is repeated
+ * (an array) or not, we keep a hard-coded list of attributes that are known
+ * to be repeated. This list is used in parsing time to convert repeated
+ * attributes into arrays even when the attribute only shows up once in the
+ * object.
+ */
+ var ARRAY_ATTRIBUTES = {
+ "node": true,
+ "node.input": true,
+ "node.attr": true,
+ "node.attr.value.list.type": true,
+ "node.attr.value.shape.dim": true,
+ "node.attr.value.tensor.string_val": true,
+ "node.attr.value.tensor.tensor_shape.dim": true
+ };
+ /**
+ * Adds a value, given the attribute name and the host object. If the
+ * attribute already exists, but is not an array, it will convert it to an
+ * array of values.
+ *
+ * @param obj The host object that holds the attribute.
+ * @param name The attribute name (key).
+ * @param value The attribute value.
+ * @param path A path that identifies the attribute. Used to check if
+ * an attribute is an array or not.
+ */
+ function addAttribute(obj, name, value, path) {
+ // We treat "node" specially since it is done so often.
+ var existingValue = obj[name];
+ if (existingValue == null) {
+ obj[name] = path.join(".") in ARRAY_ATTRIBUTES ? [value] : value;
+ }
+ else if (Array.isArray(existingValue)) {
+ existingValue.push(value);
+ }
+ else {
+ obj[name] = [existingValue, value];
+ }
+ }
+ // Run through the file a line at a time.
+ var startPos = 0;
+ while (startPos < input.length) {
+ var endPos = input.indexOf("\n", startPos);
+ if (endPos === -1) {
+ endPos = input.length;
+ }
+ var line = input.substring(startPos, endPos);
+ startPos = endPos + 1;
+ if (!line) {
+ continue;
+ }
+ switch (line[line.length - 1]) {
+ case "{":
+ var name_1 = line.substring(0, line.length - 2).trim();
+ var newValue = {};
+ stack.push(current);
+ path.push(name_1);
+ addAttribute(current, name_1, newValue, path);
+ current = newValue;
+ break;
+ case "}":
+ current = stack.pop();
+ path.pop();
+ break;
+ default:
+ var x = splitNameAndValueInAttribute(line);
+ addAttribute(current, x.name, x.value, path.concat(x.name));
+ break;
+ }
+ }
+ return output["node"];
+ }
+ parser.parsePbtxt = parsePbtxt;
+ })(parser = graph.parser || (graph.parser = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // Close module tf.graph.parser.
+</script>
+<script>/// <reference path="graph.ts" />
+/// <reference path="template.ts" />
+/**
+ * Package for the Graph Hierarchy for TensorFlow graph.
+ */
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var hierarchy;
+ (function (hierarchy_1) {
+ var LOG_PREFIX_MSG = "Graph hierarchy: ";
+ /**
+ * Class for the Graph Hierarchy for TensorFlow graph.
+ */
+ var HierarchyImpl = (function () {
+ function HierarchyImpl() {
+ this.root = graph_1.createMetanode(graph_1.ROOT_NAME, { compound: true });
+ this.templates = null;
+ this.devices = null;
+ /**
+ * @type {Object} Dictionary object that maps node name to the node
+ * (could be op-node, metanode, or series-node)
+ */
+ this.index = {};
+ this.index[graph_1.ROOT_NAME] = this.root;
+ this.orderings = {};
+ }
+ HierarchyImpl.prototype.getNodeMap = function () {
+ return this.index;
+ };
+ HierarchyImpl.prototype.node = function (name) {
+ return this.index[name];
+ };
+ HierarchyImpl.prototype.setNode = function (name, node) {
+ this.index[name] = node;
+ };
+ /**
+ * Given the name of a node in this hierarchy, get its bridgegraph, creating
+ * it on the fly if necessary. If the node is not a GroupNode, then this
+ * method returns null. If the provided name does not map to a node in the
+ * hierarchy, an error will be thrown.
+ */
+ HierarchyImpl.prototype.getBridgegraph = function (nodeName) {
+ var _this = this;
+ var node = this.index[nodeName];
+ if (!node) {
+ throw Error("Could not find node in hierarchy: " + nodeName);
+ }
+ if (!("metagraph" in node)) {
+ return null;
+ }
+ var groupNode = node;
+ if (groupNode.bridgegraph) {
+ return groupNode.bridgegraph;
+ }
+ var bridgegraph = groupNode.bridgegraph =
+ graph_1.createGraph("BRIDGEGRAPH", graph_1.GraphType.BRIDGE);
+ if (!node.parentNode || !("metagraph" in node.parentNode)) {
+ return bridgegraph;
+ }
+ var parentNode = node.parentNode;
+ var parentMetagraph = parentNode.metagraph;
+ var parentBridgegraph = this.getBridgegraph(parentNode.name);
+ // For each of the parent node's two Metaedge containing graphs, process
+ // each Metaedge involving this node.
+ _.each([parentMetagraph, parentBridgegraph], function (parentGraph) {
+ _(parentGraph.edges())
+ .filter(function (e) { return e.v === nodeName || e.w === nodeName; })
+ .each(function (parentEdgeObj) {
+ var inbound = parentEdgeObj.w === nodeName;
+ var parentMetaedge = parentGraph.edge(parentEdgeObj);
+ // The parent's Metaedge represents some number of underlying
+ // BaseEdges from the original full graph. For each of those, we need
+ // to determine which immediate child is involved and make sure
+ // there's a Metaedge in the bridgegraph that covers it.
+ _.each(parentMetaedge.baseEdgeList, function (baseEdge) {
+ // Based on the direction, figure out which is the descendant node
+ // and which is the "other" node (sibling of parent or ancestor).
+ var _a = inbound ?
+ [baseEdge.w, parentEdgeObj.v] :
+ [baseEdge.v, parentEdgeObj.w], descendantName = _a[0], otherName = _a[1];
+ // Determine the immediate child containing this descendant node.
+ var childName = _this.getChildName(nodeName, descendantName);
+ // Look for an existing Metaedge in the bridgegraph (or create a
+ // new one) that covers the relationship between child and other.
+ var bridgeEdgeObj = {
+ v: inbound ? otherName : childName,
+ w: inbound ? childName : otherName,
+ };
+ var bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj);
+ if (!bridgeMetaedge) {
+ bridgeMetaedge = graph_1.createMetaedge(bridgeEdgeObj.v, bridgeEdgeObj.w);
+ bridgeMetaedge.inbound = inbound;
+ bridgegraph.setEdge(bridgeEdgeObj.v, bridgeEdgeObj.w, bridgeMetaedge);
+ }
+ // Copy the BaseEdge from the parent's Metaedge into this
+ // bridgegraph Metaedge.
+ bridgeMetaedge.addBaseEdge(baseEdge);
+ });
+ })
+ .value(); // force lodash chain execution.
+ });
+ return bridgegraph;
+ };
+ /**
+ * Utility function for determining the name of the immediate child under a
+ * node for a given descendant path. If the descendant corresponds to no
+ * immediate child, an error is thrown.
+ */
+ HierarchyImpl.prototype.getChildName = function (nodeName, descendantName) {
+ // Walk up the hierarchy from the descendant to find the child.
+ var currentNode = this.index[descendantName];
+ while (currentNode) {
+ if (currentNode.parentNode && currentNode.parentNode.name === nodeName) {
+ return currentNode.name;
+ }
+ currentNode = currentNode.parentNode;
+ }
+ throw Error("Could not find immediate child for descendant: " +
+ descendantName);
+ };
+ ;
+ /**
+ * Given the name of a node, return the names of its predecssors.
+ * For an OpNode, this will contain the targets from the underlying BaseEdges.
+ * For a GroupNode, this will contain the targets truncated to siblings of
+ * the shared ancestor.
+ *
+ * For example, consider an original non-control BaseEdge A/B/C->Z/Y/X. Their
+ * shared ancestor is the ROOT node. A and Z are the highest siblings. Here
+ * are the results of calling getPredecessors():
+ *
+ * - getPredecessors("Z/Y/X") === {regular: ["A/B/C"], control: []};
+ * - getPredecessors("Z/Y") === {regular: ["A"], control: []};
+ * - getPredecessors("Z") === {regular: ["A"], control: []};
+ *
+ * The reason getPredecessors("Z/Y") returns ["A"] (and not ["A/B"] as you
+ * might intuitively expect) is because it's not clear how far down the
+ * other end of the hierarchy to traverse in the general case.
+ *
+ * Continuing this example, say there was another BaseEdge A/K->Z/Y/W. When
+ * we look at Z/Y's predecessors, the best we can say is ["A"] without getting
+ * into the details of which of of Z/Y's descendant nodes have predecessors to
+ * which of A's descendants.
+ *
+ * On the other hand, for an OpNode it's clear what the final predecssors
+ * ought to be. There is no ambiguity.
+ */
+ HierarchyImpl.prototype.getPredecessors = function (nodeName) {
+ var node = this.index[nodeName];
+ if (!node) {
+ throw Error("Could not find node with name: " + nodeName);
+ }
+ var predecessors = this.getOneWayEdges(node, true);
+ // Add embedded predecessors, such as constants.
+ if (!node.isGroupNode) {
+ _.each(node.inEmbeddings, function (embeddedNode) {
+ predecessors.regular.push(embeddedNode.name);
+ });
+ }
+ return predecessors;
+ };
+ /**
+ * Given the name of a node, return an array of the names of its successors.
+ * For an OpNode, this will contain the targets from the underlying BaseEdges.
+ * For a GroupNode, this will contain the targets truncated to sibling of
+ * the shared ancestor.
+ *
+ * This is the inverse of getPredecessors(). See that method's documentation
+ * for an in-depth example.
+ */
+ HierarchyImpl.prototype.getSuccessors = function (nodeName) {
+ var node = this.index[nodeName];
+ if (!node) {
+ throw Error("Could not find node with name: " + nodeName);
+ }
+ var successors = this.getOneWayEdges(node, false);
+ // Add embedded successors, such as summaries.
+ if (!node.isGroupNode) {
+ _.each(node.outEmbeddings, function (embeddedNode) {
+ successors.regular.push(embeddedNode.name);
+ });
+ }
+ return successors;
+ };
+ /** Helper method for getPredeccessors and getSuccessors */
+ HierarchyImpl.prototype.getOneWayEdges = function (node, inEdges) {
+ var edges = { control: [], regular: [] };
+ // A node with no parent cannot have any edges.
+ if (!node.parentNode) {
+ return edges;
+ }
+ if (node.parentNode.isGroupNode) {
+ var parentNode = node.parentNode;
+ var metagraph = parentNode.metagraph;
+ var bridgegraph = this.getBridgegraph(parentNode.name);
+ findEdgeTargetsInGraph(metagraph, node, inEdges, edges);
+ findEdgeTargetsInGraph(bridgegraph, node, inEdges, edges);
+ }
+ return edges;
+ };
+ /**
+ * For a given GroupNode, get or calculate an object which describes a
+ * topological ordering of child nodes within that GroupNode's metagraph.
+ *
+ * This ordering is used when rendering bridge control edges which are
+ * sometimes backwards relative to the dataflow.
+ *
+ * For example, say we have a graph with two edges A->B and A->C, and we're
+ * interested in the ordering under ROOT. In this case, any of the following
+ * would be legitimate return values:
+ *
+ * - { "A": 0, "B": 1, "C": 2 } -- most likely
+ * - { "A": 0, "B": 2, "C": 1 } -- less likely
+ * - { "A": 12, "B": 100, "C": 99 } -- unlikely, but still OK
+ *
+ * The algorithm does not guarantee that all numbers from 0-N (where N is
+ * the number of nodes) appear exactly once. Rather it guarantees that if
+ * there is a path between two nodes, the earlier one will have a lower
+ * number in the ordering hash.
+ *
+ * When generating the ordering, we ignore control Metaedges (those which
+ * represent only BaseEdges that have isControlDependency set to true).
+ *
+ * If there is no node with the specified name, an error is thrown. If the
+ * node with the specified name is not a group node, null is returned.
+ */
+ HierarchyImpl.prototype.getTopologicalOrdering = function (nodeName) {
+ var node = this.index[nodeName];
+ if (!node) {
+ throw Error("Could not find node with name: " + nodeName);
+ }
+ if (!node.isGroupNode) {
+ return null;
+ }
+ if (nodeName in this.orderings) {
+ return this.orderings[nodeName];
+ }
+ // Mapping of a child node names to lists of their successors.
+ var successors = {};
+ // Set of node names which have appeared as a destination.
+ var destinations = {};
+ var metagraph = node.metagraph;
+ _.each(metagraph.edges(), function (e) {
+ if (!metagraph.edge(e).numRegularEdges) {
+ return; // Skip control edges.
+ }
+ // Keep track of successors and destinations.
+ if (!(e.v in successors)) {
+ successors[e.v] = [];
+ }
+ successors[e.v].push(e.w);
+ destinations[e.w] = true;
+ });
+ // Seed the queue with true sources (those that are not destinations).
+ var queue = _.difference(_.keys(successors), _.keys(destinations));
+ // Produce an ordering by traversing the graph breadth first.
+ var ordering = this.orderings[nodeName] = {};
+ var index = 0;
+ while (queue.length) {
+ var childName = queue.shift();
+ ordering[childName] = index++;
+ _.each(successors[childName], function (succName) { return queue.push(succName); });
+ delete successors[childName]; // Prevent cycles from infinite looping.
+ }
+ return ordering;
+ };
+ return HierarchyImpl;
+ })();
+ /**
+ * Internal utility function - given a graph (should be either a metagraph or a
+ * bridgegraph) and a node which is known to be in that graph, determine
+ * the other ends of edges that involve that node in the direction specified
+ * by whether it's inbound.
+ *
+ * For example if you wanted to find the predecessors of a node, you'd call
+ * this method for the parent's metagraph and bridgegraph, specifying inbound
+ * as true (look at the source of inbound edges to the specified node).
+ *
+ * Discovered target names are appended to the targets array.
+ */
+ function findEdgeTargetsInGraph(graph, node, inbound, targets) {
+ _.each(graph.edges(), function (e) {
+ var _a = inbound ? [e.w, e.v] : [e.v, e.w], selfName = _a[0], otherName = _a[1];
+ if (selfName === node.name) {
+ if (node.isGroupNode) {
+ var targetList = graph.edge(e).numRegularEdges
+ ? targets.regular : targets.control;
+ targetList.push(otherName);
+ }
+ else {
+ _.each(graph.edge(e).baseEdgeList, function (baseEdge) {
+ var targetList = baseEdge.isControlDependency
+ ? targets.control : targets.regular;
+ targetList.push(inbound ? baseEdge.v : baseEdge.w);
+ });
+ }
+ }
+ });
+ }
+ /**
+ * @param graph The raw graph.
+ * @param params Parameters used when building a hierarchy.
+ */
+ function build(graph, params, tracker) {
+ var h = new HierarchyImpl();
+ var seriesNames = {};
+ return tf.runAsyncTask("Adding nodes", 20, function () {
+ // Get all the possible device names.
+ var deviceNames = {};
+ _.each(graph.nodes, function (node, nodeName) {
+ if (node.device != null) {
+ deviceNames[node.device] = true;
+ }
+ });
+ h.devices = _.keys(deviceNames);
+ addNodes(h, graph);
+ }, tracker)
+ .then(function () {
+ return tf.runAsyncTask("Detect series", 20, function () {
+ if (params.groupSeries) {
+ groupSeries(h.root, h, seriesNames);
+ }
+ }, tracker);
+ })
+ .then(function () {
+ return tf.runAsyncTask("Adding edges", 30, function () {
+ addEdges(h, graph, seriesNames);
+ }, tracker);
+ })
+ .then(function () {
+ return tf.runAsyncTask("Finding similar subgraphs", 30, function () {
+ h.templates = graph_1.template.detect(h, params.verifyTemplate);
+ }, tracker);
+ })
+ .then(function () {
+ return h;
+ }).catch(function (reason) {
+ throw new Error("Failure creating graph hierarchy");
+ });
+ }
+ hierarchy_1.build = build;
+ ;
+ /**
+ * Creates the metanodes in the hierarchical graph and assigns parent-child
+ * relationship between them.
+ */
+ function addNodes(h, graph) {
+ _.each(graph.nodes, function (node, nodeName) {
+ var path = graph_1.getHierarchicalPath(node.name);
+ var parent = h.root;
+ parent.depth = Math.max(path.length, parent.depth);
+ // Create parent metanodes for each depth. For example if the node name
+ // is 'a/b/c', then create metanodes 'a' and 'a/b', where 'a/b' is a child
+ // of a.
+ for (var i = 0; i < path.length; i++) {
+ parent.depth = Math.max(parent.depth, path.length - i);
+ parent.cardinality += node.cardinality;
+ parent.opHistogram[node.op] = (parent.opHistogram[node.op] || 0) + 1;
+ if (node.stats) {
+ parent.stats.combine(node.stats);
+ }
+ if (node.device != null) {
+ parent.deviceHistogram[node.device] =
+ (parent.deviceHistogram[node.device] || 0) + 1;
+ }
+ if (i === path.length - 1) {
+ break;
+ }
+ var name_1 = path[i];
+ var child = h.node(name_1);
+ if (!child) {
+ child = graph_1.createMetanode(name_1);
+ child.parentNode = parent;
+ h.setNode(name_1, child);
+ parent.metagraph.setNode(name_1, child);
+ }
+ parent = child;
+ }
+ // Assuming node name is 'a/b/c', assign the OpNode as a child of the metanode 'a/b'.
+ h.setNode(node.name, node);
+ node.parentNode = parent;
+ parent.metagraph.setNode(node.name, node);
+ // Add each of the in-embeddings and out-embeddings in the hierarchy.
+ _.each(node.inEmbeddings, function (embedding) {
+ h.setNode(embedding.name, embedding);
+ embedding.parentNode = node;
+ });
+ _.each(node.outEmbeddings, function (embedding) {
+ h.setNode(embedding.name, embedding);
+ embedding.parentNode = node;
+ });
+ });
+ }
+ ;
+ /**
+ * For each metanode in the hierarchical graph, this method adds:
+ * the edges in the metagraph. These are edges between nodes
+ * that share the same parent.
+ */
+ function addEdges(h, graph, seriesNames) {
+ var nodeIndex = h.getNodeMap();
+ // Ancestor paths for the source and destination nodes of an edge. These are
+ // reused for each edge rather than allocating new ones. It's about 10% faster
+ // than allocating new ones on each pass through the loop.
+ var sourcePath = [];
+ var destPath = [];
+ // Insert the ancestor path for a node into the provided array, including the
+ // node itself. Return the index of the last node inserted (always ROOT).
+ var getPath = function (node, path) {
+ var i = 0;
+ while (node) {
+ path[i++] = node.name;
+ node = node.parentNode;
+ }
+ return i - 1;
+ };
+ _.each(graph.edges, function (baseEdge) {
+ // Get the hierarchical paths for the source and destination of the edge.
+ var sourceAncestorIndex = getPath(graph.nodes[baseEdge.v], sourcePath);
+ var destAncestorIndex = getPath(graph.nodes[baseEdge.w], destPath);
+ // Find the lowest shared ancestor between source and dest by looking for
+ // the highest nodes that differ between their ancestor paths.
+ while (sourcePath[sourceAncestorIndex] === destPath[destAncestorIndex]) {
+ sourceAncestorIndex--;
+ destAncestorIndex--;
+ if (sourceAncestorIndex < 0 || destAncestorIndex < 0) {
+ // This would only occur if the two nodes were the same (a cycle in the
+ // graph), or if one endpoint was a strict ancestor of the other. The
+ // latter shouldn't happen because we rename nodes which are both
+ // metanodes and op nodes. E.g. "A/B" becomes "A/B/(B)".
+ throw Error("No difference found between ancestor paths.");
+ }
+ }
+ var sharedAncestorNode = nodeIndex[sourcePath[sourceAncestorIndex + 1]];
+ var sourceAncestorName = sourcePath[sourceAncestorIndex];
+ var destAncestorName = destPath[destAncestorIndex];
+ // Find or create the Metaedge which should contain this BaseEdge inside
+ // the shared ancestor.
+ var metaedge = sharedAncestorNode.metagraph.edge(sourceAncestorName, destAncestorName);
+ if (!metaedge) {
+ metaedge = graph_1.createMetaedge(sourceAncestorName, destAncestorName);
+ sharedAncestorNode.metagraph
+ .setEdge(sourceAncestorName, destAncestorName, metaedge);
+ }
+ if (!sharedAncestorNode.hasNonControlEdges &&
+ !baseEdge.isControlDependency) {
+ sharedAncestorNode.hasNonControlEdges = true;
+ }
+ metaedge.addBaseEdge(baseEdge);
+ });
+ }
+ ;
+ /**
+ * Using the hierarchy template information, detect series in the provided
+ * metanode. For each detected series, create a new SeriesNode
+ * and remove series members from the metanode's metagraph and move them to
+ * the new series node's metagraph.
+ *
+ * @param metanode
+ * @param hierarchy
+ * @return A dictionary from node name to series node name that contains the node
+ */
+ function groupSeries(metanode, hierarchy, seriesNames) {
+ var metagraph = metanode.metagraph;
+ _.each(metagraph.nodes(), function (n) {
+ var child = metagraph.node(n);
+ if (child.type === tf.graph.NodeType.META) {
+ groupSeries(child, hierarchy, seriesNames);
+ }
+ });
+ var clusters = clusterNodes(metagraph);
+ var seriesDict = detectSeries(clusters, metagraph);
+ // Add each series node to the graph and add its grouped children to its own
+ // metagraph.
+ _.each(seriesDict, function (seriesNode, seriesName) {
+ var nodeMemberNames = seriesNode.metagraph.nodes();
+ var firstMember = seriesNode.metagraph.node(nodeMemberNames[0]);
+ var seriesType = firstMember.type;
+ hierarchy.setNode(seriesName, seriesNode); // add to the index
+ metagraph.setNode(seriesName, seriesNode);
+ _.each(nodeMemberNames, function (n) {
+ var child = metagraph.node(n);
+ seriesNode.metagraph.setNode(n, child);
+ seriesNode.parentNode = child.parentNode;
+ seriesNode.cardinality++;
+ if (child.device != null) {
+ seriesNode.deviceHistogram[child.device] =
+ (seriesNode.deviceHistogram[child.device] || 0) + 1;
+ }
+ child.parentNode = seriesNode;
+ seriesNames[n] = seriesName;
+ if (child.stats) {
+ seriesNode.stats.combine(child.stats);
+ }
+ // Remove now-grouped node from its original parent's metagraph.
+ metagraph.removeNode(n);
+ });
+ });
+ }
+ ;
+ /** cluster op-nodes with similar op */
+ function clusterNodes(metagraph) {
+ var result = {};
+ return _.reduce(metagraph.nodes(), function (clusters, n) {
+ var child = metagraph.node(n);
+ if (child.type === graph_1.NodeType.META) {
+ // skip metanodes
+ return clusters;
+ }
+ var template = child.op;
+ if (template) {
+ clusters[template] = clusters[template] || [];
+ clusters[template].push(child.name);
+ }
+ return clusters;
+ }, result);
+ }
+ /**
+ * For each cluster of op-nodes based op type, try to detect groupings.
+ * Infer series name using by trying to find pattern "<number>" in the node
+ * name.
+ *
+ * @param clusters Dictionary output from clusterNodes().
+ * @param metagraph
+ * @return A dictionary from series name => seriesNode
+ */
+ function detectSeries(clusters, metagraph) {
+ var seriesDict = {};
+ _.each(clusters, function (members, clusterId) {
+ if (members.length <= 1) {
+ return;
+ } // isolated clusters can't make series
+ /** @type {Object} A dictionary mapping seriesName to seriesInfoArray,
+ * which is an array that contains objects with name, id, prefix, suffix,
+ * and parent properties.
+ */
+ var candidatesDict = {};
+ // Group all nodes that have the same name, with the exception of a
+ // number at the end of the name after an underscore, which is allowed to
+ // vary.
+ _.each(members, function (name) {
+ var isGroup = name.charAt(name.length - 1) === "*";
+ var namepath = name.split("/");
+ var leaf = namepath[namepath.length - 1];
+ var parent = namepath.slice(0, namepath.length - 1).join("/");
+ var matches = leaf.match(/^(\D*)_(\d+)$/);
+ var prefix;
+ var id;
+ var suffix = "";
+ if (matches) {
+ prefix = matches[1]; // the front non-numeric characters
+ id = matches[2]; // the digits
+ }
+ else {
+ prefix = isGroup ? leaf.substr(0, leaf.length - 1) : leaf;
+ if (prefix.charAt(prefix.length - 1) !== "_") {
+ prefix += "_";
+ }
+ id = 0;
+ suffix = isGroup ? "*" : "";
+ }
+ var seriesName = graph_1.getSeriesNodeName(prefix, suffix, parent);
+ candidatesDict[seriesName] = candidatesDict[seriesName] || [];
+ var seriesNode = graph_1.createSeriesNode(prefix, suffix, parent, +id, name);
+ candidatesDict[seriesName].push(seriesNode);
+ });
+ // In each group of nodes, group nodes in bunches that have monotonically
+ // increasing numbers in their names. Each of these bunches is a series.
+ _.each(candidatesDict, function (seriesInfoArray, seriesName) {
+ if (seriesInfoArray.length < 2) {
+ return;
+ }
+ seriesInfoArray.sort(function (a, b) {
+ return (+a.clusterId) - (+b.clusterId);
+ });
+ // Loop through the nodes sorted by its detected series number, grouping
+ // all nodes with monotonically-increasing series numbers.
+ var seriesNodes = [seriesInfoArray[0]];
+ for (var index = 1; index < seriesInfoArray.length; index++) {
+ var nextNode = seriesInfoArray[index];
+ if (nextNode.clusterId === seriesNodes[seriesNodes.length - 1].clusterId + 1) {
+ seriesNodes.push(nextNode);
+ continue;
+ }
+ addSeriesToDict(seriesNodes, seriesDict, +clusterId, metagraph);
+ seriesNodes = [nextNode];
+ }
+ addSeriesToDict(seriesNodes, seriesDict, +clusterId, metagraph);
+ });
+ });
+ return seriesDict;
+ }
+ /**
+ * Add a series to the provided dictionary mapping series names to series.
+ *
+ * @param seriesNodes the nodes in the series. Contains
+ * name, id, prefix, suffix and parent properties of the node.
+ * @param seriesDict the dictionary of series
+ * @param clusterId ID of the template of the nodes of the series
+ * @param metagraph
+ */
+ function addSeriesToDict(seriesNodes, seriesDict, clusterId, metagraph) {
+ if (seriesNodes.length > 1) {
+ var curSeriesName = graph_1.getSeriesNodeName(seriesNodes[0].prefix, seriesNodes[0].suffix, seriesNodes[0].parent, seriesNodes[0].clusterId, seriesNodes[seriesNodes.length - 1].clusterId);
+ var curSeriesNode = graph_1.createSeriesNode(seriesNodes[0].prefix, seriesNodes[0].suffix, seriesNodes[0].parent, clusterId, curSeriesName);
+ _.each(seriesNodes, function (node) {
+ curSeriesNode.ids.push(node.clusterId);
+ curSeriesNode.metagraph.setNode(node.name, metagraph.node(node.name));
+ });
+ seriesDict[curSeriesName] = curSeriesNode;
+ }
+ }
+ })(hierarchy = graph_1.hierarchy || (graph_1.hierarchy = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module tf.graph.hierarchy
+</script>
+<script>/// <reference path="graph.ts" />
+/// <reference path="hierarchy.ts" />
+var __extends = (this && this.__extends) || function (d, b) {
+ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ __.prototype = b.prototype;
+ d.prototype = new __();
+};
+/**
+ * Package for the Render Hierarchy for TensorFlow graph.
+ */
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var render;
+ (function (render) {
+ /**
+ * Color parameters for node encoding.
+ * @type {Object}
+ */
+ render.MetanodeColors = {
+ SATURATION: 0.6,
+ LIGHTNESS: 0.85,
+ /**
+ * Neutral color to use when the node is expanded (used when coloring by
+ * compute time, memory and device).
+ */
+ EXPANDED_COLOR: "#f0f0f0",
+ /**
+ * Standard hue values for node color palette.
+ */
+ HUES: [220, 100, 180, 40, 20, 340, 260, 300, 140, 60],
+ STRUCTURE_PALETTE: function (id, lightened) {
+ // The code below is a flexible way to computationally create a set
+ // of colors that go well together.
+ var hues = render.MetanodeColors.HUES;
+ var n = hues.length;
+ var hue = hues[id % n];
+ var m = Math.sin(hue * Math.PI / 360);
+ var sat = lightened ? 30 : 90 - 60 * m;
+ var light = lightened ? 95 : 80;
+ return d3.hsl(hue, .01 * sat, .01 * light).toString();
+ },
+ DEVICE_PALETTE: function (index) {
+ return render.MetanodeColors.STRUCTURE_PALETTE(index);
+ },
+ UNKNOWN: "#eee",
+ GRADIENT_OUTLINE: "#888"
+ };
+ /**
+ * Stores the rendering information, such as x and y coordinates,
+ * for each node in the graph.
+ */
+ var RenderGraphInformation = (function () {
+ function RenderGraphInformation(hierarchy, params) {
+ this.hierarchy = hierarchy;
+ this.index = {};
+ this.deviceColorMap = d3.scale.ordinal()
+ .domain(hierarchy.devices)
+ .range(_.map(d3.range(hierarchy.devices.length), render.MetanodeColors.DEVICE_PALETTE));
+ var topLevelGraph = hierarchy.root.metagraph;
+ // Find the maximum and minimum memory usage.
+ var memoryExtent = d3.extent(topLevelGraph.nodes(), function (nodeName, index) {
+ var node = topLevelGraph.node(nodeName);
+ // Some ops don't have stats at all.
+ if (node.stats != null) {
+ return node.stats.totalBytes;
+ }
+ });
+ this.memoryUsageScale = d3.scale.linear()
+ .domain(memoryExtent)
+ .range(params.minMaxColors);
+ // Find also the minimum and maximum compute time.
+ var computeTimeExtent = d3.extent(topLevelGraph.nodes(), function (nodeName, index) {
+ var node = topLevelGraph.node(nodeName);
+ // Some ops don't have stats at all.
+ if (node.stats != null) {
+ return node.stats.totalMicros;
+ }
+ });
+ this.computeTimeScale = d3.scale.linear()
+ .domain(computeTimeExtent)
+ .range(params.minMaxColors);
+ // Maps node name to whether the rendering hierarchy was already constructed.
+ this.hasSubhierarchy = {};
+ this.params = params;
+ this.root = new RenderGroupNodeInformation(hierarchy.root);
+ this.index[hierarchy.root.name] = this.root;
+ this.buildSubhierarchy(hierarchy.root.name);
+ this.root.expanded = true;
+ }
+ RenderGraphInformation.prototype.getRenderNodeByName = function (nodeName) {
+ return this.index[nodeName];
+ };
+ /**
+ * Return the nearest ancestor node, including itself, that is visible
+ * in the visualization. This method is used so that we can select
+ * (highlight) a node that isn't drawn yet, by selecting (highlighting)
+ * its nearest ancestor that has been drawn.
+ */
+ RenderGraphInformation.prototype.getNearestVisibleAncestor = function (name) {
+ var path = graph_1.getHierarchicalPath(name);
+ for (var i = 0; i < path.length; i++) {
+ var nodeName = path[i];
+ // Op nodes have expanded set to false by default.
+ if (!this.getRenderNodeByName(nodeName).expanded) {
+ return nodeName;
+ }
+ }
+ // Fallthrough. If everything was expanded return the node.
+ return name;
+ };
+ // TODO(jimbo): Delete this an any code it touches (all deprecated).
+ RenderGraphInformation.prototype.setDepth = function (depth) {
+ setGroupNodeDepth(this.root, +depth);
+ };
+ RenderGraphInformation.prototype.buildSubhierarchy = function (nodeName) {
+ var _this = this;
+ // Terminate if the rendering hierarchy was already constructed
+ // for this node.
+ if (nodeName in this.hasSubhierarchy) {
+ return;
+ }
+ var renderNodeInfo = this.index[nodeName];
+ // If it is not a meta node or a series node, don't do anything.
+ if (renderNodeInfo.node.type !== graph_1.NodeType.META &&
+ renderNodeInfo.node.type !== graph_1.NodeType.SERIES) {
+ return;
+ }
+ // At this point we know the rendering information is about a group node.
+ var renderGroupNodeInfo = renderNodeInfo;
+ var metagraph = renderGroupNodeInfo.node.metagraph;
+ var coreGraph = renderGroupNodeInfo.coreGraph;
+ // Create render nodes to represent each child from the metagraph. Although
+ // these will initially be added to the coreGraph, they may later be
+ // extracted. Also, due to extraction, the coreGraph may contain disjoint
+ // groups between which there is no visible path (other than annotations).
+ _.each(metagraph.nodes(), function (childName) {
+ var childNode = metagraph.node(childName);
+ var childRenderInfo = childNode.isGroupNode ?
+ new RenderGroupNodeInformation(childNode) :
+ new RenderNodeInformation(childNode);
+ _this.index[childName] = childRenderInfo;
+ coreGraph.setNode(childName, childRenderInfo);
+ if (childRenderInfo.node.stats != null) {
+ childRenderInfo.memoryColor =
+ _this.memoryUsageScale(childRenderInfo.node.stats.totalBytes);
+ childRenderInfo.computeTimeColor =
+ _this.computeTimeScale(childRenderInfo.node.stats.totalMicros);
+ }
+ if (!childNode.isGroupNode) {
+ _.each(childNode.inEmbeddings, function (embedding) {
+ var renderMetaedgeInfo = new RenderMetaedgeInformation(null);
+ addInAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, AnnotationType.CONSTANT, _this.params);
+ _this.index[embedding.name] = new RenderNodeInformation(embedding);
+ });
+ _.each(childNode.outEmbeddings, function (embedding) {
+ var renderMetaedgeInfo = new RenderMetaedgeInformation(null);
+ addOutAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, AnnotationType.SUMMARY, _this.params);
+ _this.index[embedding.name] = new RenderNodeInformation(embedding);
+ });
+ var device = childRenderInfo.node.device;
+ if (device != null) {
+ childRenderInfo.deviceColors = [{
+ color: _this.deviceColorMap(device),
+ proportion: 1.0
+ }];
+ }
+ }
+ else {
+ // Make a list of tuples (device, proportion), where proportion
+ // is the fraction of op nodes that have that device.
+ var pairs = _.pairs(childNode.deviceHistogram);
+ if (pairs.length > 0) {
+ // Compute the total # of devices.
+ var numDevices = _.sum(pairs, _.last);
+ childRenderInfo.deviceColors = _.map(pairs, function (pair) {
+ return {
+ color: _this.deviceColorMap(pair[0]),
+ // Normalize to a proportion of total # of devices.
+ proportion: pair[1] / numDevices
+ };
+ });
+ }
+ }
+ });
+ // Add render metaedge info for edges in the metagraph.
+ _.each(metagraph.edges(), function (edgeObj) {
+ var metaedge = metagraph.edge(edgeObj);
+ var renderMetaedgeInfo = new RenderMetaedgeInformation(metaedge);
+ coreGraph.setEdge(edgeObj.v, edgeObj.w, renderMetaedgeInfo);
+ });
+ if (this.params.enableExtraction &&
+ renderGroupNodeInfo.node.type === graph_1.NodeType.META) {
+ extractHighDegrees(renderGroupNodeInfo, this.params);
+ }
+ // Record that we constructed the rendering hierarchy for this node, so we
+ // don't construct it another time.
+ this.hasSubhierarchy[nodeName] = true;
+ // Look up the parent node's render information and short circuit if none.
+ var parentNode = renderGroupNodeInfo.node.parentNode;
+ if (!parentNode) {
+ return;
+ }
+ var parentNodeInfo = this.index[parentNode.name];
+ // Utility function for computing the name of a bridge node.
+ var getBridgeNodeName = function (inbound) {
+ var rest = [];
+ for (var _i = 1; _i < arguments.length; _i++) {
+ rest[_i - 1] = arguments[_i];
+ }
+ return rest.concat([inbound ? "IN" : "OUT"]).join("~~");
+ };
+ // Build out the bridgegraph.
+ var bridgegraph = this.hierarchy.getBridgegraph(nodeName);
+ // Look for popular nodes so we can make annotations instead of paths.
+ var otherCounts = {
+ // Counts of edges coming INTO other nodes by name (outgoing from self).
+ in: {},
+ // Counts of edges going OUT from other nodes by name (coming into self).
+ out: {},
+ // Counts of all control edges involving other nodes by name.
+ control: {},
+ };
+ _.each(bridgegraph.edges(), function (e) {
+ // An edge is inbound if its destination node is in the metagraph.
+ var inbound = !!metagraph.node(e.w);
+ var otherName = inbound ? e.v : e.w;
+ var metaedge = bridgegraph.edge(e);
+ if (!metaedge.numRegularEdges) {
+ otherCounts.control[otherName] =
+ (otherCounts.control[otherName] || 0) + 1;
+ }
+ else if (inbound) {
+ otherCounts.out[otherName] = (otherCounts.out[otherName] || 0) + 1;
+ }
+ else {
+ otherCounts.in[otherName] = (otherCounts.in[otherName] || 0) + 1;
+ }
+ });
+ // Add annotations and edges for bridgegraph relationships.
+ var hierarchyNodeMap = this.hierarchy.getNodeMap();
+ _.each(bridgegraph.edges(), function (bridgeEdgeObj) {
+ var bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj);
+ // Determine whether this bridge edge is incoming by checking the
+ // metagraph for a node that matches the destination end.
+ var inbound = !!metagraph.node(bridgeEdgeObj.w);
+ // Based on the direction of the edge, one endpoint will be an immediate
+ // child of this renderNodeInfo, and the other endpoint will be a sibling
+ // of the parent (or an ancestor further up).
+ var _a = inbound ?
+ [bridgeEdgeObj.w, bridgeEdgeObj.v] :
+ [bridgeEdgeObj.v, bridgeEdgeObj.w], childName = _a[0], otherName = _a[1];
+ var childRenderInfo = _this.index[childName];
+ var otherRenderInfo = _this.index[otherName];
+ var otherNode = otherRenderInfo ?
+ otherRenderInfo.node :
+ hierarchyNodeMap[otherName];
+ // Determine whether this edge is a control edge between nodes where
+ // either node is high-degree with respect to control edges. This will
+ // be a signal to show it as an annotation instead of a bridge edge.
+ var isHighDegreeControlEdge = !bridgeMetaedge.numRegularEdges &&
+ otherCounts.control[otherName] > _this.params.maxControlDegree;
+ var _b = inbound ?
+ [renderNodeInfo.inAnnotations, childRenderInfo.inAnnotations] :
+ [renderNodeInfo.outAnnotations, childRenderInfo.outAnnotations], annotations = _b[0], childAnnotations = _b[1];
+ var isOtherHighDegree = inbound ?
+ otherCounts.out[otherName] > _this.params.maxOutDegree :
+ otherCounts.in[otherName] > _this.params.maxInDegree;
+ // The adjoining render metaedge info from the parent's coreGraph, if any.
+ // It will either be a Metaedge involving this node directly, if it
+ // previously came from a metagraph, or it'll be a Metaedge involving
+ // a previously created bridge node standing in for the other node.
+ var adjoiningMetaedge = null;
+ // We can only hope to render a bridge path if:
+ // - bridgegraph paths are enabled,
+ // - the other node is not too high-degree,
+ // - the child is in the core (not extracted for being high-degree), and
+ // - there's a path (in the traversal sense) between child and other.
+ var canDrawBridgePath = false;
+ if (_this.params.enableBridgegraph &&
+ !isOtherHighDegree &&
+ !isHighDegreeControlEdge &&
+ childRenderInfo.isInCore()) {
+ // Utility function for finding an adjoining metaedge.
+ var findAdjoiningMetaedge = function (targetName) {
+ var adjoiningEdgeObj = inbound ?
+ { v: targetName, w: nodeName } :
+ { v: nodeName, w: targetName };
+ return parentNodeInfo.coreGraph.edge(adjoiningEdgeObj);
+ };
+ adjoiningMetaedge = findAdjoiningMetaedge(otherName);
+ if (!adjoiningMetaedge) {
+ adjoiningMetaedge = findAdjoiningMetaedge(getBridgeNodeName(inbound, otherName, parentNode.name));
+ }
+ canDrawBridgePath = !!adjoiningMetaedge;
+ }
+ // Although dataflow edges are acyclic, control dependency edges may
+ // actually point "backwards" in the graph. If this bridgeMetaedge is
+ // a control dependency, we need to determine whether it's backwards
+ // pointing so that we render it appropriately.
+ //
+ // For instance, say we're rendering a graph with nodes named A/B and Z/Y,
+ // and we're currently rendering the bridgegraph for A. Further, let's say
+ // that there was an original BaseEdge from A/B->Z/Y and a CONTROL EDGE
+ // from Z/Y=>A/B.
+ //
+ // +----------------+
+ // | A |
+ // | +-----+ | +------+
+ // | | B |>----->|>------->| Z |
+ // | | | | | |
+ // | | | * | | |
+ // | | |<=====<|<=======<| |
+ // | +-----+ | +------+
+ // +----------------+
+ //
+ // When we render the subhierarchy for Metanode A, we'll come across a
+ // control-only Metaedge in the bridgegraph from Z=>A/B (*). The question
+ // is whether this edge is backwards.
+ //
+ // To answer that question, we follow the chain of adjoining metaedges
+ // until we reach the topmost one. In this case, that's the control-only
+ // Metaedge Z=>A in the ROOT's metagraph. We determine that this edge
+ // is backwards by looking at the topological ordering of ROOT's metagraph
+ // (which ignores control edges) and seeing that Z comes AFTER A.
+ //
+ // The property of being backwards is independent of whether the edge
+ // is inbound or outbound. In the preceeding example, if we were building
+ // the subhierarchy for Z, we'd find bridge edge Z/Y=>A, walk to its
+ // topmost adjoining metaedge Z=>A and discover that it's backwards.
+ var backwards = false;
+ if (adjoiningMetaedge && !bridgeMetaedge.numRegularEdges) {
+ // Find the top-most adjoining render metaedge information, and the
+ // GroupNode whose metagraph must contain the associated metaedge.
+ var topAdjoiningMetaedge = adjoiningMetaedge;
+ var topGroupNode = parentNodeInfo.node;
+ while (topAdjoiningMetaedge.adjoiningMetaedge) {
+ topAdjoiningMetaedge = topAdjoiningMetaedge.adjoiningMetaedge;
+ topGroupNode = topGroupNode.parentNode;
+ }
+ // Check against the topological ordering for the top node. The current
+ // bridge metaedge we're evaluating is backwards if its source comes
+ // after its destination.
+ var ordering = _this.hierarchy.getTopologicalOrdering(topGroupNode.name);
+ var e = topAdjoiningMetaedge.metaedge;
+ backwards = ordering[e.v] > ordering[e.w];
+ }
+ // Render backwards control edges as annotations.
+ canDrawBridgePath = canDrawBridgePath && !backwards;
+ // If we can't make a bridge path for any reason, then we add an
+ // annotation instead.
+ if (!canDrawBridgePath) {
+ childAnnotations.push(new Annotation(otherNode, otherRenderInfo, new RenderMetaedgeInformation(bridgeMetaedge), AnnotationType.SHORTCUT, inbound), _this.params);
+ return;
+ }
+ // At this point, all conditions have been met for drawing a bridge path.
+ // Find or create the IN/OUT node representing otherNode.
+ var bridgeContainerName = getBridgeNodeName(inbound, nodeName);
+ var bridgeNodeName = getBridgeNodeName(inbound, otherName, nodeName);
+ var bridgeNodeRenderInfo = coreGraph.node(bridgeNodeName);
+ if (!bridgeNodeRenderInfo) {
+ // Find or create the directional container for the bridge node.
+ var bridgeContainerInfo = coreGraph.node(bridgeContainerName);
+ if (!bridgeContainerInfo) {
+ var bridgeContainerNode = {
+ // Important node properties.
+ name: bridgeContainerName,
+ type: graph_1.NodeType.BRIDGE,
+ // Unused node properties.
+ isGroupNode: false,
+ cardinality: 0,
+ parentNode: null,
+ stats: null,
+ // BridgeNode properties.
+ inbound: inbound,
+ };
+ bridgeContainerInfo =
+ new RenderNodeInformation(bridgeContainerNode);
+ _this.index[bridgeContainerName] = bridgeContainerInfo;
+ coreGraph.setNode(bridgeContainerName, bridgeContainerInfo);
+ }
+ var bridgeNode = {
+ // Important node properties.
+ name: bridgeNodeName,
+ type: graph_1.NodeType.BRIDGE,
+ // Unimportant node properties.
+ isGroupNode: false,
+ cardinality: 1,
+ parentNode: null,
+ stats: null,
+ // BridgeNode properties.
+ inbound: inbound,
+ };
+ bridgeNodeRenderInfo = new RenderNodeInformation(bridgeNode);
+ _this.index[bridgeNodeName] = bridgeNodeRenderInfo;
+ coreGraph.setNode(bridgeNodeName, bridgeNodeRenderInfo);
+ // Set bridgeNode to be a graphlib child of the container node.
+ coreGraph.setParent(bridgeNodeName, bridgeContainerName);
+ bridgeContainerInfo.node.cardinality++;
+ }
+ // Create and add a bridge render metaedge.
+ var bridgeRenderMetaedge = new RenderMetaedgeInformation(bridgeMetaedge);
+ bridgeRenderMetaedge.adjoiningMetaedge = adjoiningMetaedge;
+ inbound ?
+ coreGraph.setEdge(bridgeNodeName, childName, bridgeRenderMetaedge) :
+ coreGraph.setEdge(childName, bridgeNodeName, bridgeRenderMetaedge);
+ }); // End _.each(bridgegraph.edges).
+ // For each bridge container (IN and/or OUT), add structural edges between
+ // terminal nodes and that container. A terminal node is one which has no
+ // non-bridge edges in the direction of the container.
+ //
+ // For example, consider a Metanode A which contains two child nodes A/B
+ // and A/C. Let's say it has one edge in the metagraph from A/B->A/C, and
+ // one edge in the bridgegraph from Z->A/C.
+ //
+ // At this point, we've added a container bridge node IN to house all
+ // incoming bridge nodes. We'v alse added a bridge node Z' (with parent IN)
+ // to A, and a bridge edge from Z'->C.
+ //
+ // +----------------------+
+ // | A +---+ |
+ // | +------>| C | |
+ // | | +---+ |
+ // | | ^ |
+ // | | | |
+ // | | +----|----+ |
+ // | | | IN | | |
+ // | +---+ | +---+ | |
+ // | | B | | | Z'| | |
+ // | +---+ | +---+ | |
+ // | +---------+ |
+ // +----------------------+
+ //
+ // With no other help, dagre would lay out B and Z' on the same level,
+ // because both of them have no incoming edges. In other words, B is a
+ // terminal node in the INCOMING direction.
+ //
+ // But we want to force dagre to lay out Z' (and everything in IN) lower
+ // than all non-bridge nodes, so that there's enough room for the bridge
+ // edges after they've been adjusted to meet up with paths coming in from
+ // outside.
+ //
+ // To force Z' (and all other bridge nodes) to be lowest in the graph, we
+ // identify terminal nodes like B and give them structural edges to
+ // a new structural bridge node S which we add to IN.
+ //
+ // +----------------------+
+ // | A +---+ |
+ // | +--->| C | |
+ // | | +---+ |
+ // | +---+ ^ |
+ // | | B | | |
+ // | +---+ | |
+ // | ^ | |
+ // | | | |
+ // | +----|------|----+ |
+ // | |IN | | | |
+ // | | +---+ +---+ | |
+ // | | | S | | Z'| | |
+ // | | +---+ +---+ | |
+ // | +----------------+ |
+ // +----------------------+
+ //
+ // This ensures that dagre will lay out the bridge containers strictly at
+ // the ends of the graph. The structural edges will never be seen in the
+ // visualization except as a debugging aid.
+ _.each([true, false], function (inbound) {
+ var bridgeContainerName = getBridgeNodeName(inbound, nodeName);
+ var bridgeContainerInfo = coreGraph.node(bridgeContainerName);
+ if (!bridgeContainerInfo) {
+ return;
+ }
+ _.each(coreGraph.nodes(), function (childName) {
+ // Short-circuit if this child is a bridge node or it's not a terminal
+ // node in the direction we're interested in.
+ var childNodeInfo = coreGraph.node(childName);
+ if (childNodeInfo.node.type === graph_1.NodeType.BRIDGE) {
+ return;
+ }
+ var isTerminal = inbound ?
+ !coreGraph.predecessors(childName).length :
+ !coreGraph.successors(childName).length;
+ if (!isTerminal) {
+ return;
+ }
+ // Find or create a bridge node in the container for all structural
+ // metaedges. It would have been nice to skip this step and simply
+ // set a metaedge between the terminal node and the container node, but
+ // in that case, something about the graph upsets dagre.layout()'s
+ // longestPath algorithm (was getting errors due to an undefined).
+ var structuralNodeName = getBridgeNodeName(inbound, nodeName, "STRUCTURAL_TARGET");
+ var structuralRenderInfo = coreGraph.node(structuralNodeName);
+ if (!structuralRenderInfo) {
+ var bridgeNode = {
+ // Important Node properties.
+ name: structuralNodeName,
+ type: graph_1.NodeType.BRIDGE,
+ // Unimportant Node properties.
+ isGroupNode: false,
+ cardinality: 1,
+ parentNode: null,
+ stats: null,
+ // BridgeNode properties.
+ inbound: inbound,
+ };
+ structuralRenderInfo = new RenderNodeInformation(bridgeNode);
+ structuralRenderInfo.structural = true;
+ _this.index[structuralNodeName] = structuralRenderInfo;
+ coreGraph.setNode(structuralNodeName, structuralRenderInfo);
+ bridgeContainerInfo.node.cardinality++;
+ coreGraph.setParent(structuralNodeName, bridgeContainerName);
+ }
+ // Create the structural Metaedge and insert it.
+ var structuralMetaedgeInfo = new RenderMetaedgeInformation(null);
+ structuralMetaedgeInfo.structural = true;
+ structuralMetaedgeInfo.weight--; // Reduce weight for dagre layout.
+ inbound ?
+ coreGraph.setEdge(structuralNodeName, childName, structuralMetaedgeInfo) :
+ coreGraph.setEdge(childName, structuralNodeName, structuralMetaedgeInfo);
+ });
+ });
+ };
+ return RenderGraphInformation;
+ })();
+ render.RenderGraphInformation = RenderGraphInformation;
+ /**
+ * A class for rendering annotation object which contains label
+ * about the node embedded as annotation, type of annotation and the location
+ * of both the annotation's node and edge.
+ *
+ * Annotation objects include embedded constants, embedded summary, and
+ * edge shortcuts.
+ */
+ var Annotation = (function () {
+ /**
+ * Creates a new Annotation.
+ *
+ * @param node The underlying node this annotation points to.
+ * @param renderNodeInfo The render information for the underlying node
+ * this annotation points to. This can be null if the annotation
+ * denotes an embedding (constant, summary), in which case we
+ * use the node property.
+ * @param renderMetaedgeInfo The render information for the edge associated
+ * with the annotation.
+ * @param type The type of the annotation.
+ * @param isIn True if it is an in-annotation. False if it is an
+ * out-annotation.
+ */
+ function Annotation(node, renderNodeInfo, renderMetaedgeInfo, type, isIn) {
+ this.node = node;
+ this.renderNodeInfo = renderNodeInfo;
+ this.renderMetaedgeInfo = renderMetaedgeInfo;
+ this.annotationType = type;
+ // Properties specified by layout
+ this.dx = 0;
+ this.dy = 0;
+ this.width = 0;
+ this.height = 0;
+ this.isIn = isIn;
+ this.points = [];
+ }
+ return Annotation;
+ })();
+ render.Annotation = Annotation;
+ ;
+ (function (AnnotationType) {
+ AnnotationType[AnnotationType["SHORTCUT"] = 0] = "SHORTCUT";
+ AnnotationType[AnnotationType["CONSTANT"] = 1] = "CONSTANT";
+ AnnotationType[AnnotationType["SUMMARY"] = 2] = "SUMMARY";
+ AnnotationType[AnnotationType["ELLIPSIS"] = 3] = "ELLIPSIS";
+ })(render.AnnotationType || (render.AnnotationType = {}));
+ var AnnotationType = render.AnnotationType;
+ ;
+ /**
+ * Manages a list of annotations. Two will be used for each
+ * RenderNodeInformation, one for in annotations and one for out annotations.
+ */
+ var AnnotationList = (function () {
+ function AnnotationList() {
+ this.list = [];
+ this.nodeNames = {};
+ }
+ /**
+ * Append an annotation to the list, or a stand-in ellipsis annotation instead
+ * if this would make it too many.
+ */
+ AnnotationList.prototype.push = function (annotation, params) {
+ if (annotation.node.name in this.nodeNames) {
+ return; // Skip duplicate annotation.
+ }
+ this.nodeNames[annotation.node.name] = true;
+ if (this.list.length < params.maxAnnotations) {
+ this.list.push(annotation);
+ return;
+ }
+ var lastAnnotation = this.list[this.list.length - 1];
+ if (lastAnnotation.annotationType === AnnotationType.ELLIPSIS) {
+ var ellipsisNode_1 = lastAnnotation.node;
+ ellipsisNode_1.setNumMoreNodes(++ellipsisNode_1.numMoreNodes);
+ return;
+ }
+ var ellipsisNode = new tf.graph.EllipsisNodeImpl(1);
+ this.list.push(new Annotation(ellipsisNode, new RenderNodeInformation(ellipsisNode), null, AnnotationType.ELLIPSIS, annotation.isIn));
+ };
+ return AnnotationList;
+ })();
+ render.AnnotationList = AnnotationList;
+ /**
+ * Contains rendering information about a node in the hierarchical graph.
+ */
+ var RenderNodeInformation = (function () {
+ function RenderNodeInformation(node) {
+ this.node = node;
+ this.expanded = false;
+ this.inAnnotations = new AnnotationList();
+ this.outAnnotations = new AnnotationList();
+ // Params specified by layout
+ this.x = 0;
+ this.y = 0;
+ this.width = 0;
+ this.height = 0;
+ this.inboxWidth = 0;
+ this.outboxWidth = 0;
+ this.excluded = false;
+ // Params for bridge paths.
+ this.structural = false;
+ // Params for node box.
+ this.labelOffset = 0;
+ this.extractXOffset = 0;
+ this.radius = 0;
+ // Params for expanded node
+ this.labelHeight = 0;
+ this.paddingTop = 0;
+ this.paddingLeft = 0;
+ this.paddingRight = 0;
+ this.paddingBottom = 0;
+ this.outerWidth = 0;
+ this.outerHeight = 0;
+ this.isInExtract = false;
+ this.isOutExtract = false;
+ }
+ RenderNodeInformation.prototype.isInCore = function () {
+ return !this.isInExtract && !this.isOutExtract;
+ };
+ return RenderNodeInformation;
+ })();
+ render.RenderNodeInformation = RenderNodeInformation;
+ /**
+ * Contains rendering information about a Metaedge from the underlying
+ * hierarchical graph. It may be from either a metagraph or a bridgegraph.
+ */
+ var RenderMetaedgeInformation = (function () {
+ function RenderMetaedgeInformation(metaedge) {
+ this.metaedge = metaedge;
+ this.adjoiningMetaedge = null;
+ this.structural = false;
+ this.weight = 1;
+ }
+ return RenderMetaedgeInformation;
+ })();
+ render.RenderMetaedgeInformation = RenderMetaedgeInformation;
+ function addInAnnotation(node, predecessor, predecessorRenderInfo, edge, type, params) {
+ var annotation = new Annotation(predecessor, predecessorRenderInfo, edge, type, true);
+ node.inAnnotations.push(annotation, params);
+ }
+ function addOutAnnotation(node, successor, successorRenderInfo, edge, type, params) {
+ var annotation = new Annotation(successor, successorRenderInfo, edge, type, false);
+ node.outAnnotations.push(annotation, params);
+ }
+ function setGraphDepth(graph, depth) {
+ _.each(graph.nodes(), function (nodeName) {
+ var child = graph.node(nodeName);
+ child.expanded = depth > 1; // set all child of depth 1 to collapsed
+ if (depth > 0) {
+ switch (child.node.type) {
+ case graph_1.NodeType.META:
+ case graph_1.NodeType.SERIES:
+ setGroupNodeDepth(child, depth - 1);
+ break;
+ }
+ }
+ });
+ }
+ ;
+ var RenderGroupNodeInformation = (function (_super) {
+ __extends(RenderGroupNodeInformation, _super);
+ function RenderGroupNodeInformation(groupNode) {
+ _super.call(this, groupNode);
+ var metagraph = groupNode.metagraph;
+ var gl = metagraph.graph();
+ this.coreGraph =
+ graph_1.createGraph(gl.name, graph_1.GraphType.CORE, { compound: true });
+ this.coreBox = { width: 0, height: 0 };
+ this.inExtractBox = { width: 0, height: 0 };
+ this.outExtractBox = { width: 0, height: 0 };
+ this.isolatedInExtract = [];
+ this.isolatedOutExtract = [];
+ }
+ return RenderGroupNodeInformation;
+ })(RenderNodeInformation);
+ render.RenderGroupNodeInformation = RenderGroupNodeInformation;
+ function setGroupNodeDepth(renderInfo, depth) {
+ if (renderInfo.coreGraph) {
+ setGraphDepth(renderInfo.coreGraph, depth);
+ }
+ }
+ /**
+ * Remove an edge from the graph and add annotations to both ends of the edge.
+ *
+ * @param The core graph.
+ * @param v Source name.
+ * @param w Sink name.
+ */
+ function createShortcut(graph, v, w, params) {
+ var src = graph.node(v);
+ var sink = graph.node(w);
+ var edge = graph.edge(v, w);
+ // Add each annotation.
+ addOutAnnotation(src, sink.node, sink, edge, AnnotationType.SHORTCUT, params);
+ addInAnnotation(sink, src.node, src, edge, AnnotationType.SHORTCUT, params);
+ // Remove the edge from the core graph.
+ graph.removeEdge(v, w);
+ }
+ /**
+ * Remove edges from a node, and set its isOutExtract property to true,
+ * and remove the node and move it to isolatedOutExtract.
+ *
+ * If detachAllEdgesForHighDegree is true, extract all of its edges.
+ * Otherwise, only extract all in-edges.
+ */
+ function makeOutExtract(renderNode, n, params) {
+ var graph = renderNode.coreGraph;
+ graph.node(n).isOutExtract = true;
+ _.each(graph.predecessors(n), function (p, index) {
+ createShortcut(graph, p, n, params);
+ });
+ if (params.detachAllEdgesForHighDegree) {
+ _.each(graph.successors(n), function (s, index) {
+ createShortcut(graph, n, s, params);
+ });
+ }
+ if (params.detachAllEdgesForHighDegree || graph.neighbors(n).length === 0) {
+ renderNode.isolatedOutExtract.push(graph.node(n));
+ graph.removeNode(n);
+ }
+ }
+ /**
+ * Remove edges from a node, set its isInExtract property to true,
+ * and remove the node and move it to isolatedInExtract.
+ * If detachAllEdgesForHighDegree is true, extract all of its edges.
+ * Otherwise, only remove all out-edges.
+ */
+ function makeInExtract(renderNode, n, params) {
+ var graph = renderNode.coreGraph;
+ graph.node(n).isInExtract = true;
+ _.each(graph.successors(n), function (s, index) {
+ createShortcut(graph, n, s, params);
+ });
+ if (params.detachAllEdgesForHighDegree) {
+ _.each(graph.predecessors(n), function (p, index) {
+ createShortcut(graph, p, n, params);
+ });
+ }
+ // Remove the node from the core graph if conditions are met.
+ if (params.detachAllEdgesForHighDegree || graph.neighbors(n).length === 0) {
+ renderNode.isolatedInExtract.push(graph.node(n));
+ graph.removeNode(n);
+ }
+ }
+ /**
+ * Check whether the node's type is a member of the given list of types.
+ *
+ * @param node Node.
+ * @param types List of type to match.
+ */
+ function hasTypeIn(node, types) {
+ if (node.type === graph_1.NodeType.OP) {
+ for (var i = 0; i < types.length; i++) {
+ if (node.op === types[i]) {
+ return true;
+ }
+ }
+ }
+ else if (node.type === graph_1.NodeType.META) {
+ var rootOpNode = node.getRootOp();
+ if (rootOpNode) {
+ for (var i = 0; i < types.length; i++) {
+ if (rootOpNode.op === types[i]) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ /** Remove edges from pre-defined out-extract patterns */
+ function extractPredefinedSink(renderNode, params) {
+ var graph = renderNode.coreGraph;
+ _.each(graph.nodes(), function (n) {
+ var renderInfo = graph.node(n);
+ if (hasTypeIn(renderInfo.node, params.outExtractTypes)) {
+ makeOutExtract(renderNode, n, params);
+ }
+ });
+ }
+ /** Remove edges from pre-defined in-extract patterns */
+ function extractPredefinedSource(renderNode, params) {
+ var graph = renderNode.coreGraph;
+ _.each(graph.nodes(), function (n) {
+ var renderInfo = graph.node(n);
+ if (hasTypeIn(renderInfo.node, params.inExtractTypes)) {
+ makeInExtract(renderNode, n, params);
+ }
+ });
+ }
+ /** Extract from nodes with in-degree > maxInDegree */
+ function extractHighInDegree(renderNode, params) {
+ var graph = renderNode.coreGraph;
+ var maxInDegree = params.maxInDegree;
+ // detect first so degrees don't get affected by other removal
+ var highInDegreeNames = _.filter(graph.nodes(), function (n) {
+ // Count the in-degree based on only regular edges, unless there are
+ // no regular edges, in which case use the number of control edges.
+ // This is done so that control edges don't effect if nodes are extracted
+ // from the core graph, unless the node is only used for control.
+ var numEdgesToCount = _.reduce(graph.predecessors(n), function (numEdgesToCount, pred) {
+ var metaedge = graph.edge(pred, n).metaedge;
+ return numEdgesToCount + (metaedge.numRegularEdges ? 1 : 0);
+ }, 0);
+ if (numEdgesToCount === 0 && graph.predecessors(n).length > 0) {
+ numEdgesToCount = graph.predecessors(n).length;
+ }
+ return numEdgesToCount > maxInDegree;
+ });
+ _.each(highInDegreeNames, function (n) {
+ makeOutExtract(renderNode, n, params);
+ });
+ }
+ /** Extract nodes with out-degree > maxOutDegree */
+ function extractHighOutDegree(renderNode, params) {
+ var graph = renderNode.coreGraph;
+ var maxOutDegree = params.maxOutDegree;
+ // detect first so degrees don't get affected by other removal
+ var highOutDegreeNames = _.filter(graph.nodes(), function (n) {
+ // Count the out-degree based on only regular edges, unless there are
+ // no regular edges, in which case use the number of control edges.
+ // This is done so that control edges don't effect if nodes are extracted
+ // from the core graph, unless the node is only used for control.
+ var numEdgesToCount = _.reduce(graph.successors(n), function (numEdgesToCount, succ) {
+ var metaedge = graph.edge(n, succ).metaedge;
+ return numEdgesToCount + (metaedge.numRegularEdges ? 1 : 0);
+ }, 0);
+ if (numEdgesToCount === 0 && graph.successors(n).length > 0) {
+ numEdgesToCount = graph.successors(n).length;
+ }
+ return numEdgesToCount > maxOutDegree;
+ });
+ _.each(highOutDegreeNames, function (n) {
+ makeInExtract(renderNode, n, params);
+ });
+ }
+ /** Remove control edges from nodes that have too many control edges */
+ function removeControlEdges(renderNode, params) {
+ var graph = renderNode.coreGraph;
+ // Collect control edges into a map by node name.
+ var map = {};
+ _.each(graph.edges(), function (e) {
+ if (!graph.edge(e).metaedge.numRegularEdges) {
+ (map[e.v] = map[e.v] || []).push(e);
+ (map[e.w] = map[e.w] || []).push(e);
+ }
+ });
+ // For each node with too many control edges, turn them into annotations.
+ _.each(map, function (edges, nodeName) {
+ if (edges.length > params.maxControlDegree) {
+ _.each(edges, function (e) { return createShortcut(graph, e.v, e.w, params); });
+ }
+ });
+ }
+ /**
+ * Given an integer, picks a hue that is far apart from other colors.
+ * The formula for picking color that avoid collision is:
+ * hue = (color range * golden ratio * index) % color range
+ */
+ function mapIndexToHue(id) {
+ var GOLDEN_RATIO = 1.61803398875;
+ // Hue of 0 is reserved for the gray nodes.
+ var MIN_HUE = 1;
+ var MAX_HUE = 359;
+ var COLOR_RANGE = MAX_HUE - MIN_HUE;
+ return MIN_HUE + ((COLOR_RANGE * GOLDEN_RATIO * id) % COLOR_RANGE);
+ }
+ render.mapIndexToHue = mapIndexToHue;
+ ;
+ /**
+ * Remove edges and add to annotation instead.
+ *
+ * For root node, consider predefined types for source and sink.
+ * We do not extract predefined type from non-root so that Variables and the
+ * sgd node (op type = "NoOp") do not get extract from inside own group.
+ *
+ * The order of extraction is important here as swapping the order can totally
+ * screw up the graph layout.
+ *
+ * @param {Render.Node} renderNode Node to manipulate.
+ * @param {Object} params render Graph construction parameters. See
+ * <tf-graph-params>'s output
+ */
+ function extractHighDegrees(renderNode, params) {
+ if (params.outExtractTypes) {
+ extractPredefinedSink(renderNode, params);
+ }
+ // This has to come before extract high in-degree to protect the core part
+ // that takes many variables.
+ if (params.inExtractTypes) {
+ extractPredefinedSource(renderNode, params);
+ }
+ // This has to come before extract high out-degree to protect the core part
+ // that output to many places as there are more high-degree sinks than
+ // sources.
+ if (params.maxInDegree) {
+ extractHighInDegree(renderNode, params);
+ }
+ if (params.maxOutDegree) {
+ extractHighOutDegree(renderNode, params);
+ }
+ if (params.maxControlDegree) {
+ removeControlEdges(renderNode, params);
+ }
+ // Extract isolated nodes, which can be
+ // (1) source-like and sink-like nodes that are not originally isolated but
+ // become isolated after further removal.
+ // (2) isolated nodes with annotations on one-side. These might be either
+ // - nodes that originally have high out-degree but because we remove
+ // high in-degree nodes first, they no longer have high in-degree when
+ // we check. (Detecting all high-degree before removing also leads to
+ // another problem.)
+ // - nodes that do not have high degree, but their neighbors are all
+ // extracted, so it might make sense to extract them too.
+ var graph = renderNode.coreGraph;
+ _.each(graph.nodes(), function (n) {
+ var child = graph.node(n);
+ var degree = graph.neighbors(n).length;
+ if (degree === 0) {
+ var hasOutAnnotations = child.outAnnotations.list.length > 0;
+ var hasInAnnotations = child.inAnnotations.list.length > 0;
+ if (child.isInExtract) {
+ // This case only happens if detachAllEdgesForHighDegree is false.
+ // (Otherwise all source-like nodes are all isolated already.)
+ renderNode.isolatedInExtract.push(child);
+ graph.removeNode(n);
+ }
+ else if (child.isOutExtract) {
+ // This case only happens if detachAllEdgesForHighDegree is false.
+ // // (Otherwise all sink-like nodes are all isolated already.)
+ renderNode.isolatedOutExtract.push(child);
+ graph.removeNode(n);
+ }
+ else if (params.extractIsolatedNodesWithAnnotationsOnOneSide) {
+ if (hasOutAnnotations && !hasInAnnotations) {
+ child.isInExtract = true; // for ones with high out-annotations
+ renderNode.isolatedInExtract.push(child);
+ graph.removeNode(n);
+ }
+ else if (hasInAnnotations && !hasOutAnnotations) {
+ child.isOutExtract = true; // for ones with high in-annotations
+ renderNode.isolatedOutExtract.push(child);
+ graph.removeNode(n);
+ }
+ else {
+ }
+ }
+ }
+ });
+ }
+ })(render = graph_1.render || (graph_1.render = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module tf.graph.render
+</script>
+<script>/// <reference path="graph.ts" />
+/// <reference path="hierarchy.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var template;
+ (function (template) {
+ /**
+ * Detect repeating patterns of subgraphs.
+ * Assign templateId to each subgraph if it belongs to a template.
+ * Returns clusters of similar subgraphs .
+ *
+ * @param graph
+ * @param verifyTemplate whether to run the template verification algorithm
+ * @return a dict (template id => Array of node names)
+ */
+ function detect(h, verifyTemplate) {
+ // In any particular subgraph, there are either
+ // - leaf nodes (which do not have subgraph)
+ // - metanode nodes - some of them have only one member (singular metanode)
+ // and some have multiple members (non-singular metanode)
+ // First, generate a nearest neighbor hash of metanode nodes.
+ var nnGroups = clusterSimilarSubgraphs(h);
+ // For each metanode, compare its subgraph (starting from shallower groups)
+ // and assign template id.
+ var templates = groupTemplateAndAssignId(nnGroups, verifyTemplate);
+ // Sort the templates by minimum level in the graph at which they appear,
+ // as this leads to optimal setting of the colors of each template for
+ // maximum differentiation.
+ return _(templates).pairs()
+ .sortBy(function (pair) {
+ return pair[1].level;
+ })
+ .map(function (pair) {
+ return [pair[0], pair[1].nodes];
+ })
+ .object().value();
+ }
+ template.detect = detect;
+ ;
+ /**
+ * @return Unique string for a metanode based on depth, |V|, |E| and
+ * op type histogram.
+ */
+ function getSignature(metanode) {
+ // depth=<number> |V|=<number> |E|=<number>
+ var props = _.map({
+ "depth": metanode.depth,
+ "|V|": metanode.metagraph.nodes().length,
+ "|E|": metanode.metagraph.edges().length
+ }, function (v, k) { return k + "=" + v; }).join(" ");
+ // optype1=count1,optype2=count2
+ var ops = _.map(metanode.opHistogram, function (count, op) {
+ return op + "=" + count;
+ }).join(",");
+ return props + " [ops] " + ops;
+ }
+ /**
+ * Generate a nearest neighbor hash of metanodes
+ * based on depth, |V|, |E|, and opHistogram of their subgraph
+ * (excluding leaf nodes and singular metanodes).
+ * @param graph The graph
+ * @return Array of pairs of [signature,
+ * Object with min level of the template and an Array of tf.graph.Group]
+ * sort by ascending order of minimum depth at which metanode appears.
+ */
+ function clusterSimilarSubgraphs(h) {
+ /** a dict from metanode.signature() => Array of tf.graph.Groups */
+ var hashDict = _(h.getNodeMap()).reduce(function (hash, node, name) {
+ if (node.type !== graph_1.NodeType.META) {
+ return hash;
+ }
+ var levelOfMetaNode = name.split("/").length - 1;
+ var signature = getSignature(node);
+ var templateInfo = hash[signature] ||
+ { nodes: [], level: levelOfMetaNode };
+ hash[signature] = templateInfo;
+ templateInfo.nodes.push(node);
+ if (templateInfo.level > levelOfMetaNode) {
+ templateInfo.level = levelOfMetaNode;
+ }
+ return hash;
+ }, {});
+ return _(hashDict).pairs()
+ .filter(function (pair) {
+ return pair[1].nodes.length > 1;
+ })
+ .sortBy(function (pair) {
+ // sort by depth
+ // (all members in the same nnGroup has equal depth)
+ return pair[1].nodes[0].depth;
+ })
+ .value();
+ }
+ function groupTemplateAndAssignId(nnGroups, verifyTemplate) {
+ // For each metanode, compare its subgraph (starting from shallower groups)
+ // and assign template id.
+ return _.reduce(nnGroups, function (templates, nnGroupPair) {
+ var signature = nnGroupPair[0], nnGroup = nnGroupPair[1].nodes, clusters = [];
+ nnGroup.forEach(function (metanode) {
+ // check with each existing cluster
+ for (var i = 0; i < clusters.length; i++) {
+ var similar = !verifyTemplate ||
+ isSimilarSubgraph(clusters[i].metanode.metagraph, metanode.metagraph);
+ // if similar, just add this metanode to the cluster
+ if (similar) {
+ // get template from the first one
+ metanode.templateId = clusters[i].metanode.templateId;
+ clusters[i].members.push(metanode.name);
+ return;
+ }
+ }
+ // otherwise create a new cluster with id "signature [count] "
+ metanode.templateId = signature + "[" + clusters.length + "]";
+ clusters.push({
+ metanode: metanode,
+ members: [metanode.name]
+ });
+ });
+ clusters.forEach(function (c) {
+ templates[c.metanode.templateId] = {
+ level: nnGroupPair[1].level,
+ nodes: c.members
+ };
+ });
+ return templates;
+ }, {});
+ }
+ function sortNodes(names, graph, prefix) {
+ return _.sortByAll(names, function (name) {
+ var node = graph.node(name);
+ return node.op;
+ }, function (name) {
+ var node = graph.node(name);
+ return node.templateId;
+ }, function (name) {
+ return graph.neighbors(name).length;
+ }, function (name) {
+ return graph.predecessors(name).length;
+ }, function (name) {
+ return graph.successors(name).length;
+ }, function (name) {
+ return name.substr(prefix.length);
+ });
+ }
+ function isSimilarSubgraph(g1, g2) {
+ if (!tf.graph.hasSimilarDegreeSequence(g1, g2)) {
+ return false;
+ }
+ // if we want to skip, just return true here.
+ // return true;
+ // Verify sequence by running DFS
+ var g1prefix = g1.graph().name;
+ var g2prefix = g2.graph().name;
+ var visited1 = {};
+ var visited2 = {};
+ var stack = [];
+ /**
+ * push sources or successors into the stack
+ * if the visiting pattern has been similar.
+ */
+ function stackPushIfNotDifferent(n1, n2) {
+ var sub1 = n1.substr(g1prefix.length), sub2 = n2.substr(g2prefix.length);
+ /* tslint:disable */
+ if (visited1[sub1] ^ visited2[sub1]) {
+ console.warn("different visit pattern", "[" + g1prefix + "]", sub1, "[" + g2prefix + "]", sub2);
+ return true;
+ }
+ /* tslint:enable */
+ if (!visited1[sub1]) {
+ visited1[sub1] = visited2[sub2] = true;
+ stack.push({ n1: n1, n2: n2 });
+ }
+ return false;
+ }
+ // check if have same # of sources then sort and push
+ var sources1 = g1.sources();
+ var sources2 = g2.sources();
+ if (sources1.length !== sources2.length) {
+ /* tslint:disable */
+ console.log("different source length");
+ /* tslint:enable */
+ return false;
+ }
+ sources1 = sortNodes(sources1, g1, g1prefix);
+ sources2 = sortNodes(sources2, g2, g2prefix);
+ for (var i = 0; i < sources1.length; i++) {
+ var different = stackPushIfNotDifferent(sources1[i], sources2[i]);
+ if (different) {
+ return false;
+ }
+ }
+ while (stack.length > 0) {
+ var cur = stack.pop();
+ // check node
+ var similar = isSimilarNode(g1.node(cur.n1), g2.node(cur.n2));
+ if (!similar) {
+ return false;
+ }
+ // check if have same # of successors then sort and push
+ var succ1 = g1.successors(cur.n1), succ2 = g2.successors(cur.n2);
+ if (succ1.length !== succ2.length) {
+ /* tslint:disable */
+ console.log("# of successors mismatch", succ1, succ2);
+ /* tslint:enable */
+ return false;
+ }
+ succ1 = sortNodes(succ1, g1, g1prefix);
+ succ2 = sortNodes(succ2, g2, g2prefix);
+ for (var j = 0; j < succ1.length; j++) {
+ var different = stackPushIfNotDifferent(succ1[j], succ2[j]);
+ if (different) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ /**
+ * Returns if two nodes have identical structure.
+ */
+ function isSimilarNode(n1, n2) {
+ if (n1.type === graph_1.NodeType.META) {
+ // compare metanode
+ var metanode1 = n1;
+ var metanode2 = n2;
+ return metanode1.templateId && metanode2.templateId && metanode1.templateId === metanode2.templateId;
+ }
+ else if (n1.type === graph_1.NodeType.OP && n2.type === graph_1.NodeType.OP) {
+ // compare leaf node
+ return n1.op === n2.op;
+ }
+ else if (n1.type === graph_1.NodeType.SERIES && n2.type === graph_1.NodeType.SERIES) {
+ // compare series node sizes and operations
+ // (only need to check one op as all op nodes are identical in series)
+ var seriesnode1 = n1;
+ var seriesnode2 = n2;
+ var seriesnode1Count = seriesnode1.metagraph.nodeCount();
+ return (seriesnode1Count === seriesnode2.metagraph.nodeCount() &&
+ (seriesnode1Count === 0 ||
+ (seriesnode1.metagraph.node(seriesnode1.metagraph.nodes()[0]).op ===
+ seriesnode2.metagraph.node(seriesnode2.metagraph.nodes()[0]).op)));
+ }
+ return false;
+ }
+ })(template = graph_1.template || (graph_1.template = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {}));
+</script>
+<script>/// <reference path="../graph.ts" />
+/// <reference path="edge.ts" />
+/// <reference path="node.ts" />
+/// <reference path="../layout.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph) {
+ var scene;
+ (function (scene) {
+ /** Enums element class of objects in the scene */
+ scene.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.
+ */
+ function fit(svg, zoomG, d3zoom, callback) {
+ var svgRect = svg.getBoundingClientRect();
+ var sceneSize = zoomG.getBBox();
+ var scale = 0.9 * Math.min(svgRect.width / sceneSize.width, svgRect.height / sceneSize.height, 2);
+ var params = graph.layout.PARAMS.graph;
+ var zoomEvent = d3zoom.scale(scale)
+ .on("zoomend.fitted", function () {
+ // 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);
+ }
+ scene.fit = fit;
+ ;
+ /**
+ * 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.
+ */
+ function panToNode(nodeName, svg, zoomG, d3zoom) {
+ var node = d3.selectAll("[data-name='" + nodeName + "']."
+ + scene.Class.Node.GROUP)[0][0];
+ if (!node) {
+ return false;
+ }
+ var translate = d3zoom.translate();
+ // Check if the selected node is off-screen in either
+ // X or Y dimension in either direction.
+ var nodeBox = node.getBBox();
+ var nodeCtm = node.getScreenCTM();
+ var pointTL = svg.createSVGPoint();
+ var 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);
+ var isOutsideOfBounds = function (start, end, bound) {
+ return end < 0 || start > bound;
+ };
+ var 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.
+ var centerX = (pointTL.x + pointBR.x) / 2;
+ var centerY = (pointTL.y + pointBR.y) / 2;
+ var dx = ((svgRect.width / 2) - centerX);
+ var dy = ((svgRect.height / 2) - centerY);
+ var zoomEvent = d3zoom.translate([translate[0] + dx, translate[1] + dy])
+ .event;
+ d3.select(zoomG).transition().duration(500).call(zoomEvent);
+ return true;
+ }
+ return false;
+ }
+ scene.panToNode = panToNode;
+ ;
+ /**
+ * 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
+ */
+ function selectOrCreateChild(container, tagName, className, before) {
+ var child = selectChild(container, tagName, className);
+ if (!child.empty()) {
+ return child;
+ }
+ var newElement = document.createElementNS("http://www.w3.org/2000/svg", tagName);
+ if (className) {
+ newElement.classList.add(className);
+ }
+ if (before) {
+ container.node().insertBefore(newElement, before);
+ }
+ else {
+ container.node().appendChild(newElement);
+ }
+ return d3.select(newElement)
+ .datum(container.datum());
+ }
+ scene.selectOrCreateChild = selectOrCreateChild;
+ ;
+ /**
+ * 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
+ */
+ function selectChild(container, tagName, className) {
+ var children = container.node().childNodes;
+ for (var i = 0; i < children.length; i++) {
+ var child = children[i];
+ if (child.tagName === tagName &&
+ (!className || child.classList.contains(className))) {
+ return d3.select(child);
+ }
+ }
+ return d3.select(null);
+ }
+ scene.selectChild = selectChild;
+ ;
+ /**
+ * 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").
+ */
+ function buildGroup(container, renderNode, sceneBehavior, sceneClass) {
+ sceneClass = sceneClass || scene.Class.Scene.GROUP;
+ var isNewSceneGroup = selectChild(container, "g", sceneClass).empty();
+ var sceneGroup = selectOrCreateChild(container, "g", sceneClass);
+ // core
+ var coreGroup = selectOrCreateChild(sceneGroup, "g", scene.Class.Scene.CORE);
+ var coreNodes = _.reduce(renderNode.coreGraph.nodes(), function (nodes, name) {
+ var node = renderNode.coreGraph.node(name);
+ if (!node.excluded) {
+ nodes.push(node);
+ }
+ return nodes;
+ }, []);
+ if (renderNode.node.type === graph.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).
+ scene.edge.buildGroup(coreGroup, renderNode.coreGraph, sceneBehavior);
+ // Create the layer of nodes for this scene (ellipses, rects etc).
+ scene.node.buildGroup(coreGroup, coreNodes, sceneBehavior);
+ // In-extract
+ if (renderNode.isolatedInExtract.length > 0) {
+ var inExtractGroup = selectOrCreateChild(sceneGroup, "g", scene.Class.Scene.INEXTRACT);
+ scene.node.buildGroup(inExtractGroup, renderNode.isolatedInExtract, sceneBehavior);
+ }
+ else {
+ selectChild(sceneGroup, "g", scene.Class.Scene.INEXTRACT).remove();
+ }
+ // Out-extract
+ if (renderNode.isolatedOutExtract.length > 0) {
+ var outExtractGroup = selectOrCreateChild(sceneGroup, "g", scene.Class.Scene.OUTEXTRACT);
+ scene.node.buildGroup(outExtractGroup, renderNode.isolatedOutExtract, sceneBehavior);
+ }
+ else {
+ selectChild(sceneGroup, "g", scene.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;
+ }
+ scene.buildGroup = buildGroup;
+ ;
+ /**
+ * 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) {
+ // 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.
+ var yTranslate = renderNode.node.type === graph.NodeType.SERIES ?
+ 0 : graph.layout.PARAMS.subscene.meta.labelHeight;
+ // core
+ translate(selectChild(sceneGroup, "g", scene.Class.Scene.CORE), 0, yTranslate);
+ // in-extract
+ var inExtractX = renderNode.coreBox.width === 0 ?
+ 0 : renderNode.coreBox.width;
+ var hasInExtract = renderNode.isolatedInExtract.length > 0;
+ if (hasInExtract) {
+ translate(selectChild(sceneGroup, "g", scene.Class.Scene.INEXTRACT), inExtractX, yTranslate);
+ }
+ // out-extract
+ var hasOutExtract = renderNode.isolatedOutExtract.length > 0;
+ if (hasOutExtract) {
+ var outExtractX = inExtractX + renderNode.inExtractBox.width
+ + renderNode.extractXOffset;
+ translate(selectChild(sceneGroup, "g", scene.Class.Scene.OUTEXTRACT), outExtractX, yTranslate);
+ }
+ }
+ ;
+ /** Adds a click listener to a group that fires a graph-select event */
+ function addGraphClickListener(graphGroup, sceneBehavior) {
+ d3.select(graphGroup).on("click", function () {
+ sceneBehavior.fire("graph-select");
+ });
+ }
+ scene.addGraphClickListener = addGraphClickListener;
+ ;
+ /** Helper for adding transform: translate(x0, y0) */
+ function translate(selection, x0, y0) {
+ selection.attr("transform", "translate(" + x0 + "," + y0 + ")");
+ }
+ scene.translate = translate;
+ ;
+ /**
+ * 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.
+ */
+ function positionRect(rect, cx, cy, width, height) {
+ rect.transition().attr({
+ x: cx - width / 2,
+ y: cy - height / 2,
+ width: width,
+ height: height
+ });
+ }
+ scene.positionRect = positionRect;
+ ;
+ /**
+ * 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.
+ */
+ function positionButton(button, renderNode) {
+ // Position the button in the top-right corner of the group node,
+ // with space given the draw the button inside of the corner.
+ var x = renderNode.x + renderNode.width / 2 - 6;
+ var 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 === graph.NodeType.SERIES && !renderNode.expanded) {
+ x += 10;
+ y -= 2;
+ }
+ var translateStr = "translate(" + x + "," + y + ")";
+ button.selectAll("path").transition().attr("transform", translateStr);
+ button.select("circle").transition().attr({
+ cx: x,
+ cy: y,
+ r: graph.layout.PARAMS.nodeSize.meta.expandButtonRadius
+ });
+ }
+ scene.positionButton = positionButton;
+ ;
+ /**
+ * 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.
+ */
+ function positionEllipse(ellipse, cx, cy, width, height) {
+ ellipse.transition().attr({
+ cx: cx,
+ cy: cy,
+ rx: width / 2,
+ ry: height / 2
+ });
+ }
+ scene.positionEllipse = positionEllipse;
+ ;
+ })(scene = graph.scene || (graph.scene = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+<script>/// <reference path="../graph.ts" />
+/// <reference path="../render.ts" />
+/// <reference path="scene.ts" />
+/// <reference path="edge.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph) {
+ var scene;
+ (function (scene) {
+ var annotation;
+ (function (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
+ */
+ function buildGroup(container, annotationData, d, sceneBehavior) {
+ // Select all children and join with data.
+ var 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, function (d) { return d.node.name; });
+ annotationGroups.enter()
+ .append("g")
+ .attr("data-name", function (a) { return a.node.name; })
+ .each(function (a) {
+ var aGroup = d3.select(this);
+ // Add annotation to the index in the scene
+ sceneBehavior.addAnnotationGroup(a, d, aGroup);
+ // Append annotation edge
+ var edgeType = scene.Class.Annotation.EDGE;
+ var metaedge = a.renderMetaedgeInfo && a.renderMetaedgeInfo.metaedge;
+ if (metaedge && !metaedge.numRegularEdges) {
+ edgeType += " " + scene.Class.Annotation.CONTROL_EDGE;
+ }
+ // If any edges are reference edges, add the reference edge class.
+ if (metaedge && metaedge.numRefEdges) {
+ edgeType += " " + scene.Class.Edge.REF_LINE;
+ }
+ scene.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, scene.Class.Annotation.ELLIPSIS);
+ }
+ });
+ annotationGroups
+ .attr("class", function (a) {
+ return scene.Class.Annotation.GROUP + " " +
+ annotationToClassName(a.annotationType) +
+ " " + scene.node.nodeClass(a);
+ })
+ .each(function (a) {
+ var 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) {
+ var aGroup = d3.select(this);
+ // Remove annotation from the index in the scene
+ sceneBehavior.removeAnnotationGroup(a, d, aGroup);
+ })
+ .remove();
+ return annotationGroups;
+ }
+ annotation.buildGroup = buildGroup;
+ ;
+ /**
+ * Maps an annotation enum to a class name used in css rules.
+ */
+ function annotationToClassName(annotationType) {
+ return (tf.graph.render.AnnotationType[annotationType] || "")
+ .toLowerCase() || null;
+ }
+ function buildShape(aGroup, a, sceneBehavior) {
+ if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) {
+ var image = scene.selectOrCreateChild(aGroup, "image");
+ image.attr({
+ "xlink:href": sceneBehavior.resolveUrl("../../lib/svg/summary-icon.svg"),
+ "height": "12px",
+ "width": "12px",
+ "cursor": "pointer"
+ });
+ }
+ else {
+ var shape = scene.node.buildShape(aGroup, a, scene.Class.Annotation.NODE);
+ // add title tag to get native tooltips
+ scene.selectOrCreateChild(shape, "title").text(a.node.name);
+ }
+ }
+ function addAnnotationLabelFromNode(aGroup, a) {
+ var namePath = a.node.name.split("/");
+ var text = namePath[namePath.length - 1];
+ var shortenedText = text.length > 8 ? text.substring(0, 8) + "..." : text;
+ return addAnnotationLabel(aGroup, shortenedText, a, null, text);
+ }
+ function addAnnotationLabel(aGroup, label, a, additionalClassNames, fullLabel) {
+ var classNames = scene.Class.Annotation.LABEL;
+ if (additionalClassNames) {
+ classNames += " " + additionalClassNames;
+ }
+ var 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, sceneBehavior) {
+ selection
+ .on("mouseover", function (a) {
+ sceneBehavior.fire("annotation-highlight", {
+ name: a.node.name,
+ hostName: d.node.name
+ });
+ })
+ .on("mouseout", function (a) {
+ sceneBehavior.fire("annotation-unhighlight", {
+ name: a.node.name,
+ hostName: d.node.name
+ });
+ })
+ .on("click", function (a) {
+ // Stop this event"s propagation so that it isn't also considered a
+ // graph-select.
+ 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, a, 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) {
+ scene.node.stylize(aGroup, a.renderNodeInfo, sceneBehavior, scene.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." + scene.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("." + scene.Class.Annotation.NODE + " ellipse"), d.x + a.dx, d.y + a.dy, a.width, a.height);
+ scene.positionRect(aGroup.select("." + scene.Class.Annotation.NODE + " rect"), d.x + a.dx, d.y + a.dy, a.width, a.height);
+ scene.positionRect(aGroup.select("." + scene.Class.Annotation.NODE + " use"), d.x + a.dx, d.y + a.dy, a.width, a.height);
+ // Edge position
+ aGroup.select("path." + scene.Class.Annotation.EDGE).transition().attr("d", function (a) {
+ // map relative position to absolute position
+ var points = a.points.map(function (p) {
+ return { x: p.dx + d.x, y: p.dy + d.y };
+ });
+ return scene.edge.interpolate(points);
+ });
+ }
+ ;
+ })(annotation = scene.annotation || (scene.annotation = {}));
+ })(scene = graph.scene || (graph.scene = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+<script>/// <reference path="../graph.ts" />
+/// <reference path="../render.ts" />
+/// <reference path="scene.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var scene;
+ (function (scene) {
+ var edge;
+ (function (edge) {
+ var Scene = tf.graph.scene; // Aliased
+ function getEdgeKey(edgeObj) {
+ return edgeObj.v + tf.graph.EDGE_KEY_DELIM + edgeObj.w;
+ }
+ edge.getEdgeKey = getEdgeKey;
+ /**
+ * 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
+ */
+ function buildGroup(sceneGroup, graph, sceneBehavior) {
+ var edgeData = _.reduce(graph.edges(), function (edges, edgeObj) {
+ var edgeLabel = graph.edge(edgeObj);
+ edges.push({
+ v: edgeObj.v,
+ w: edgeObj.w,
+ label: edgeLabel
+ });
+ return edges;
+ }, []);
+ var container = scene.selectOrCreateChild(sceneGroup, "g", scene.Class.Edge.CONTAINER);
+ var containerNode = container.node();
+ // Select all children and join with data.
+ // (Note that all children of g.edges are g.edge)
+ var 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", scene.Class.Edge.GROUP)
+ .attr("data-edge", getEdgeKey)
+ .each(function (d) {
+ var 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.
+ var extraEdgeClass = d.label.metaedge && d.label.metaedge.numRefEdges
+ ? scene.Class.Edge.REF_LINE + " " + scene.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(function (d) {
+ delete sceneBehavior._edgeGroupIndex[getEdgeKey(d)];
+ })
+ .remove();
+ return edgeGroups;
+ }
+ edge.buildGroup = buildGroup;
+ ;
+ /**
+ * 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.
+ */
+ function appendEdge(edgeGroup, d, sceneBehavior, edgeClass) {
+ edgeClass = edgeClass || scene.Class.Edge.LINE; // set default type
+ if (d.label && d.label.structural) {
+ edgeClass += " " + scene.Class.Edge.STRUCTURAL;
+ }
+ edgeGroup.append("path")
+ .attr("class", edgeClass);
+ }
+ edge.appendEdge = appendEdge;
+ ;
+ /**
+ * Returns a tween interpolator for the endpoint of an edge path.
+ */
+ function getEdgePathInterpolator(d, i, a) {
+ var renderMetaedgeInfo = d.label;
+ var adjoiningMetaedge = renderMetaedgeInfo.adjoiningMetaedge;
+ if (!adjoiningMetaedge) {
+ return d3.interpolate(a, edge.interpolate(renderMetaedgeInfo.points));
+ }
+ var renderPath = this;
+ // Get the adjoining path that matches the adjoining metaedge.
+ var adjoiningPath = (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).
+ var inbound = renderMetaedgeInfo.metaedge.inbound;
+ return function (t) {
+ var 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.
+ var points = renderMetaedgeInfo.points;
+ var index = inbound ? 0 : points.length - 1;
+ points[index].x = adjoiningPoint.x;
+ points[index].y = adjoiningPoint.y;
+ var dPath = edge.interpolate(points);
+ return dPath;
+ };
+ }
+ edge.interpolate = d3.svg.line()
+ .interpolate("basis")
+ .x(function (d) { return d.x; })
+ .y(function (d) { return d.y; });
+ function position(d) {
+ d3.select(this).select("path." + scene.Class.Edge.LINE)
+ .each(function (d) {
+ var 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) {
+ var a;
+ var metaedge = d.label.metaedge;
+ edgeGroup
+ .select("path." + scene.Class.Edge.LINE)
+ .classed("control-dep", metaedge && !metaedge.numRegularEdges);
+ }
+ ;
+ })(edge = scene.edge || (scene.edge = {}));
+ })(scene = graph_1.scene || (graph_1.scene = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+<script>/// <reference path="../graph.ts" />
+/// <reference path="scene.ts" />
+/// <reference path="annotation.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph) {
+ var scene;
+ (function (scene) {
+ var node;
+ (function (node_1) {
+ /**
+ * 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
+ */
+ function buildGroup(sceneGroup, nodeData, sceneBehavior) {
+ var container = scene.selectOrCreateChild(sceneGroup, "g", scene.Class.Node.CONTAINER);
+ // Select all children and join with data.
+ // (Note that all children of g.nodes are g.node)
+ var 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, function (d) {
+ // 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", function (d) { return d.node.name; })
+ .each(function (d) {
+ var nodeGroup = d3.select(this);
+ // index node group for quick stylizing
+ sceneBehavior.addNodeGroup(d.node.name, nodeGroup);
+ });
+ // UPDATE
+ nodeGroups
+ .attr("class", function (d) {
+ return scene.Class.Node.GROUP + " " + nodeClass(d);
+ })
+ .each(function (d) {
+ var nodeGroup = d3.select(this);
+ // add g.in-annotations (always add -- to keep layer order consistent.)
+ var inAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", scene.Class.Annotation.INBOX);
+ scene.annotation.buildGroup(inAnnotationBox, d.inAnnotations, d, sceneBehavior);
+ // add g.out-annotations (always add -- to keep layer order consistent.)
+ var outAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", scene.Class.Annotation.OUTBOX);
+ scene.annotation.buildGroup(outAnnotationBox, d.outAnnotations, d, sceneBehavior);
+ // label
+ var 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 === graph.NodeType.META);
+ // build .shape below label
+ var shape = buildShape(nodeGroup, d, scene.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);
+ var nodeGroup = d3.select(this);
+ if (d.inAnnotations.list.length > 0) {
+ nodeGroup.select("." + scene.Class.Annotation.INBOX)
+ .selectAll("." + scene.Class.Annotation.GROUP)
+ .each(function (a) {
+ sceneBehavior.removeAnnotationGroup(a, d);
+ });
+ }
+ if (d.outAnnotations.list.length > 0) {
+ nodeGroup.select("." + scene.Class.Annotation.OUTBOX)
+ .selectAll("." + scene.Class.Annotation.GROUP)
+ .each(function (a) {
+ sceneBehavior.removeAnnotationGroup(a, d);
+ });
+ }
+ })
+ .remove();
+ return nodeGroups;
+ }
+ node_1.buildGroup = buildGroup;
+ ;
+ /**
+ * 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, sceneBehavior) {
+ if (renderNodeInfo.node.isGroupNode) {
+ if (renderNodeInfo.expanded) {
+ // Recursively build the subscene.
+ return scene.buildGroup(nodeGroup, renderNodeInfo, sceneBehavior, scene.Class.Subscene.GROUP);
+ }
+ // Clean out existing subscene if the node is not expanded.
+ scene.selectChild(nodeGroup, "g", scene.Class.Subscene.GROUP).remove();
+ }
+ return null;
+ }
+ ;
+ /**
+ * Translate the subscene of the given node group
+ */
+ function subscenePosition(nodeGroup, d) {
+ var x0 = d.x - d.width / 2.0 + d.paddingLeft;
+ var y0 = d.y - d.height / 2.0 + d.paddingTop;
+ var subscene = scene.selectChild(nodeGroup, "g", scene.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, sceneBehavior) {
+ var group = scene.selectOrCreateChild(selection, "g", scene.Class.Node.BUTTON_CONTAINER);
+ scene.selectOrCreateChild(group, "circle", scene.Class.Node.BUTTON_CIRCLE);
+ scene.selectOrCreateChild(group, "path", scene.Class.Node.EXPAND_BUTTON).attr("d", "M0,-2.2 V2.2 M-2.2,0 H2.2");
+ scene.selectOrCreateChild(group, "path", scene.Class.Node.COLLAPSE_BUTTON).attr("d", "M-2.2,0 H2.2");
+ group.on("click", function (d) {
+ // Stop this event's propagation so that it isn't also considered a
+ // node-select.
+ 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, sceneBehavior, disableInteraction) {
+ if (disableInteraction) {
+ selection.attr("pointer-events", "none");
+ return;
+ }
+ selection.on("dblclick", function (d) {
+ sceneBehavior.fire("node-toggle-expand", { name: d.node.name });
+ })
+ .on("mouseover", function (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", function (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", function (d) {
+ // Stop this event's propagation so that it isn't also considered
+ // a graph-select.
+ 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, sceneBehavior) {
+ var namePath = renderNodeInfo.node.name.split("/");
+ var text = namePath[namePath.length - 1];
+ // Truncate long labels for unexpanded Metanodes.
+ var useFontScale = renderNodeInfo.node.type === graph.NodeType.META &&
+ !renderNodeInfo.expanded;
+ var label = scene.selectOrCreateChild(nodeGroup, "text", scene.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) + "...";
+ }
+ var 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.
+ */
+ var 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, yOffset) {
+ scene.selectChild(nodeGroup, "text", scene.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.
+ */
+ function buildShape(nodeGroup, d, nodeClass, before) {
+ // Create a group to house the underlying visual elements.
+ var shapeGroup = scene.selectOrCreateChild(nodeGroup, "g", nodeClass, before);
+ // TODO(jimbo): DOM structure should be templated in HTML somewhere, not JS.
+ switch (d.node.type) {
+ case graph.NodeType.OP:
+ scene.selectOrCreateChild(shapeGroup, "ellipse", scene.Class.Node.COLOR_TARGET);
+ break;
+ case graph.NodeType.SERIES:
+ // Choose the correct stamp to use to represent this series.
+ var stampType = "annotation";
+ var groupNodeInfo = d;
+ if (groupNodeInfo.coreGraph) {
+ stampType = groupNodeInfo.node.hasNonControlEdges
+ ? "vertical" : "horizontal";
+ }
+ scene.selectOrCreateChild(shapeGroup, "use", scene.Class.Node.COLOR_TARGET)
+ .attr("xlink:href", "#op-series-" + stampType + "-stamp");
+ scene.selectOrCreateChild(shapeGroup, "rect", scene.Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ case graph.NodeType.BRIDGE:
+ scene.selectOrCreateChild(shapeGroup, "rect", scene.Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ case graph.NodeType.META:
+ scene.selectOrCreateChild(shapeGroup, "rect", scene.Class.Node.COLOR_TARGET)
+ .attr({ rx: d.radius, ry: d.radius });
+ break;
+ default:
+ throw Error("Unrecognized node type: " + d.node.type);
+ }
+ return shapeGroup;
+ }
+ node_1.buildShape = buildShape;
+ ;
+ function nodeClass(d) {
+ switch (d.node.type) {
+ case graph.NodeType.OP:
+ return scene.Class.OPNODE;
+ case graph.NodeType.META:
+ return scene.Class.METANODE;
+ case graph.NodeType.SERIES:
+ return scene.Class.SERIESNODE;
+ case graph.NodeType.BRIDGE:
+ return scene.Class.BRIDGENODE;
+ case graph.NodeType.ELLIPSIS:
+ return scene.Class.ELLIPSISNODE;
+ }
+ ;
+ throw Error("Unrecognized node type: " + d.node.type);
+ }
+ node_1.nodeClass = nodeClass;
+ ;
+ /** Modify node and its subscene and its label's positional attributes */
+ function position(nodeGroup, d, sceneBehavior) {
+ var shapeGroup = scene.selectChild(nodeGroup, "g", scene.Class.Node.SHAPE);
+ switch (d.node.type) {
+ case graph.NodeType.OP: {
+ // position shape
+ var shape = scene.selectChild(shapeGroup, "ellipse");
+ scene.positionEllipse(shape, d.x, d.y, d.width, d.height);
+ labelPosition(nodeGroup, d, d.labelOffset);
+ break;
+ }
+ case graph.NodeType.META: {
+ // position shape
+ var 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 graph.NodeType.SERIES: {
+ var 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 graph.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.
+ var 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 */
+ var 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, isExpanded) {
+ var colorParams = tf.graph.render.MetanodeColors;
+ switch (colorBy) {
+ case ColorBy.STRUCTURE:
+ if (renderInfo.node.type === tf.graph.NodeType.META) {
+ var tid = 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 === graph.NodeType.BRIDGE) {
+ return renderInfo.structural ? "#f0e" :
+ 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;
+ }
+ var id = renderInfo.node.name;
+ var escapedId = tf.escapeQuerySelector(id);
+ var gradientDefs = d3.select("svg#svg defs #linearGradients");
+ var 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();
+ var cumulativeProportion = 0;
+ // For each device, create a stop using the proportion of that device.
+ _.each(renderInfo.deviceColors, function (d) {
+ var 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).
+ */
+ function stylize(nodeGroup, renderInfo, sceneBehavior, nodeClass) {
+ nodeClass = nodeClass || scene.Class.Node.SHAPE;
+ var isHighlighted = sceneBehavior.isNodeHighlighted(renderInfo.node.name);
+ var isSelected = sceneBehavior.isNodeSelected(renderInfo.node.name);
+ var isExtract = renderInfo.isInExtract || renderInfo.isOutExtract;
+ var 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.
+ var node = nodeGroup.select("." + nodeClass + " ." + scene.Class.Node.COLOR_TARGET);
+ var 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.
+ var outlineColor = fillColor.substring(0, 3) === "url" ?
+ tf.graph.render.MetanodeColors.GRADIENT_OUTLINE :
+ d3.rgb(fillColor).darker().toString();
+ node.style("stroke", outlineColor);
+ }
+ }
+ node_1.stylize = stylize;
+ ;
+ })(node = scene.node || (scene.node = {}));
+ })(scene = graph.scene || (graph.scene = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+<script>/// <reference path="graph.ts" />
+/// <reference path="render.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var layout;
+ (function (layout) {
+ /** Set of parameters that define the look and feel of the graph. */
+ layout.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,
+ 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,
+ 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. */
+ function scene(renderNodeInfo) {
+ // 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 === graph_1.NodeType.META) {
+ layoutMetanode(renderNodeInfo);
+ }
+ else if (renderNodeInfo.node.type === graph_1.NodeType.SERIES) {
+ layoutSeriesNode(renderNodeInfo);
+ }
+ }
+ layout.scene = scene;
+ ;
+ /**
+ * Update layout, size, and annotations of its children nodes and edges.
+ */
+ function layoutChildren(renderNodeInfo) {
+ var children = renderNodeInfo.coreGraph.nodes().map(function (n) {
+ return renderNodeInfo.coreGraph.node(n);
+ }).concat(renderNodeInfo.isolatedInExtract, renderNodeInfo.isolatedOutExtract);
+ _.each(children, function (childNodeInfo) {
+ // Set size of each child
+ switch (childNodeInfo.node.type) {
+ case graph_1.NodeType.OP:
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.op);
+ break;
+ case graph_1.NodeType.BRIDGE:
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.bridge);
+ break;
+ case graph_1.NodeType.META:
+ if (!childNodeInfo.expanded) {
+ // set fixed width and scalable height based on cardinality
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.meta);
+ childNodeInfo.height =
+ layout.PARAMS.nodeSize.meta.height(childNodeInfo.node.cardinality);
+ }
+ else {
+ var childGroupNodeInfo = childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ }
+ break;
+ case graph_1.NodeType.SERIES:
+ if (childNodeInfo.expanded) {
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.series.expanded);
+ var childGroupNodeInfo = childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ }
+ else {
+ var childGroupNodeInfo = childNodeInfo;
+ var seriesParams = childGroupNodeInfo.node.hasNonControlEdges ?
+ layout.PARAMS.nodeSize.series.vertical :
+ layout.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, params) {
+ _.extend(graph.graph(), {
+ nodeSep: params.nodeSep,
+ rankSep: params.rankSep
+ });
+ var bridgeNodeNames = [];
+ var nonBridgeNodeNames = [];
+ // Split out nodes into bridge and non-bridge nodes, and calculate the total
+ // width we should use for bridge nodes.
+ _.each(graph.nodes(), function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ if (nodeInfo.node.type === graph_1.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);
+ var 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.
+ var minX = Infinity;
+ var minY = Infinity;
+ var maxX = -Infinity;
+ var maxY = -Infinity;
+ _.each(nonBridgeNodeNames, function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ var w = 0.5 * nodeInfo.width;
+ var x1 = nodeInfo.x - w - nodeInfo.inboxWidth;
+ var x2 = nodeInfo.x + w + nodeInfo.outboxWidth;
+ minX = x1 < minX ? x1 : minX;
+ maxX = x2 > maxX ? x2 : maxX;
+ var labelLength = nodeName.length - nodeName.lastIndexOf(graph_1.NAMESPACE_DELIM);
+ // TODO(jimbo): Account for font width rather than using a magic number.
+ var charWidth = 3; // 3 pixels per character.
+ var lw = 0.5 * labelLength * charWidth;
+ var lx1 = nodeInfo.x - lw;
+ var 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.
+ var h = 0.5 * nodeInfo.outerHeight;
+ var y1 = nodeInfo.y - h;
+ var y2 = nodeInfo.y + h;
+ minY = y1 < minY ? y1 : minY;
+ maxY = y2 > maxY ? y2 : maxY;
+ });
+ _.each(graph.edges(), function (edgeObj) {
+ var renderMetaedgeInfo = graph.edge(edgeObj);
+ if (renderMetaedgeInfo.structural) {
+ return; // Skip structural edges from min/max calculations.
+ }
+ _.each(renderMetaedgeInfo.points, function (point) {
+ 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(), function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ nodeInfo.x -= minX;
+ nodeInfo.y -= minY;
+ });
+ _.each(graph.edges(), function (edgeObj) {
+ _.each(graph.edge(edgeObj).points, function (point) {
+ point.x -= minX;
+ point.y -= minY;
+ });
+ });
+ return {
+ width: maxX - minX,
+ height: maxY - minY,
+ };
+ }
+ /** Layout a metanode. */
+ function layoutMetanode(renderNodeInfo) {
+ // First, copy params specific to meta nodes onto this render info object.
+ var params = layout.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, layout.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.
+ var hasInExtract = renderNodeInfo.isolatedInExtract.length > 0;
+ renderNodeInfo.inExtractBox.width = hasInExtract ?
+ _(renderNodeInfo.isolatedInExtract).pluck("outerWidth").max() : 0;
+ renderNodeInfo.inExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedInExtract, function (height, child, i) {
+ var 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.
+ var hasOutExtract = renderNodeInfo.isolatedOutExtract.length > 0;
+ renderNodeInfo.outExtractBox.width = hasOutExtract ?
+ _(renderNodeInfo.isolatedOutExtract).pluck("outerWidth").max() : 0;
+ renderNodeInfo.outExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedOutExtract, function (height, child, i) {
+ var 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) {
+ var graph = node.coreGraph;
+ var params = layout.PARAMS.subscene.series;
+ _.extend(node, params);
+ // Layout the core.
+ _.extend(node.coreBox, dagreLayout(node.coreGraph, layout.PARAMS.graph.series));
+ _.each(graph.nodes(), function (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) {
+ // 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;
+ }
+ var inAnnotations = renderNodeInfo.inAnnotations.list;
+ var outAnnotations = renderNodeInfo.outAnnotations.list;
+ // Calculate size for in-annotations
+ _.each(inAnnotations, function (a) { return sizeAnnotation(a); });
+ // Calculate size for out-annotations
+ _.each(outAnnotations, function (a) { return sizeAnnotation(a); });
+ var params = layout.PARAMS.annotations;
+ renderNodeInfo.inboxWidth =
+ inAnnotations.length > 0 ?
+ _(inAnnotations).pluck("width").max() +
+ params.xOffset + params.labelWidth + params.labelOffset :
+ 0;
+ renderNodeInfo.outboxWidth =
+ outAnnotations.length > 0 ?
+ _(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)
+ var inboxHeight = _.reduce(inAnnotations, function (height, a, i) {
+ var 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, function (a) {
+ 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)
+ var outboxHeight = _.reduce(outAnnotations, function (height, a, i) {
+ var 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, function (a) {
+ // 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.
+ var inTouchHeight = Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, inboxHeight / 2);
+ inTouchHeight = inTouchHeight < 0 ? 0 : inTouchHeight;
+ var inY = d3.scale.linear()
+ .domain([0, inAnnotations.length - 1])
+ .range([-inTouchHeight, inTouchHeight]);
+ // Calculate annotation edge position
+ _.each(inAnnotations, function (a, 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.
+ var outTouchHeight = Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, outboxHeight / 2);
+ outTouchHeight = outTouchHeight < 0 ? 0 : outTouchHeight;
+ var outY = d3.scale.linear()
+ .domain([0, outAnnotations.length - 1])
+ .range([-outTouchHeight, outTouchHeight]);
+ _.each(outAnnotations, function (a, 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) {
+ switch (a.annotationType) {
+ case graph_1.render.AnnotationType.CONSTANT:
+ _.extend(a, layout.PARAMS.constant.size);
+ break;
+ case graph_1.render.AnnotationType.SHORTCUT:
+ if (a.node.type === graph_1.NodeType.OP) {
+ _.extend(a, layout.PARAMS.shortcutSize.op);
+ }
+ else if (a.node.type === graph_1.NodeType.META) {
+ _.extend(a, layout.PARAMS.shortcutSize.meta);
+ }
+ else if (a.node.type === graph_1.NodeType.SERIES) {
+ _.extend(a, layout.PARAMS.shortcutSize.series);
+ }
+ else {
+ throw Error("Invalid node type: " + a.node.type);
+ }
+ break;
+ case graph_1.render.AnnotationType.SUMMARY:
+ _.extend(a, layout.PARAMS.constant.size);
+ break;
+ }
+ }
+ })(layout = graph_1.layout || (graph_1.layout = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+<script>var tf;
+(function (tf) {
+ /**
+ * Mapping from color palette name to color pallette, which contains
+ * exact colors for multiple states of a single color pallette.
+ */
+ tf.COLORS = [
+ {
+ "name": "Google Blue",
+ "color": "#4184f3",
+ "active": "#3a53c5",
+ "disabled": "#cad8fc"
+ },
+ {
+ "name": "Google Red",
+ "color": "#db4437",
+ "active": "#8f2a0c",
+ "disabled": "#e8c6c1"
+ },
+ {
+ "name": "Google Yellow",
+ "color": "#f4b400",
+ "active": "#db9200",
+ "disabled": "#f7e8b0"
+ },
+ {
+ "name": "Google Green",
+ "color": "#0f9d58",
+ "active": "#488046",
+ "disabled": "#c2e1cc"
+ },
+ {
+ "name": "Purple",
+ "color": "#aa46bb",
+ "active": "#5c1398",
+ "disabled": "#d7bce6"
+ },
+ {
+ "name": "Teal",
+ "color": "#00abc0",
+ "active": "#47828e",
+ "disabled": "#c2eaf2"
+ },
+ {
+ "name": "Deep Orange",
+ "color": "#ff6f42",
+ "active": "#ca4a06",
+ "disabled": "#f2cbba"
+ },
+ {
+ "name": "Lime",
+ "color": "#9d9c23",
+ "active": "#7f771d",
+ "disabled": "#f1f4c2"
+ },
+ {
+ "name": "Indigo",
+ "color": "#5b6abf",
+ "active": "#3e47a9",
+ "disabled": "#c5c8e8"
+ },
+ {
+ "name": "Pink",
+ "color": "#ef6191",
+ "active": "#ca1c60",
+ "disabled": "#e9b9ce"
+ },
+ {
+ "name": "Deep Teal",
+ "color": "#00786a",
+ "active": "#2b4f43",
+ "disabled": "#bededa"
+ },
+ {
+ "name": "Deep Pink",
+ "color": "#c1175a",
+ "active": "#75084f",
+ "disabled": "#de8cae"
+ },
+ {
+ "name": "Gray",
+ "color": "#9E9E9E",
+ "active": "#424242",
+ "disabled": "F5F5F5" // 100
+ }
+ ].reduce(function (m, c) {
+ m[c.name] = c;
+ return m;
+ }, {});
+ /**
+ * Mapping from op category to color palette name
+ * e.g., OP_GROUP_COLORS["state_ops"] = "Google Blue";
+ */
+ tf.OP_GROUP_COLORS = [
+ {
+ color: "Google Red",
+ groups: ["gen_legacy_ops", "legacy_ops", "legacy_flogs_input",
+ "legacy_image_input", "legacy_input_example_input",
+ "legacy_sequence_input", "legacy_seti_input_input"]
+ }, {
+ color: "Deep Orange",
+ groups: ["constant_ops"]
+ }, {
+ color: "Indigo",
+ groups: ["state_ops"]
+ }, {
+ color: "Purple",
+ groups: ["nn_ops", "nn"]
+ }, {
+ color: "Google Green",
+ groups: ["math_ops"]
+ }, {
+ color: "Lime",
+ groups: ["array_ops"]
+ }, {
+ color: "Teal",
+ groups: ["control_flow_ops", "data_flow_ops"]
+ }, {
+ color: "Pink",
+ groups: ["summary_ops"]
+ }, {
+ color: "Deep Pink",
+ groups: ["io_ops"]
+ }
+ ].reduce(function (m, c) {
+ c.groups.forEach(function (group) {
+ m[group] = c.color;
+ });
+ return m;
+ }, {});
+})(tf || (tf = {}));
+</script>
+<script>/// <reference path="../../../../typings/tsd.d.ts" />
+/// <reference path="../common.ts" />
+var tf;
+(function (tf) {
+ var scene;
+ (function (scene) {
+ /** Show minimap when the viewpoint area is less than X% of the whole area. */
+ var FRAC_VIEWPOINT_AREA = 0.8;
+ var Minimap = (function () {
+ /**
+ * 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.
+ */
+ function Minimap(svg, zoomG, mainZoom, minimap, maxWandH, labelPadding) {
+ var _this = this;
+ this.svg = svg;
+ this.labelPadding = labelPadding;
+ this.zoomG = zoomG;
+ this.mainZoom = mainZoom;
+ this.maxWandH = maxWandH;
+ var $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.
+ var $minimapSvg = $minimap.select("svg");
+ // Make the viewpoint rectangle draggable.
+ var $viewpoint = $minimapSvg.select("rect");
+ var dragmove = function (d) {
+ _this.viewpointCoord.x = d3.event.x;
+ _this.viewpointCoord.y = d3.event.y;
+ _this.updateViewpoint();
+ };
+ this.viewpointCoord = { x: 0, y: 0 };
+ var drag = d3.behavior.drag().origin(Object).on("drag", dragmove);
+ $viewpoint.datum(this.viewpointCoord).call(drag);
+ // Make the minimap clickable.
+ $minimapSvg.on("click", function () {
+ if (d3.event.defaultPrevented) {
+ // This click was part of a drag event, so suppress it.
+ return;
+ }
+ // Update the coordinates of the viewpoint.
+ var width = Number($viewpoint.attr("width"));
+ var height = Number($viewpoint.attr("height"));
+ var clickCoords = d3.mouse($minimapSvg.node());
+ _this.viewpointCoord.x = clickCoords[0] - width / 2;
+ _this.viewpointCoord.y = clickCoords[1] - height / 2;
+ _this.updateViewpoint();
+ });
+ this.viewpoint = $viewpoint.node();
+ this.minimapSvg = $minimapSvg.node();
+ this.minimap = minimap;
+ this.canvas = $minimap.select("canvas.first").node();
+ this.canvasBuffer =
+ $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.
+ */
+ Minimap.prototype.updateViewpoint = function () {
+ // 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.
+ var mainX = -this.viewpointCoord.x * this.scaleMain / this.scaleMinimap;
+ var mainY = -this.viewpointCoord.y * this.scaleMain / this.scaleMinimap;
+ var 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).
+ */
+ Minimap.prototype.update = function () {
+ var _this = this;
+ var $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.
+ var stylesText = "";
+ for (var k = 0; k < document.styleSheets.length; k++) {
+ try {
+ var cssRules = document.styleSheets[k].cssRules ||
+ document.styleSheets[k].rules;
+ if (cssRules == null) {
+ continue;
+ }
+ for (var 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.
+ var 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.
+ var $zoomG = d3.select(this.zoomG);
+ var zoomTransform = $zoomG.attr("transform");
+ $zoomG.attr("transform", null);
+ // Get the size of the entire scene.
+ var 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(this.minimapSize);
+ d3.select(this.canvasBuffer).attr(this.minimapSize);
+ if (this.translate != null && this.zoom != null) {
+ // Update the viewpoint rectangle shape since the aspect ratio of the
+ // map has changed.
+ requestAnimationFrame(function () { return _this.zoom(); });
+ }
+ // Serialize the main svg to a string which will be used as the rendering
+ // content for the canvas.
+ var 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);
+ var image = new Image();
+ image.onload = function () {
+ // Draw the svg content onto the buffer canvas.
+ var 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(function () {
+ // 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.
+ _a = [_this.canvasBuffer, _this.canvas], _this.canvas = _a[0], _this.canvasBuffer = _a[1];
+ var _a;
+ });
+ };
+ 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.
+ */
+ Minimap.prototype.zoom = function (translate, scale) {
+ // 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.
+ var svgRect = this.svg.getBoundingClientRect();
+ var $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;
+ var viewpointWidth = svgRect.width * this.scaleMinimap / this.scaleMain;
+ var 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.
+ var mapWidth = this.minimapSize.width;
+ var mapHeight = this.minimapSize.height;
+ var x = this.viewpointCoord.x;
+ var y = this.viewpointCoord.y;
+ var w = Math.min(Math.max(0, x + viewpointWidth), mapWidth) -
+ Math.min(Math.max(0, x), mapWidth);
+ var h = Math.min(Math.max(0, y + viewpointHeight), mapHeight) -
+ Math.min(Math.max(0, y), mapHeight);
+ var fracIntersect = (w * h) / (mapWidth * mapHeight);
+ if (fracIntersect < FRAC_VIEWPOINT_AREA) {
+ this.minimap.classList.remove("hidden");
+ }
+ else {
+ this.minimap.classList.add("hidden");
+ }
+ };
+ return Minimap;
+ })();
+ scene.Minimap = Minimap;
+ })(scene = tf.scene || (tf.scene = {}));
+})(tf || (tf = {})); // close module tf.scene
+</script>
+<script>/// <reference path="graph.ts" />
+/// <reference path="render.ts" />
+var tf;
+(function (tf) {
+ var graph;
+ (function (graph_1) {
+ var layout;
+ (function (layout) {
+ /** Set of parameters that define the look and feel of the graph. */
+ layout.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,
+ 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,
+ 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. */
+ function scene(renderNodeInfo) {
+ // 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 === graph_1.NodeType.META) {
+ layoutMetanode(renderNodeInfo);
+ }
+ else if (renderNodeInfo.node.type === graph_1.NodeType.SERIES) {
+ layoutSeriesNode(renderNodeInfo);
+ }
+ }
+ layout.scene = scene;
+ ;
+ /**
+ * Update layout, size, and annotations of its children nodes and edges.
+ */
+ function layoutChildren(renderNodeInfo) {
+ var children = renderNodeInfo.coreGraph.nodes().map(function (n) {
+ return renderNodeInfo.coreGraph.node(n);
+ }).concat(renderNodeInfo.isolatedInExtract, renderNodeInfo.isolatedOutExtract);
+ _.each(children, function (childNodeInfo) {
+ // Set size of each child
+ switch (childNodeInfo.node.type) {
+ case graph_1.NodeType.OP:
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.op);
+ break;
+ case graph_1.NodeType.BRIDGE:
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.bridge);
+ break;
+ case graph_1.NodeType.META:
+ if (!childNodeInfo.expanded) {
+ // set fixed width and scalable height based on cardinality
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.meta);
+ childNodeInfo.height =
+ layout.PARAMS.nodeSize.meta.height(childNodeInfo.node.cardinality);
+ }
+ else {
+ var childGroupNodeInfo = childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ }
+ break;
+ case graph_1.NodeType.SERIES:
+ if (childNodeInfo.expanded) {
+ _.extend(childNodeInfo, layout.PARAMS.nodeSize.series.expanded);
+ var childGroupNodeInfo = childNodeInfo;
+ scene(childGroupNodeInfo); // Recursively layout its subscene.
+ }
+ else {
+ var childGroupNodeInfo = childNodeInfo;
+ var seriesParams = childGroupNodeInfo.node.hasNonControlEdges ?
+ layout.PARAMS.nodeSize.series.vertical :
+ layout.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, params) {
+ _.extend(graph.graph(), {
+ nodeSep: params.nodeSep,
+ rankSep: params.rankSep
+ });
+ var bridgeNodeNames = [];
+ var nonBridgeNodeNames = [];
+ // Split out nodes into bridge and non-bridge nodes, and calculate the total
+ // width we should use for bridge nodes.
+ _.each(graph.nodes(), function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ if (nodeInfo.node.type === graph_1.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);
+ var 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.
+ var minX = Infinity;
+ var minY = Infinity;
+ var maxX = -Infinity;
+ var maxY = -Infinity;
+ _.each(nonBridgeNodeNames, function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ var w = 0.5 * nodeInfo.width;
+ var x1 = nodeInfo.x - w - nodeInfo.inboxWidth;
+ var x2 = nodeInfo.x + w + nodeInfo.outboxWidth;
+ minX = x1 < minX ? x1 : minX;
+ maxX = x2 > maxX ? x2 : maxX;
+ var labelLength = nodeName.length - nodeName.lastIndexOf(graph_1.NAMESPACE_DELIM);
+ // TODO(jimbo): Account for font width rather than using a magic number.
+ var charWidth = 3; // 3 pixels per character.
+ var lw = 0.5 * labelLength * charWidth;
+ var lx1 = nodeInfo.x - lw;
+ var 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.
+ var h = 0.5 * nodeInfo.outerHeight;
+ var y1 = nodeInfo.y - h;
+ var y2 = nodeInfo.y + h;
+ minY = y1 < minY ? y1 : minY;
+ maxY = y2 > maxY ? y2 : maxY;
+ });
+ _.each(graph.edges(), function (edgeObj) {
+ var renderMetaedgeInfo = graph.edge(edgeObj);
+ if (renderMetaedgeInfo.structural) {
+ return; // Skip structural edges from min/max calculations.
+ }
+ _.each(renderMetaedgeInfo.points, function (point) {
+ 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(), function (nodeName) {
+ var nodeInfo = graph.node(nodeName);
+ nodeInfo.x -= minX;
+ nodeInfo.y -= minY;
+ });
+ _.each(graph.edges(), function (edgeObj) {
+ _.each(graph.edge(edgeObj).points, function (point) {
+ point.x -= minX;
+ point.y -= minY;
+ });
+ });
+ return {
+ width: maxX - minX,
+ height: maxY - minY,
+ };
+ }
+ /** Layout a metanode. */
+ function layoutMetanode(renderNodeInfo) {
+ // First, copy params specific to meta nodes onto this render info object.
+ var params = layout.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, layout.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.
+ var hasInExtract = renderNodeInfo.isolatedInExtract.length > 0;
+ renderNodeInfo.inExtractBox.width = hasInExtract ?
+ _(renderNodeInfo.isolatedInExtract).pluck("outerWidth").max() : 0;
+ renderNodeInfo.inExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedInExtract, function (height, child, i) {
+ var 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.
+ var hasOutExtract = renderNodeInfo.isolatedOutExtract.length > 0;
+ renderNodeInfo.outExtractBox.width = hasOutExtract ?
+ _(renderNodeInfo.isolatedOutExtract).pluck("outerWidth").max() : 0;
+ renderNodeInfo.outExtractBox.height =
+ _.reduce(renderNodeInfo.isolatedOutExtract, function (height, child, i) {
+ var 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) {
+ var graph = node.coreGraph;
+ var params = layout.PARAMS.subscene.series;
+ _.extend(node, params);
+ // Layout the core.
+ _.extend(node.coreBox, dagreLayout(node.coreGraph, layout.PARAMS.graph.series));
+ _.each(graph.nodes(), function (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) {
+ // 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;
+ }
+ var inAnnotations = renderNodeInfo.inAnnotations.list;
+ var outAnnotations = renderNodeInfo.outAnnotations.list;
+ // Calculate size for in-annotations
+ _.each(inAnnotations, function (a) { return sizeAnnotation(a); });
+ // Calculate size for out-annotations
+ _.each(outAnnotations, function (a) { return sizeAnnotation(a); });
+ var params = layout.PARAMS.annotations;
+ renderNodeInfo.inboxWidth =
+ inAnnotations.length > 0 ?
+ _(inAnnotations).pluck("width").max() +
+ params.xOffset + params.labelWidth + params.labelOffset :
+ 0;
+ renderNodeInfo.outboxWidth =
+ outAnnotations.length > 0 ?
+ _(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)
+ var inboxHeight = _.reduce(inAnnotations, function (height, a, i) {
+ var 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, function (a) {
+ 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)
+ var outboxHeight = _.reduce(outAnnotations, function (height, a, i) {
+ var 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, function (a) {
+ // 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.
+ var inTouchHeight = Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, inboxHeight / 2);
+ inTouchHeight = inTouchHeight < 0 ? 0 : inTouchHeight;
+ var inY = d3.scale.linear()
+ .domain([0, inAnnotations.length - 1])
+ .range([-inTouchHeight, inTouchHeight]);
+ // Calculate annotation edge position
+ _.each(inAnnotations, function (a, 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.
+ var outTouchHeight = Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, outboxHeight / 2);
+ outTouchHeight = outTouchHeight < 0 ? 0 : outTouchHeight;
+ var outY = d3.scale.linear()
+ .domain([0, outAnnotations.length - 1])
+ .range([-outTouchHeight, outTouchHeight]);
+ _.each(outAnnotations, function (a, 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) {
+ switch (a.annotationType) {
+ case graph_1.render.AnnotationType.CONSTANT:
+ _.extend(a, layout.PARAMS.constant.size);
+ break;
+ case graph_1.render.AnnotationType.SHORTCUT:
+ if (a.node.type === graph_1.NodeType.OP) {
+ _.extend(a, layout.PARAMS.shortcutSize.op);
+ }
+ else if (a.node.type === graph_1.NodeType.META) {
+ _.extend(a, layout.PARAMS.shortcutSize.meta);
+ }
+ else if (a.node.type === graph_1.NodeType.SERIES) {
+ _.extend(a, layout.PARAMS.shortcutSize.series);
+ }
+ else {
+ throw Error("Invalid node type: " + a.node.type);
+ }
+ break;
+ case graph_1.render.AnnotationType.SUMMARY:
+ _.extend(a, layout.PARAMS.constant.size);
+ break;
+ }
+ }
+ })(layout = graph_1.layout || (graph_1.layout = {}));
+ })(graph = tf.graph || (tf.graph = {}));
+})(tf || (tf = {})); // close module
+</script>
+
+
+
+
+
+
+
+</head><body><div hidden="" by-vulcanize=""><dom-module id="tf-data-coordinator" assetpath="../components/tf-event-dashboard/">
+ <script>/// <reference path="../../typings/tsd.d.ts" />
+/// <reference path="../../bower_components/plottable/plottable.d.ts" />
+var TF;
+(function (TF) {
+ /* The DataCoordinator generates TF.Datasets for each run/tag combination,
+ * and is responsible for communicating with the backend to load data into them.
+ * A key fact about this design is that when Datasets modify their data, they
+ * automatically notify all dependent Plottable charts.
+ */
+ var DataCoordinator = (function () {
+ function DataCoordinator(urlGenerator, runToTag) {
+ this.datasets = {};
+ this.urlGenerator = urlGenerator;
+ this.runToTag = runToTag;
+ }
+ /* Create or return an array of Datasets for the given
+ * tag and runs. It filters which runs it uses by checking
+ * that data exists for each tag-run combination.
+ * Calling this triggers a load on the dataset.
+ */
+ DataCoordinator.prototype.getDatasets = function (tag, runs) {
+ var _this = this;
+ var usableRuns = runs.filter(function (r) {
+ var tags = _this.runToTag[r];
+ return tags.indexOf(tag) !== -1;
+ });
+ return usableRuns.map(function (r) { return _this.getDataset(tag, r); });
+ };
+ /* Create or return a Dataset for given tag and run.
+ * Calling this triggers a load on the dataset.
+ */
+ DataCoordinator.prototype.getDataset = function (tag, run) {
+ var dataset = this._getDataset(tag, run);
+ dataset.load();
+ return dataset;
+ };
+ DataCoordinator.prototype._getDataset = function (tag, run) {
+ var key = [tag, run].toString();
+ var dataset;
+ if (this.datasets[key] != null) {
+ dataset = this.datasets[key];
+ }
+ else {
+ dataset = new TF.Dataset(tag, run, this.urlGenerator);
+ this.datasets[key] = dataset;
+ }
+ return dataset;
+ };
+ return DataCoordinator;
+ })();
+ TF.DataCoordinator = DataCoordinator;
+})(TF || (TF = {}));
+</script>
+ <script>/// <reference path="../../typings/tsd.d.ts" />
+/// <reference path="../../bower_components/plottable/plottable.d.ts" />
+var __extends = (this && this.__extends) || function (d, b) {
+ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ __.prototype = b.prototype;
+ d.prototype = new __();
+};
+var TF;
+(function (TF) {
+ /* An extension of Plottable.Dataset that knows how to load data from a backend.
+ */
+ var Dataset = (function (_super) {
+ __extends(Dataset, _super);
+ function Dataset(tag, run, urlGenerator) {
+ _super.call(this, [], { tag: tag, run: run });
+ this.load = _.debounce(this._load, 10);
+ this.tag = tag;
+ this.run = run;
+ this.urlGenerator = urlGenerator;
+ }
+ Dataset.prototype._load = function () {
+ var _this = this;
+ var url = this.urlGenerator(this.tag, this.run);
+ if (this.lastRequest != null) {
+ this.lastRequest.abort();
+ }
+ this.lastRequest = d3.json(url, function (error, json) {
+ _this.lastRequest = null;
+ if (error) {
+ /* tslint:disable */
+ console.log(error);
+ /* tslint:enable */
+ throw new Error("Failure loading JSON at url: \"" + url + "\"");
+ }
+ else {
+ _this.data(json);
+ }
+ });
+ };
+ return Dataset;
+ })(Plottable.Dataset);
+ TF.Dataset = Dataset;
+})(TF || (TF = {}));
+</script>
+ <script>
+ Polymer({
+ is: "tf-data-coordinator",
+ properties: {
+ urlGenerator: Object,
+ outDataCoordinator: {
+ type: Object,
+ computed: "getCoordinator(urlGenerator, runToTag)",
+ notify: true,
+ },
+ },
+ getCoordinator: function(generator, runToTag) {
+ return new TF.DataCoordinator(generator, runToTag);
+ }
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-tooltip-coordinator" assetpath="../components/tf-event-dashboard/">
+ <script>
+ Polymer({
+ is: "tf-tooltip-coordinator",
+ properties: {
+ outTooltipUpdater: {
+ type: Function,
+ value: function() {
+ return (function(tooltipMap, xValue, closestRun) {
+ this._setOutTooltipMap(tooltipMap);
+ this._setOutXValue(xValue);
+ this._setOutClosestRun(closestRun);
+ }).bind(this);
+ },
+ notify: true,
+ readOnly: true,
+ },
+ outTooltipMap: {
+ // a {runName: tooltipValue} map, where runName and tooltipValue are strings.
+ type: Object,
+ notify: true,
+ readOnly: true,
+ },
+ outXValue: {
+ // a string representation of the closest x value for the tooltips
+ type: Number,
+ notify: true,
+ readOnly: true,
+ },
+ outClosestRun: {
+ // the name of the run that is closest to the user cursor (if any)
+ type: String,
+ notify: true,
+ readOnly: true,
+ },
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="scrollbar-style" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <style>
+ .scrollbar::-webkit-scrollbar-track
+ {
+ visibility: hidden;
+ }
+
+ .scrollbar::-webkit-scrollbar
+ {
+ width: 10px;
+ }
+
+ .scrollbar::-webkit-scrollbar-thumb
+ {
+ border-radius: 10px;
+ -webkit-box-shadow: inset 0 0 2px rgba(0,0,0,.3);
+ background-color: var(--paper-grey-500);
+ color: var(--paper-grey-900);
+ }
+ .scrollbar {
+ box-sizing: border-box;
+ }
+ </style>
+ </template>
+</dom-module>
+<dom-module id="run-color-style" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <style>
+ [color-class="light-blue"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-light-blue-500);
+ --paper-checkbox-checked-ink-color: var(--paper-light-blue-500);
+ --paper-checkbox-unchecked-color: var(--paper-light-blue-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-light-blue-900);
+ }
+ [color-class="red"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-red-500);
+ --paper-checkbox-checked-ink-color: var(--paper-red-500);
+ --paper-checkbox-unchecked-color: var(--paper-red-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-red-900);
+ }
+ [color-class="green"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-green-500);
+ --paper-checkbox-checked-ink-color: var(--paper-green-500);
+ --paper-checkbox-unchecked-color: var(--paper-green-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-green-900);
+ }
+ [color-class="purple"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-purple-500);
+ --paper-checkbox-checked-ink-color: var(--paper-purple-500);
+ --paper-checkbox-unchecked-color: var(--paper-purple-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-purple-900);
+ }
+ [color-class="teal"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-teal-500);
+ --paper-checkbox-checked-ink-color: var(--paper-teal-500);
+ --paper-checkbox-unchecked-color: var(--paper-teal-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-teal-900);
+ }
+ [color-class="pink"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-pink-500);
+ --paper-checkbox-checked-ink-color: var(--paper-pink-500);
+ --paper-checkbox-unchecked-color: var(--paper-pink-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-pink-900);
+ }
+ [color-class="orange"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-orange-500);
+ --paper-checkbox-checked-ink-color: var(--paper-orange-500);
+ --paper-checkbox-unchecked-color: var(--paper-orange-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-orange-900);
+ }
+ [color-class="brown"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-brown-500);
+ --paper-checkbox-checked-ink-color: var(--paper-brown-500);
+ --paper-checkbox-unchecked-color: var(--paper-brown-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-brown-900);
+ }
+ [color-class="indigo"] paper-checkbox {
+ --paper-checkbox-checked-color: var(--paper-indigo-500);
+ --paper-checkbox-checked-ink-color: var(--paper-indigo-500);
+ --paper-checkbox-unchecked-color: var(--paper-indigo-900);
+ --paper-checkbox-unchecked-ink-color: var(--paper-indigo-900);
+ }
+ </style>
+ </template>
+</dom-module>
+<dom-module id="tf-multi-checkbox" assetpath="../components/tf-multi-checkbox/">
+ <style include="scrollbar-style"></style>
+ <style include="run-color-style"></style>
+
+ <template>
+ <div id="outer-container" class="scrollbar">
+ <template is="dom-repeat" items="[[names]]" sort="[[_tooltipComparator(tooltips, tooltipOrderer)]]">
+ <div class="run-row" color-class$="[[_applyColorClass(item, classScale)]]" null-tooltip$="[[_isNullTooltip(item, tooltips)]]" highlight$="[[_isHighlighted(item, highlights.*)]]">
+ <div class="checkbox-container vertical-align-container">
+ <paper-checkbox class="checkbox vertical-align-center" name="[[item]]" checked$="[[_isChecked(item,outSelected.*)]]" on-change="_checkboxChange"></paper-checkbox>
+ </div>
+ <div class="item-label-container">
+ <span>[[item]]</span>
+ </div>
+ <div class="tooltip-value-container vertical-align-container">
+ <span class="vertical-align-top">[[_lookupTooltip(item,tooltips)]]</span>
+ </div>
+ </div>
+ </template>
+ </div>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ #outer-container {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ width: 100%;
+ flex-grow: 1;
+ flex-shrink: 1;
+ word-wrap: break-word;
+ }
+ .run-row {
+ padding-top: 5px;
+ padding-bottom: 5px;
+ display: flex;
+ flex-direction: row;
+ font-size: 13px;
+ }
+ .checkbox-container {
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+ .checkbox {
+ padding-left: 2px;
+ width: 32px;
+ }
+ .item-label-container {
+ flex-grow: 1;
+ flex-shrink: 1;
+ width: 0px; /* hack to get the flex-grow to work properly */
+ }
+ .tooltip-value-container {
+ display: flex;
+ justify-content: center;
+ flex-grow: 0;
+ flex-shrink: 0;
+ text-align:right;
+ padding-left: 2px;
+ }
+ .vertical-align-container {
+ display: flex;
+ justify-content: center;
+ }
+ .vertical-align-container .vertical-align-center {
+ align-self: center;
+ }
+ .vertical-align-container .vertical-align-top {
+ align-self: start;
+ }
+ [null-tooltip] {
+ display: none;
+ }
+ [highlight] {
+ font-weight: bold;
+ }
+ </style>
+ </template>
+
+ <script>
+ Polymer({
+ is: "tf-multi-checkbox",
+ properties: {
+ names: Array,
+ tooltipOrderer: {
+ /* Used to compute how to order the tooltips based on the tooltip value.
+ * By default, it parses the tooltip strings as numbers.
+ * If set to a falsey value, tooltips are always ordered lexicographically.
+ */
+ type: Function,
+ value: function() {
+ return function(x) {return +x;}
+ },
+ },
+ tooltips: Object,
+ highlights: Array,
+ outSelected: {
+ type: Array,
+ notify: true,
+ value: function() {
+ return [];
+ },
+ },
+ hideMissingTooltips: {
+ // If we have tooltips, but some names are missing, do we hide them?
+ type: Boolean,
+ value: true,
+ },
+ classScale: Function, // map from run name to css class
+ },
+ observers: [
+ "_initializeOutSelected(names.*)",
+ ],
+ _lookupTooltip: function(item, tooltips) {
+ return tooltips != null ? tooltips[item] : null;
+ },
+ _isNullTooltip: function(item, tooltips) {
+ if (!this.hideMissingTooltips) {
+ return true;
+ }
+ if (tooltips == null) {
+ return false;
+ }
+ return tooltips[item] == null;
+ },
+ _initializeOutSelected: function(change) {
+ this.outSelected = change.base.slice();
+ },
+ _tooltipComparator: function(tooltips, tooltipOrderer) {
+ return function(a, b) {
+ if (!tooltips || !tooltipOrderer) {
+ // if we're missing tooltips or orderer, do lexicogrpahic sort
+ return a.localeCompare(b);
+ }
+ function getValue(x) {
+ var value = tooltipOrderer(tooltips[x]);
+ return value == null || _.isNaN(value) ? -Infinity : value;
+ }
+ var aValue = getValue(a);
+ var bValue = getValue(b);
+ return aValue === bValue ? a.localeCompare(b) : bValue - aValue;
+ }
+ },
+ _checkboxChange: function(e) {
+ var name = e.srcElement.name;
+ var idx = this.outSelected.indexOf(name);
+ var checked = e.srcElement.checked;
+ if (checked && idx === -1) {
+ this.push("outSelected", name);
+ } else if (!checked && idx !== -1) {
+ this.splice("outSelected", idx, 1);
+ }
+ },
+ _isChecked: function(item, outSelectedChange) {
+ var outSelected = outSelectedChange.base;
+ return outSelected.indexOf(item) !== -1;
+ },
+ _initializeRuns: function(change) {
+ this.outSelected = change.base.slice();
+ },
+ _applyColorClass: function(item, classScale) {
+ // TODO: Update style just on the element that changes
+ // and apply at microtask timing
+ this.debounce("restyle", function (){
+ this.updateStyles();
+ }, 16);
+ return classScale(item);
+ },
+ _isHighlighted: function(item, highlights) {
+ return highlights.base.indexOf(item) !== -1;
+ },
+ });
+ </script>
+
+</dom-module>
+<dom-module id="tf-run-selector" assetpath="../components/tf-event-dashboard/">
+ <template>
+ <div id="top-text">
+ <template is="dom-if" if="[[xValue]]">
+ <div class="x-tooltip tooltip-container">
+ <div class="x-tooltip-label">[[xType]]</div>
+ <div class="x-tooltip-value">[[xValue]]</div>
+ </div>
+ </template>
+ <template is="dom-if" if="[[!xValue]]">
+ <div id="tooltip-help" class="tooltip-container">
+ Selected Runs:
+ </div>
+ </template>
+ </div>
+ <tf-multi-checkbox names="[[runs]]" tooltips="[[tooltips]]" highlights="[[_arrayify(closestRun)]]" out-selected="{{outSelected}}" class-scale="[[classScale]]" hide-missing-tooltips=""></tf-multi-checkbox>
+ <style>
+ :host {
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 10px;
+ box-sizing: border-box;
+ }
+ #top-text {
+ width: 100%;
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding-left: 35px;
+ padding-right: 16px;
+ padding-bottom: 6px;
+ box-sizing: border-box;
+ color: var(--paper-grey-800);
+ }
+ tf-multi-checkbox {
+ display: flex;
+ flex-grow: 1;
+ flex-shrink: 1;
+ height: 0px; /* hackhack So the flex-grow takes over and gives it space */
+ }
+ .x-tooltip {
+ display: flex;
+ flex-direction: row;
+ }
+ .x-tooltip-label {
+ flex-grow: 1;
+ align-self: flex-start;
+ }
+ .x-tooltip-value {
+ align-self: flex-end;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-run-selector",
+ properties: {
+ outSelected: {type: Array, notify: true},
+ // runs: an array of strings, representing the run names that may be chosen
+ runs: Array,
+ tooltips: {type: Object, value: null}, // {[run: string]: string}
+ xValue: {type: String, value: null}, // the string representing run's x val
+ xType: String, // string: relative, stpe, wall_time
+ classScale: Object, // map from run name to color class (css)
+ closestRun: {type: String, value: null}, // which run has a value closest to mouse coordinate
+ },
+ _arrayify: function(item) {
+ return [item];
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-x-type-selector" assetpath="../components/tf-event-dashboard/">
+ <template>
+ <div id="buttons">
+ <p>X Type: </p>
+ <paper-button class="x-button selected" id="step" on-tap="_select" raised="">
+ step
+ </paper-button>
+ <paper-button class="x-button" id="relative" on-tap="_select">
+ relative
+ </paper-button>
+ <paper-button class="x-button" id="wall_time" on-tap="_select">
+ wall
+ </paper-button>
+ </div>
+ <style>
+ .x-button {
+ width: 29%;
+ font-size: 14px;
+ background-color: var(--paper-grey-500);
+ margin-top: 5px;
+ color: white;
+ }
+
+ .x-button.selected {
+ font-weight: bold;
+ background-color: var(--tb-orange-strong) !important;
+ }
+
+ #buttons p {
+ text-align: center;
+ font-size: 12px;
+ margin: 0;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-x-type-selector",
+ properties: {
+ outXType: {type: String, notify: true, readOnly: true, value: "step"},
+ },
+ _select: function(e) {
+ var _this = this;
+ ["step", "wall_time", "relative"].forEach(function(id) {
+ _this.$[id].raised = false;
+ _this.$[id].classList.remove("selected");
+ });
+ e.currentTarget.raised = true;
+ this._setOutXType(e.currentTarget.id);
+ e.currentTarget.classList.add("selected");
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-run-generator" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <iron-ajax id="ajax" auto="" url="[[url]]" handle-as="json" debounce="300" on-response="_setResponse" verbose="true">
+ </iron-ajax>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-run-generator",
+ properties: {
+ url: String,
+ _runToTag: {
+ type: Object,
+ readOnly: true,
+ },
+ outRunToScalars: {
+ // {[runName: string]: string[]}
+ // the names of scalar tags.
+ type: Object,
+ computed: "_scalars(_runToTag.*)",
+ notify: true,
+ },
+ outRunToHistograms: {
+ // {[runName: string]: string[]}
+ // the names of histogram tags.
+ type: Object,
+ computed: "_histograms(_runToTag.*)",
+ notify: true,
+ },
+ outRunToCompressedHistograms: {
+ // {[runName: string]: string[]}
+ // the names of histogram tags.
+ type: Object,
+ computed: "_compressedHistograms(_runToTag.*)",
+ notify: true,
+ },
+ outRunToImages: {
+ // {[runName: string]: string[]}
+ // the names of image tags.
+ type: Object,
+ computed: "_images(_runToTag.*)",
+ notify: true,
+ },
+ outRunsWithGraph: {
+ // ["run1", "run2", ...]
+ // array of run names that have an associated graph definition.
+ type: Array,
+ computed: "_graphs(_runToTag.*)",
+ notify: true
+ }
+ },
+ _scalars: function(_runToTag) {
+ return _.mapValues(_runToTag.base, "scalars");
+ },
+ _histograms: function(_runToTag) {
+ return _.mapValues(_runToTag.base, "histograms");
+ },
+ _compressedHistograms: function(_runToTag) {
+ return _.mapValues(_runToTag.base, "compressedHistograms");
+ },
+ _images: function(_runToTag) {
+ return _.mapValues(_runToTag.base, "images");
+ },
+ _graphs: function(_runToTag) {
+ var runsWithGraph = [];
+ _.each(_runToTag.base, function(runInfo, runName) {
+ if (runInfo.graph === true) {
+ runsWithGraph.push(runName);
+ }
+ });
+ return runsWithGraph;
+ },
+ _setResponse: function(event) {
+ this._set_runToTag(event.detail.response);
+ }
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-color-scale" assetpath="../components/tf-event-dashboard/">
+ <script>
+ (function() {
+ // TODO(danmane) - get Plottable team to make an API point for this
+ Plottable.Scales.Color._LOOP_LIGHTEN_FACTOR = 0;
+ var classColorPairs = [
+ ["light-blue", "#03A9F4"],
+ ["red" , "#f44366"],
+ ["green" , "#4CAF50"],
+ ["purple" , "#9c27b0"],
+ ["teal" , "#009688"],
+ ["pink" , "#e91e63"],
+ ["orange" , "#ff9800"],
+ ["brown" , "#795548"],
+ ["indigo" , "#3f51b5"],
+ ];
+ var classes = _.pluck(classColorPairs, 0);
+ var colors = _.pluck(classColorPairs, 1);
+ Polymer({
+ is: "tf-color-scale",
+ properties: {
+ runs: Array,
+ outClassScale: {
+ type: Object,
+ notify: true,
+ readOnly: true,
+ value: function() {
+ return new d3.scale.ordinal().range(classes);
+ },
+ // TODO(danmane): the class scale will not update if the domain changes.
+ // this behavior is inconsistent with the ColorScale.
+ // in practice we don't change runs after initial load so it's not currently an issue
+ },
+ outColorScale: {
+ type: Object,
+ notify: true,
+ readOnly: true,
+ value: function() {
+ var scale = new Plottable.Scales.Color().range(colors);
+ scale.onUpdate(this._notifyColorScaleDomainChange.bind(this));
+ return scale;
+ },
+ },
+ },
+ observers: ["_changeRuns(runs.*)"],
+ _changeRuns: function(runs) {
+ this.outClassScale.domain(this.runs);
+ this.outColorScale.domain(this.runs);
+ },
+ _notifyColorScaleDomainChange: function() {
+ this.notifyPath("outColorScale.domain_path", this.outColorScale.domain());
+ this.outColorScale.domain_path = null;
+ },
+ });
+ })();
+ </script>
+</dom-module>
+<dom-module id="tf-url-generator" assetpath="../components/tf-dashboard-common/">
+ <script>/// <reference path="../../typings/tsd.d.ts" />
+/// <reference path="../../bower_components/plottable/plottable.d.ts" />
+var TF;
+(function (TF) {
+ var Urls;
+ (function (Urls) {
+ Urls.routes = ["runs", "scalars", "histograms",
+ "compressedHistograms", "images",
+ "individualImage", "graph"];
+ function router(route) {
+ return function (tag, run) {
+ return "/" + route + "?tag=" + encodeURIComponent(tag)
+ + "&run=" + encodeURIComponent(run);
+ };
+ }
+ function runsUrl() {
+ return "/runs";
+ }
+ Urls.runsUrl = runsUrl;
+ Urls.scalarsUrl = router("scalars");
+ Urls.histogramsUrl = router("histograms");
+ Urls.compressedHistogramsUrl = router("compressedHistograms");
+ Urls.imagesUrl = router("images");
+ function individualImageUrl(query) {
+ return "/individualImage?" + query;
+ }
+ Urls.individualImageUrl = individualImageUrl;
+ function graphUrl(run) {
+ return "/graph?run=" + encodeURIComponent(run);
+ }
+ Urls.graphUrl = graphUrl;
+ })(Urls = TF.Urls || (TF.Urls = {}));
+})(TF || (TF = {}));
+</script>
+ <script>
+ var polymerObject = {
+ is: "tf-url-generator",
+ properties: {
+ outRunsUrl: {
+ type: String,
+ value: function() {
+ return TF.Urls.runsUrl();
+ },
+ readOnly: true,
+ notify: true,
+ },
+ },
+ };
+ TF.Urls.routes.forEach(function(route) {
+ /* for each route (other than runs, handled seperately):
+ * out`RouteName`: {
+ * type: Function,
+ * readOnly: true,
+ * notify: true,
+ * value: function() {
+ * return TF.Urls.`routeName`Url;
+ * }
+ */
+ if (route === "runs") {
+ return;
+ }
+ var urlName = route + "Url";
+ var propertyName = Polymer.CaseMap.dashToCamelCase("out-" + urlName + "Generator");
+ polymerObject.properties[propertyName] = {
+ type: Function,
+ value: function() {
+ return TF.Urls[urlName];
+ },
+ notify: true,
+ readOnly: true,
+ }
+ });
+ Polymer(polymerObject);
+ </script>
+</dom-module>
+<dom-module id="tf-dashboard-layout" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <div id="sidebar">
+ <content select=".sidebar"></content>
+ </div>
+
+ <div id="center" class="scrollbar">
+ <content select=".center"></content>
+ </div>
+ <style include="scrollbar-style"></style>
+ <style>
+ #sidebar {
+ width: inherit;
+ height: 100%;
+ background-color: var(--tb-grey-darker);
+ background-image: linear-gradient(to right, var(--tb-grey-lighter), var(--tb-grey-lighter));
+ overflow: ellipsis;
+ padding-left: 10px;
+ padding-right: 10px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+
+ #center {
+ margin: 0 10px;
+ height: 100%;
+ overflow-y: scroll;
+ padding-right: 12px;
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+ :host {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-dashboard-layout",
+ });
+ </script>
+</dom-module>
+<dom-module id="dashboard-style" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <style>
+ .card {
+ height: 200px;
+ width: 300px;
+ display: flex;
+ flex-direction: column;
+ margin: 5px 5px;
+ padding: 5px;
+ border: 1px solid var(--paper-grey-500);
+ border-radius: 3px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ position: relative;
+ }
+
+ .card .card-title {
+ flex-grow: 0;
+ flex-shrink: 0;
+ margin-bottom: 2px;
+ font-size: 14px;
+ font-weight: bold;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .card .card-content {
+ flex-grow: 1;
+ flex-shrink: 1;
+ display: flex;
+ }
+ .card .card-bottom-row {
+ flex-grow: 0;
+ flex-shrink: 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ }
+
+ .card.selected {
+ height: 400px;
+ width: 100%;
+ }
+
+ [shift] {
+ bottom: 20px !important;
+ }
+
+ .expand-button {
+ position: absolute;
+ left: 0px;
+ bottom: 0px;
+ color: #2196F3;
+ display: block;
+ }
+
+ #content-container{
+ display: block;
+ }
+
+ .sidebar {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+
+ #categorizer {
+ flex-shrink: 0;
+ }
+
+ #xTypeSelector {
+ flex-shrink: 0;
+ margin: 20px 0;
+ }
+
+ #runSelector {
+ flex-shrink: 1;
+ flex-grow: 1;
+ }
+
+ #download-option {
+ padding-left: 55px;
+ color: var(--paper-grey-700);
+ font-size: 14px;
+ }
+
+ #download-option paper-toggle-button {
+ --paper-toggle-button-checked-button-color: var(--tb-orange-strong);
+ --paper-toggle-button-checked-bar-color: var(--tb-orange-weak);
+
+ }
+ </style>
+ </template>
+</dom-module>
+<dom-module id="tf-downloader" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <paper-dropdown-menu no-label-float="true" label="run to download" selected-item-label="{{_run}}">
+ <paper-menu class="dropdown-content">
+ <template is="dom-repeat" items="[[_runs]]">
+ <paper-item no-label-float="true">[[item]]</paper-item>
+ </template>
+ </paper-menu>
+ </paper-dropdown-menu>
+ <a download="[[_csvName(_run)]]" href="[[_csvUrl(_run, urlFn)]]">CSV</a>
+ <a download="[[_jsonName(_run)]]" href="[[_jsonUrl(_run, urlFn)]]">JSON</a>
+ <style>
+ :host {
+ display: block;
+ }
+ paper-dropdown-menu {
+ width: 220px;
+ --paper-input-container-label: {
+ font-size: 10px;
+ }
+ --paper-input-container-input: {
+ font-size: 10px;
+ }
+ }
+ a {
+ font-size: 10px;
+ border-radius: 3px;
+ border: 1px solid #EEE;
+ }
+ paper-input {
+ font-size: 22px;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-downloader",
+ properties: {
+ _run: String,
+ _runs: {
+ type: Array,
+ computed: "_computeRuns(runToTag.*, selectedRuns.*)",
+ },
+ selectedRuns: Array,
+ runToTag: Object,
+ tag: String,
+ urlFn: Function,
+ },
+ _computeRuns: function(runToTagChange, selectedRunsChange) {
+ var runToTag = this.runToTag;
+ var tag = this.tag;
+ return this.selectedRuns.filter(function(x) {
+ return runToTag[x].indexOf(tag) !== -1;
+ })
+ },
+ _csvUrl: function(_run, urlFn) {
+ return urlFn(this.tag, _run) + "&format=csv";
+ },
+ _jsonUrl: function(_run, urlFn) {
+ return urlFn(this.tag, _run);
+ },
+ _csvName: function(_run) {
+ return "run_" + _run + ",tag_" + this.tag + ".csv";
+ },
+ _jsonName: function(_run) {
+ return "run-" + _run + "-tag-" + this.tag + ".json";
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-regex-group" assetpath="../components/tf-regex-group/">
+ <template>
+ <div class="regex-list">
+ <template is="dom-repeat" items="{{rawRegexes}}">
+ <div class="regex-line">
+ <paper-input id="text-input" class="regex-input" label="input new regex" no-label-float="" bind-value="{{item.regex}}" invalid="[[!item.valid]]" on-keyup="moveFocus"></paper-input>
+ <paper-toggle-button class="active-button" checked="{{item.active}}" disabled="[[!item.valid]]"></paper-toggle-button>
+
+ <paper-icon-button icon="delete" class="delete-button" aria-label="Delete Regex" tabindex="0" on-tap="deleteRegex"></paper-icon-button>
+ </div>
+ <style>
+ .regex-input {
+ width: 210px;
+ display: inline-block;
+ padding-left: 8px;
+ padding-right: 5px;
+ }
+
+ .active-button {
+ --paper-toggle-button-checked-button-color: var(--tb-orange-strong);
+ --paper-toggle-button-checked-bar-color: var(--tb-orange-weak);
+ border: none;
+ }
+
+ .delete-button {
+ color: var(--paper-pink-900);
+ width: 24px;
+ height: 24px;
+ }
+ .regex-list {
+ margin-bottom: 10px;
+ }
+ paper-input {
+ --paper-input-container-focus-color: var(--tb-orange-strong);
+ }
+ </style>
+ </template>
+ </div>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-regex-group",
+ properties: {
+ rawRegexes: {
+ type: Array,
+ value: function() {
+ return [{regex: "", active: true, valid: true}];
+ }
+ },
+ regexes: {type: Array, computed: "usableRegexes(rawRegexes.*)", notify: true},
+ },
+ observers: [
+ "addNewRegexIfNeeded(rawRegexes.*)",
+ "checkValidity(rawRegexes.*)",
+ ],
+ checkValidity: function(x) {
+ var match = x.path.match(/rawRegexes\.(\d+)\.regex/);
+ if (match) {
+ var idx = match[1];
+ this.set("rawRegexes." + idx + ".valid", this.isValid(x.value));
+ }
+ },
+ isValid: function(s) {
+ try {
+ new RegExp(s);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
+ usableRegexes: function(regexes) {
+ var isValid = this.isValid;
+ return regexes.base.filter(function (r) {
+ // Checking validity here (rather than using the data property)
+ // is necessary because otherwise we might send invalid regexes due
+ // to the fact that this function can call before the observer does
+ return r.regex !== "" && r.active && isValid(r.regex);
+ }).map(function(r) {
+ return r.regex;
+ });
+ },
+ addNewRegexIfNeeded: function() {
+ var last = this.rawRegexes[this.rawRegexes.length - 1];
+ if (last.regex !== "") {
+ this.push("rawRegexes", {regex: "", active: true, valid: true});
+ }
+ },
+ deleteRegex: function(e) {
+ if (this.rawRegexes.length > 1) {
+ this.splice("rawRegexes", e.model.index, 1);
+ }
+ },
+ moveFocus: function(e) {
+ if (e.keyCode === 13) {
+ var idx = e.model.index;
+ var inputs = Polymer.dom(this.root).querySelectorAll(".regex-input");
+ if (idx < this.rawRegexes.length - 1) {
+ inputs[idx+1].$.input.focus();
+ } else {
+ document.activeElement.blur();
+ }
+ }
+ }
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-categorizer" assetpath="../components/tf-categorizer/">
+ <template>
+ <div class="inputs">
+ <tf-regex-group id="regex-group" regexes="{{regexes}}"></tf-regex-group>
+ </div>
+ <div id="underscore-categorization">
+ <span>Split On Underscores:</span>
+ <paper-toggle-button checked="{{splitOnUnderscore}}"></paper-toggle-button>
+ </div>
+ <style>
+ :host {
+ display: block;
+ padding-bottom: 5px;
+ padding-top: 5px;
+ }
+
+ .inputs {
+ padding-left: 5px;
+ }
+
+ paper-toggle-button {
+ --paper-toggle-button-checked-button-color: var(--tb-orange-strong);
+ --paper-toggle-button-checked-bar-color: var(--tb-orange-weak);
+ }
+ #underscore-categorization {
+ padding-left: 94px;
+ color: var(--paper-grey-700);
+ font-size: 14px;
+ }
+ </style>
+ </template>
+ <script>/// <reference path="../../typings/tsd.d.ts" />
+var Categorizer;
+(function (Categorizer) {
+ /* Canonical TensorFlow ops are namespaced using forward slashes.
+ * This fallback categorizer categorizes by the top-level namespace.
+ */
+ Categorizer.topLevelNamespaceCategorizer = splitCategorizer(/\//);
+ // Try to produce good categorizations on legacy graphs, which often
+ // are namespaced like l1_foo/bar or l2_baz/bam.
+ // If there is no leading underscore before the first forward slash,
+ // then it behaves the same as topLevelNamespaceCategorizer
+ Categorizer.legacyUnderscoreCategorizer = splitCategorizer(/[\/_]/);
+ function fallbackCategorizer(s) {
+ switch (s) {
+ case "TopLevelNamespaceCategorizer":
+ return Categorizer.topLevelNamespaceCategorizer;
+ case "LegacyUnderscoreCategorizer":
+ return Categorizer.legacyUnderscoreCategorizer;
+ default:
+ throw new Error("Unrecognized categorization strategy: " + s);
+ }
+ }
+ Categorizer.fallbackCategorizer = fallbackCategorizer;
+ /* An "extractor" is a function that takes a tag name, and "extracts" a category name.
+ * This function takes an extractor, and produces a categorizer.
+ * Currently, it is just used for the fallbackCategorizer, but we may want to
+ * refactor the general categorization logic to use the concept of extractors.
+ */
+ function extractorToCategorizer(extractor) {
+ return function (tags) {
+ if (tags.length === 0) {
+ return [];
+ }
+ var sortedTags = tags.slice().sort();
+ var categories = [];
+ var currentCategory = {
+ name: extractor(sortedTags[0]),
+ tags: [],
+ };
+ sortedTags.forEach(function (t) {
+ var topLevel = extractor(t);
+ if (currentCategory.name !== topLevel) {
+ categories.push(currentCategory);
+ currentCategory = {
+ name: topLevel,
+ tags: [],
+ };
+ }
+ currentCategory.tags.push(t);
+ });
+ categories.push(currentCategory);
+ return categories;
+ };
+ }
+ function splitCategorizer(r) {
+ var extractor = function (t) {
+ return t.split(r)[0];
+ };
+ return extractorToCategorizer(extractor);
+ }
+ function defineCategory(ruledef) {
+ var r = new RegExp(ruledef);
+ var f = function (tag) {
+ return r.test(tag);
+ };
+ return { name: ruledef, matches: f };
+ }
+ Categorizer.defineCategory = defineCategory;
+ function _categorizer(rules, fallback) {
+ return function (tags) {
+ var remaining = d3.set(tags);
+ var userSpecified = rules.map(function (def) {
+ var tags = [];
+ remaining.forEach(function (t) {
+ if (def.matches(t)) {
+ tags.push(t);
+ }
+ });
+ var cat = { name: def.name, tags: tags.sort() };
+ return cat;
+ });
+ var defaultCategories = fallback(remaining.values());
+ return userSpecified.concat(defaultCategories);
+ };
+ }
+ Categorizer._categorizer = _categorizer;
+ function categorizer(s) {
+ var rules = s.categoryDefinitions.map(defineCategory);
+ var fallback = fallbackCategorizer(s.fallbackCategorizer);
+ return _categorizer(rules, fallback);
+ }
+ Categorizer.categorizer = categorizer;
+ ;
+})(Categorizer || (Categorizer = {}));
+</script>
+ <script>
+ Polymer({
+ is: "tf-categorizer",
+ properties: {
+ regexes: {type: Array},
+ tags: {type: Array},
+ categoriesAreExclusive: {type: Boolean, value: true},
+ fallbackCategorizer: {
+ type: String,
+ computed: "chooseFallbackCategorizer(splitOnUnderscore)"
+ },
+ splitOnUnderscore: {
+ type: Boolean,
+ value: false,
+ },
+ categorizer: {
+ type: Object,
+ computed: "computeCategorization(regexes.*, categoriesAreExclusive, fallbackCategorizer)",
+ },
+ categories: {type: Array, value: function() {return [];}, notify: true, readOnly: true},
+ },
+ observers: ['recategorize(tags.*, categorizer)'],
+ computeCategorization: function(regexes, categoriesAreExclusive, fallbackCategorizer) {
+ var categorizationStrategy = {
+ categoryDefinitions: regexes.base,
+ categoriesAreExclusive: categoriesAreExclusive,
+ fallbackCategorizer: fallbackCategorizer,
+ };
+ return Categorizer.categorizer(categorizationStrategy);
+ },
+ recategorize: function() {
+ this.debounce("tf-categorizer-recategorize", function (){
+ var categories = this.categorizer(this.tags);
+ this._setCategories(categories);
+ })
+ },
+ chooseFallbackCategorizer: function(splitOnUnderscore) {
+ if (splitOnUnderscore) {
+ return "LegacyUnderscoreCategorizer";
+ } else {
+ return "TopLevelNamespaceCategorizer";
+ }
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-chart" assetpath="../components/tf-event-dashboard/">
+ <template>
+ <svg id="chartsvg"></svg>
+ <style>
+ :host {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+ svg {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+ .plottable .crosshairs line.guide-line {
+ stroke: #777;
+ }
+ </style>
+ </template>
+ <script>var __extends = (this && this.__extends) || function (d, b) {
+ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ __.prototype = b.prototype;
+ d.prototype = new __();
+};
+var Plottable;
+(function (Plottable) {
+ var DragZoomLayer = (function (_super) {
+ __extends(DragZoomLayer, _super);
+ /* Constructs a SelectionBoxLayer with an attached DragInteraction and ClickInteraction.
+ * On drag, it triggers an animated zoom into the box that was dragged.
+ * On double click, it zooms back out to the original view, before any zooming.
+ * The zoom animation uses an easing function (default d3.ease("cubic-in-out")) and is customizable.
+ * Usage: Construct the selection box layer and attach x and y scales, and then add the layer
+ * over the plot you are zooming on using a Component Group.
+ * TODO(danmane) - merge this into Plottable
+ */
+ function DragZoomLayer(xScale, yScale) {
+ _super.call(this);
+ this.isZoomed = false;
+ this.easeFn = d3.ease("cubic-in-out");
+ this._animationTime = 750;
+ this.xScale(xScale);
+ this.yScale(yScale);
+ this._dragInteraction = new Plottable.Interactions.Drag();
+ this._dragInteraction.attachTo(this);
+ this._doubleClickInteraction = new Plottable.Interactions.DoubleClick();
+ this._doubleClickInteraction.attachTo(this);
+ this.setupCallbacks();
+ }
+ DragZoomLayer.prototype.setupCallbacks = function () {
+ var _this = this;
+ var dragging = false;
+ this._dragInteraction.onDragStart(function (startPoint) {
+ _this.bounds({
+ topLeft: startPoint,
+ bottomRight: startPoint,
+ });
+ });
+ this._dragInteraction.onDrag(function (startPoint, endPoint) {
+ _this.bounds({ topLeft: startPoint, bottomRight: endPoint });
+ _this.boxVisible(true);
+ dragging = true;
+ });
+ this._dragInteraction.onDragEnd(function (startPoint, endPoint) {
+ _this.boxVisible(false);
+ _this.bounds({ topLeft: startPoint, bottomRight: endPoint });
+ if (dragging) {
+ _this.zoom();
+ }
+ dragging = false;
+ });
+ this._doubleClickInteraction.onDoubleClick(this.unzoom.bind(this));
+ };
+ DragZoomLayer.prototype.animationTime = function (animationTime) {
+ if (animationTime == null) {
+ return this._animationTime;
+ }
+ if (animationTime < 0) {
+ throw new Error("animationTime cannot be negative");
+ }
+ this._animationTime = animationTime;
+ return this;
+ };
+ /* Set the easing function, which determines how the zoom interpolates over time. */
+ DragZoomLayer.prototype.ease = function (fn) {
+ if (typeof (fn) !== "function") {
+ throw new Error("ease function must be a function");
+ }
+ if (fn(0) !== 0 || fn(1) !== 1) {
+ Plottable.Utils.Window.warn("Easing function does not maintain invariant f(0)==0 && f(1)==1. Bad behavior may result.");
+ }
+ this.easeFn = fn;
+ return this;
+ };
+ // Zoom into extent of the selection box bounds
+ DragZoomLayer.prototype.zoom = function () {
+ var x0 = this.xExtent()[0].valueOf();
+ var x1 = this.xExtent()[1].valueOf();
+ var y0 = this.yExtent()[1].valueOf();
+ var y1 = this.yExtent()[0].valueOf();
+ if (x0 === x1 || y0 === y1) {
+ return;
+ }
+ if (!this.isZoomed) {
+ this.isZoomed = true;
+ this.xDomainToRestore = this.xScale().domain();
+ this.yDomainToRestore = this.yScale().domain();
+ }
+ this.interpolateZoom(x0, x1, y0, y1);
+ };
+ // Restore the scales to their state before any zoom
+ DragZoomLayer.prototype.unzoom = function () {
+ if (!this.isZoomed) {
+ return;
+ }
+ this.isZoomed = false;
+ this.interpolateZoom(this.xDomainToRestore[0], this.xDomainToRestore[1], this.yDomainToRestore[0], this.yDomainToRestore[1]);
+ };
+ // If we are zooming, disable interactions, to avoid contention
+ DragZoomLayer.prototype.isZooming = function (isZooming) {
+ this._dragInteraction.enabled(!isZooming);
+ this._doubleClickInteraction.enabled(!isZooming);
+ };
+ DragZoomLayer.prototype.interpolateZoom = function (x0f, x1f, y0f, y1f) {
+ var _this = this;
+ var x0s = this.xScale().domain()[0].valueOf();
+ var x1s = this.xScale().domain()[1].valueOf();
+ var y0s = this.yScale().domain()[0].valueOf();
+ var y1s = this.yScale().domain()[1].valueOf();
+ // Copy a ref to the ease fn, so that changing ease wont affect zooms in progress
+ var ease = this.easeFn;
+ var interpolator = function (a, b, p) { return d3.interpolateNumber(a, b)(ease(p)); };
+ this.isZooming(true);
+ var start = Date.now();
+ var draw = function () {
+ var now = Date.now();
+ var passed = now - start;
+ var p = _this._animationTime === 0 ? 1 : Math.min(1, passed / _this._animationTime);
+ var x0 = interpolator(x0s, x0f, p);
+ var x1 = interpolator(x1s, x1f, p);
+ var y0 = interpolator(y0s, y0f, p);
+ var y1 = interpolator(y1s, y1f, p);
+ _this.xScale().domain([x0, x1]);
+ _this.yScale().domain([y0, y1]);
+ if (p < 1) {
+ Plottable.Utils.DOM.requestAnimationFramePolyfill(draw);
+ }
+ else {
+ _this.isZooming(false);
+ }
+ };
+ draw();
+ };
+ return DragZoomLayer;
+ })(Plottable.Components.SelectionBoxLayer);
+ Plottable.DragZoomLayer = DragZoomLayer;
+})(Plottable || (Plottable = {}));
+</script>
+ <script>/// <reference path="../../typings/tsd.d.ts" />
+/// <reference path="../../bower_components/plottable/plottable.d.ts" />
+var __extends = (this && this.__extends) || function (d, b) {
+ for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
+ function __() { this.constructor = d; }
+ __.prototype = b.prototype;
+ d.prototype = new __();
+};
+var TF;
+(function (TF) {
+ var Y_TOOLTIP_FORMATTER_PRECISION = 4;
+ var STEP_AXIS_FORMATTER_PRECISION = 4;
+ var Y_AXIS_FORMATTER_PRECISION = 3;
+ var BaseChart = (function () {
+ function BaseChart(tag, dataCoordinator, tooltipUpdater, xType, colorScale) {
+ this.dataCoordinator = dataCoordinator;
+ this.tag = tag;
+ this.colorScale = colorScale;
+ this.tooltipUpdater = tooltipUpdater;
+ this.buildChart(xType);
+ }
+ BaseChart.prototype.changeRuns = function (runs) {
+ throw new Error("Abstract method not implemented");
+ };
+ BaseChart.prototype.addCrosshairs = function (plot, yAccessor) {
+ var _this = this;
+ var pi = new Plottable.Interactions.Pointer();
+ pi.attachTo(plot);
+ var xGuideLine = new Plottable.Components.GuideLineLayer("vertical");
+ var yGuideLine = new Plottable.Components.GuideLineLayer("horizontal");
+ xGuideLine.addClass("crosshairs");
+ yGuideLine.addClass("crosshairs");
+ var group = new Plottable.Components.Group([plot, xGuideLine, yGuideLine]);
+ var yfmt = multiscaleFormatter(Y_TOOLTIP_FORMATTER_PRECISION);
+ pi.onPointerMove(function (p) {
+ var run2val = {};
+ var x = _this.xScale.invert(p.x).valueOf();
+ var yMin = _this.yScale.domain()[0];
+ var yMax = _this.yScale.domain()[1];
+ var closestRun = null;
+ var minYDistToRun = Infinity;
+ var yValueForCrosshairs = p.y;
+ plot.datasets().forEach(function (dataset) {
+ var run = dataset.metadata().run;
+ var data = dataset.data();
+ var xs = data.map(function (d, i) { return _this.xAccessor(d, i, dataset).valueOf(); });
+ var idx = _.sortedIndex(xs, x);
+ if (idx === 0 || idx === data.length) {
+ // Only find a point when the cursor is inside the range of the data
+ // if the cursor is to the left or right of all the data, dont attach.
+ return;
+ }
+ var previous = data[idx - 1];
+ var next = data[idx];
+ var x0 = _this.xAccessor(previous, idx - 1, dataset).valueOf();
+ var x1 = _this.xAccessor(next, idx, dataset).valueOf();
+ var y0 = yAccessor(previous, idx - 1, dataset).valueOf();
+ var y1 = yAccessor(next, idx, dataset).valueOf();
+ var slope = (y1 - y0) / (x1 - x0);
+ var y = y0 + slope * (x - x0);
+ if (y < yMin || y > yMax || y !== y) {
+ // don't find data that is off the top or bottom of the plot.
+ // also don't find data if it is NaN
+ return;
+ }
+ var dist = Math.abs(_this.yScale.scale(y) - p.y);
+ if (dist < minYDistToRun) {
+ minYDistToRun = dist;
+ closestRun = run;
+ yValueForCrosshairs = _this.yScale.scale(y);
+ }
+ // Note this tooltip will display linearly interpolated values
+ // e.g. will display a y=0 value halfway between [y=-1, y=1], even
+ // though there is not actually any 0 datapoint. This could be misleading
+ run2val[run] = yfmt(y);
+ });
+ xGuideLine.pixelPosition(p.x);
+ yGuideLine.pixelPosition(yValueForCrosshairs);
+ _this.tooltipUpdater(run2val, _this.xTooltipFormatter(x), closestRun);
+ });
+ pi.onPointerExit(function () {
+ _this.tooltipUpdater(null, null, null);
+ xGuideLine.pixelPosition(-1);
+ yGuideLine.pixelPosition(-1);
+ });
+ return group;
+ };
+ BaseChart.prototype.buildChart = function (xType) {
+ if (this.outer) {
+ this.outer.destroy();
+ }
+ var xComponents = getXComponents(xType);
+ this.xAccessor = xComponents.accessor;
+ this.xScale = xComponents.scale;
+ this.xAxis = xComponents.axis;
+ this.xAxis.margin(0).tickLabelPadding(3);
+ this.xTooltipFormatter = xComponents.tooltipFormatter;
+ this.yScale = new Plottable.Scales.Linear();
+ this.yAxis = new Plottable.Axes.Numeric(this.yScale, "left");
+ var yFormatter = multiscaleFormatter(Y_AXIS_FORMATTER_PRECISION);
+ this.yAxis.margin(0).tickLabelPadding(5).formatter(yFormatter);
+ this.yAxis.usesTextWidthApproximation(true);
+ var center = this.buildPlot(this.xAccessor, this.xScale, this.yScale);
+ this.gridlines = new Plottable.Components.Gridlines(this.xScale, this.yScale);
+ var dzl = new Plottable.DragZoomLayer(this.xScale, this.yScale);
+ this.center = new Plottable.Components.Group([center, this.gridlines, dzl]);
+ this.outer = new Plottable.Components.Table([
+ [this.yAxis, this.center],
+ [null, this.xAxis]
+ ]);
+ };
+ BaseChart.prototype.buildPlot = function (xAccessor, xScale, yScale) {
+ throw new Error("Abstract method not implemented.");
+ };
+ BaseChart.prototype.renderTo = function (target) {
+ this.outer.renderTo(target);
+ };
+ BaseChart.prototype.redraw = function () {
+ this.outer.redraw();
+ };
+ BaseChart.prototype.destroy = function () {
+ this.outer.destroy();
+ };
+ return BaseChart;
+ })();
+ TF.BaseChart = BaseChart;
+ var LineChart = (function (_super) {
+ __extends(LineChart, _super);
+ function LineChart() {
+ _super.apply(this, arguments);
+ }
+ LineChart.prototype.buildPlot = function (xAccessor, xScale, yScale) {
+ var yAccessor = accessorize("2");
+ var plot = new Plottable.Plots.Line();
+ plot.x(xAccessor, xScale);
+ plot.y(yAccessor, yScale);
+ plot.attr("stroke", function (d, i, m) { return m.run; }, this.colorScale);
+ this.plot = plot;
+ var group = this.addCrosshairs(plot, yAccessor);
+ return group;
+ };
+ LineChart.prototype.changeRuns = function (runs) {
+ var datasets = this.dataCoordinator.getDatasets(this.tag, runs);
+ this.plot.datasets(datasets);
+ };
+ return LineChart;
+ })(BaseChart);
+ TF.LineChart = LineChart;
+ var HistogramChart = (function (_super) {
+ __extends(HistogramChart, _super);
+ function HistogramChart() {
+ _super.apply(this, arguments);
+ }
+ HistogramChart.prototype.changeRuns = function (runs) {
+ var datasets = this.dataCoordinator.getDatasets(this.tag, runs);
+ this.plots.forEach(function (p) { return p.datasets(datasets); });
+ };
+ HistogramChart.prototype.buildPlot = function (xAccessor, xScale, yScale) {
+ var _this = this;
+ var percents = [0, 228, 1587, 3085, 5000, 6915, 8413, 9772, 10000];
+ var opacities = _.range(percents.length - 1).map(function (i) { return (percents[i + 1] - percents[i]) / 2500; });
+ var accessors = percents.map(function (p, i) { return function (datum) { return datum[2][i][1]; }; });
+ var median = 4;
+ var medianAccessor = accessors[median];
+ var plots = _.range(accessors.length - 1).map(function (i) {
+ var p = new Plottable.Plots.Area();
+ p.x(xAccessor, xScale);
+ var y0 = i > median ? accessors[i] : accessors[i + 1];
+ var y = i > median ? accessors[i + 1] : accessors[i];
+ p.y(y, yScale);
+ p.y0(y0);
+ p.attr("fill", function (d, i, m) { return m.run; }, _this.colorScale);
+ p.attr("stroke", function (d, i, m) { return m.run; }, _this.colorScale);
+ p.attr("stroke-weight", function (d, i, m) { return "0.5px"; });
+ p.attr("stroke-opacity", function () { return opacities[i]; });
+ p.attr("fill-opacity", function () { return opacities[i]; });
+ return p;
+ });
+ var medianPlot = new Plottable.Plots.Line();
+ medianPlot.x(xAccessor, xScale);
+ medianPlot.y(medianAccessor, yScale);
+ medianPlot.attr("stroke", function (d, i, m) { return m.run; }, this.colorScale);
+ this.plots = plots;
+ var group = this.addCrosshairs(medianPlot, medianAccessor);
+ return new Plottable.Components.Group([new Plottable.Components.Group(plots), group]);
+ };
+ return HistogramChart;
+ })(BaseChart);
+ TF.HistogramChart = HistogramChart;
+ /* Create a formatter function that will switch between exponential and
+ * regular display depending on the scale of the number being formatted,
+ * and show `digits` significant digits.
+ */
+ function multiscaleFormatter(digits) {
+ return function (v) {
+ var absv = Math.abs(v);
+ if (absv < 1E-15) {
+ // Sometimes zero-like values get an annoying representation
+ absv = 0;
+ }
+ var f;
+ if (absv >= 1E4) {
+ f = d3.format("." + digits + "e");
+ }
+ else if (absv > 0 && absv < 0.01) {
+ f = d3.format("." + digits + "e");
+ }
+ else {
+ f = d3.format("." + digits + "g");
+ }
+ return f(v);
+ };
+ }
+ function accessorize(key) {
+ return function (d, index, dataset) { return d[key]; };
+ }
+ function stepX() {
+ var scale = new Plottable.Scales.Linear();
+ var axis = new Plottable.Axes.Numeric(scale, "bottom");
+ var formatter = Plottable.Formatters.siSuffix(STEP_AXIS_FORMATTER_PRECISION);
+ axis.formatter(formatter);
+ return {
+ scale: scale,
+ axis: axis,
+ accessor: accessorize("1"),
+ tooltipFormatter: formatter,
+ };
+ }
+ function wallX() {
+ var scale = new Plottable.Scales.Time();
+ var formatter = Plottable.Formatters.time("%a %b %e, %H:%M:%S");
+ return {
+ scale: scale,
+ axis: new Plottable.Axes.Time(scale, "bottom"),
+ accessor: function (d, index, dataset) {
+ return d[0] * 1000; // convert seconds to ms
+ },
+ tooltipFormatter: function (d) { return formatter(new Date(d)); },
+ };
+ }
+ function relativeX() {
+ var scale = new Plottable.Scales.Linear();
+ var formatter = function (n) {
+ var days = Math.floor(n / 24);
+ n -= (days * 24);
+ var hours = Math.floor(n);
+ n -= hours;
+ n *= 60;
+ var minutes = Math.floor(n);
+ n -= minutes;
+ n *= 60;
+ var seconds = Math.floor(n);
+ return days + "d " + hours + "h " + minutes + "m " + seconds + "s";
+ };
+ return {
+ scale: scale,
+ axis: new Plottable.Axes.Numeric(scale, "bottom"),
+ accessor: function (d, index, dataset) {
+ var data = dataset && dataset.data();
+ // I can't imagine how this function would be called when the data is empty
+ // (after all, it iterates over the data), but lets guard just to be safe.
+ var first = data.length > 0 ? data[0][0] : 0;
+ return (d[0] - first) / (60 * 60); // convert seconds to hours
+ },
+ tooltipFormatter: formatter,
+ };
+ }
+ function getXComponents(xType) {
+ switch (xType) {
+ case "step":
+ return stepX();
+ case "wall_time":
+ return wallX();
+ case "relative":
+ return relativeX();
+ default:
+ throw new Error("invalid xType: " + xType);
+ }
+ }
+})(TF || (TF = {}));
+</script>
+ <script>
+ Polymer({
+ is: "tf-chart",
+ properties: {
+ type: String, // "scalar" or "compressedHistogram"
+ _chart: Object,
+ colorScale: Object,
+ tag: String,
+ selectedRuns: Array,
+ xType: String,
+ dataCoordinator: Object,
+ tooltipUpdater: Function,
+ _initialized: Boolean,
+ },
+ observers: [
+ "_makeChart(type, tag, dataCoordinator, tooltipUpdater, xType, colorScale, _initialized)",
+ "_changeRuns(_chart, selectedRuns.*)"
+ ],
+ _changeRuns: function(chart, change) {
+ this._chart.changeRuns(this.selectedRuns);
+ this.redraw();
+ },
+ redraw: function() {
+ this._chart.redraw();
+ },
+ _constructor: function(type) {
+ if (type === "scalar") {
+ return TF.LineChart;
+ } else if (type === "compressedHistogram") {
+ return TF.HistogramChart;
+ } else {
+ throw new Error("Unrecognized chart type");
+ }
+ },
+ _makeChart: function(type, tag, dataCoordinator, tooltipUpdater, xType, colorScale, _initialized) {
+ if (!_initialized) {
+ return;
+ }
+ if (this._chart) this._chart.destroy();
+ var cns = this._constructor(type);
+ var chart = new cns(tag, dataCoordinator, tooltipUpdater, xType, colorScale);
+ var svg = d3.select(this.$.chartsvg);
+ this.async(function() {
+ chart.renderTo(svg);
+ this._chart = chart;
+ }, 350);
+ },
+ attached: function() {
+ this._initialized = true;
+ },
+ detached: function() {
+ this._initialized = false;
+ }
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-collapsable-pane" assetpath="../components/tf-collapsable-pane/">
+ <template>
+ <button class="heading" on-tap="togglePane" open-button$="[[opened]]">
+ <span class="name">[[name]]</span>
+ <span class="hackpadding"></span>
+ <span class="count">
+ (<span>[[count]]</span>)
+ </span>
+ </button>
+ <iron-collapse opened="[[opened]]">
+ <div class="content">
+ <template is="dom-if" if="[[opened]]" restamp="[[restamp]]">
+ <content></content>
+ </template>
+ </div>
+ </iron-collapse>
+ <style>
+ .heading {
+ margin-top: 10px;
+ padding-left: 15px;
+ background-color: #f3f3f3;
+ border: 1px solid #dedede;
+ border-radius: 5px;
+ font-size: 18px;
+ cursor: pointer;
+ -webkit-tap-highlight-color: rgba(0,0,0,0);
+ width: 100%;
+ height: 30px;
+ box-sizing: border-box;
+ font-size: 16px;
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ line-height: 1;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ }
+
+ .content {
+ padding: 15px;
+ border: 1px solid #dedede;
+ border-top: none;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+ [open-button] {
+ border-bottom-left-radius: 0px !important;
+ border-bottom-right-radius: 0px !important;
+ }
+ .name {
+ flex-grow: 0;
+ }
+ .count {
+ flex-grow: 0;
+ float: right;
+ font-size: 12px;
+ }
+ .hackpadding {
+ /* An obnoxious hack, but I can't get justify-content: space-between to work */
+ flex-grow: 1;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-collapsable-pane",
+ properties: {
+ opened: {type: Boolean, value: false},
+ restamp: {type: Boolean, value: true},
+ name: {type: String, observer: "hide"},
+ count: {type: Number},
+ },
+ hide: function() {
+ this.opened = false;
+ },
+ togglePane: function() {
+ this.opened = !this.opened;
+ }
+ });
+ </script>
+
+</dom-module>
+<dom-module id="warning-style" assetpath="../components/tf-dashboard-common/">
+ <template>
+ <style>
+ .warning {
+ max-width: 540px;
+ margin: 80px auto 0 auto;
+ }
+ </style>
+ </template>
+</dom-module>
+<dom-module id="tf-event-dashboard" assetpath="../components/tf-event-dashboard/">
+ <template>
+ <div id="plumbing">
+ <tf-url-generator out-runs-url="{{runsUrl}}" out-scalars-url-generator="{{scalarsUrlGen}}" id="urlGenerator"></tf-url-generator>
+
+ <tf-data-coordinator id="dataCoordinator" url-generator="[[scalarsUrlGen]]" run-to-tag="[[runToScalars]]" color-scale="[[colorScale]]" out-data-coordinator="{{dataCoordinator}}"></tf-data-coordinator>
+
+ <tf-run-generator id="runGenerator" url="[[runsUrl]]" out-run-to-scalars="{{runToScalars}}"></tf-run-generator>
+
+ <tf-color-scale id="colorScale" runs="[[_runs]]" out-color-scale="{{colorScale}}" out-class-scale="{{classScale}}"></tf-color-scale>
+
+ <tf-tooltip-coordinator id="tooltipCoordinator" out-tooltip-updater="{{tooltipUpdater}}" out-tooltip-map="{{tooltipMap}}" out-x-value="{{tooltipXValue}}" out-closest-run="{{closestRun}}"></tf-tooltip-coordinator>
+
+ </div>
+
+ <tf-dashboard-layout>
+ <div class="sidebar">
+ <tf-categorizer id="categorizer" tags="[[_visibleTags]]" categories="{{categories}}"></tf-categorizer>
+ <span id="download-option">
+ Show Data Download Links:
+ <paper-toggle-button checked="{{_show_download_links}}"></paper-toggle-button>
+ </span>
+
+ <tf-x-type-selector id="xTypeSelector" out-x-type="{{xType}}"></tf-x-type-selector>
+
+ <tf-run-selector id="runSelector" runs="[[_runs]]" class-scale="[[classScale]]" out-selected="{{selectedRuns}}" tooltips="[[tooltipMap]]" closest-run="[[closestRun]]" x-value="[[tooltipXValue]]" x-type="[[xType]]"></tf-run-selector>
+
+ </div>
+ <div class="center">
+ <template is="dom-if" if="[[!categories.length]]">
+ <div class="warning">
+ <p>
+ No scalar summary tags were found.
+ </p>
+ <p>
+ Maybe data hasn't loaded yet, or maybe you need
+ to add some <code>tf.scalar_summary</code> ops to your graph, and
+ serialize them using the <code>tf.training.summary_io.SummaryWriter</code>.
+ </p>
+ </div>
+ </template>
+ <template is="dom-repeat" items="[[categories]]">
+ <tf-collapsable-pane name="[[item.name]]" count="[[item.tags.length]]">
+ <div class="layout horizontal wrap">
+ <template is="dom-repeat" items="[[item.tags]]" as="tag">
+ <div class="card">
+ <span class="card-title">[[tag]]</span>
+ <div class="card-content">
+ <tf-chart tag="[[tag]]" type="scalar" id="chart" selected-runs="[[selectedRuns]]" x-type="[[xType]]" data-coordinator="[[dataCoordinator]]" color-scale="[[colorScale]]" on-keyup="toggleSelected" tabindex="2" tooltip-updater="[[tooltipUpdater]]"></tf-chart>
+ <paper-icon-button class="expand-button" shift$="[[_show_download_links]]" icon="fullscreen" on-tap="toggleSelected"></paper-icon-button>
+ </div>
+ <template is="dom-if" if="[[_show_download_links]]">
+ <div class="card-bottom-row">
+ <tf-downloader selected-runs="[[selectedRuns]]" tag="[[tag]]" url-fn="[[scalarsUrlGen]]" run-to-tag="[[runToScalars]]">
+ </tf-downloader>
+ </div>
+ </template>
+ </div>
+ </template>
+ </div>
+ </tf-collapsable-pane>
+ </template>
+ </div>
+ </tf-dashboard-layout>
+
+ <style include="dashboard-style"></style>
+ <style include="warning-style"></style>
+
+ </template>
+
+ <script>
+ Polymer({
+ is: "tf-event-dashboard",
+ properties: {
+ _runs: {
+ type: Array,
+ computed: "_getRuns(runToScalars)",
+ },
+ _visibleTags: {
+ type: Array,
+ computed: "_getVisibleTags(selectedRuns.*, runToScalars.*)"
+ },
+ _show_download_links: Boolean,
+ },
+ observers: ['redraw(_show_download_links)'],
+ redraw: function(_show_download_links) {
+ var els = this.getElementsByTagName("tf-chart");
+ for (var i=0; i<els.length; i++) {
+ els[i].redraw();
+ }
+ },
+ _getRuns: function(runToScalars) {
+ return _.keys(runToScalars);
+ },
+ _getVisibleTags: function(selectedRunsChange, runsToScalarsChange) {
+ var keys = selectedRunsChange.base;
+ var dict = runsToScalarsChange.base;
+ return _.union.apply(null, keys.map(function(k) {return dict[k]}));
+ },
+ toggleSelected: function(e) {
+ var currentTarget = Polymer.dom(e.currentTarget);
+ var parentDiv = currentTarget.parentNode.parentNode;
+ parentDiv.classList.toggle("selected");
+ var chart = currentTarget.previousElementSibling;
+ if (chart) {
+ chart.redraw();
+ }
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-histogram-dashboard" assetpath="../components/tf-histogram-dashboard/">
+ <template>
+ <div id="plumbing">
+ <tf-url-generator out-runs-url="{{runsUrl}}" out-compressed-histograms-url-generator="{{compressedHistogramsUrlGen}}" id="urlGenerator"></tf-url-generator>
+
+ <tf-data-coordinator id="dataCoordinator" url-generator="[[compressedHistogramsUrlGen]]" run-to-tag="[[runToCompressedHistograms]]" color-scale="[[colorScale]]" out-data-coordinator="{{dataCoordinator}}"></tf-data-coordinator>
+
+ <tf-run-generator id="runGenerator" url="[[runsUrl]]" out-run-to-compressed-histograms="{{runToCompressedHistograms}}"></tf-run-generator>
+
+ <tf-color-scale id="colorScale" runs="[[_runs]]" out-color-scale="{{colorScale}}" out-class-scale="{{classScale}}"></tf-color-scale>
+
+ <tf-tooltip-coordinator id="tooltipCoordinator" out-tooltip-updater="{{tooltipUpdater}}" out-tooltip-map="{{tooltipMap}}" out-x-value="{{tooltipXValue}}" out-closest-run="{{closestRun}}"></tf-tooltip-coordinator>
+ </div>
+
+ <tf-dashboard-layout>
+ <div class="sidebar">
+
+ <tf-categorizer id="categorizer" tags="[[_visibleTags]]" categories="{{categories}}"></tf-categorizer>
+
+ <tf-x-type-selector id="xTypeSelector" out-x-type="{{xType}}"></tf-x-type-selector>
+
+ <tf-run-selector id="runSelector" runs="[[_runs]]" class-scale="[[classScale]]" out-selected="{{selectedRuns}}" tooltips="[[tooltipMap]]" closest-run="[[closestRun]]" x-value="[[tooltipXValue]]" x-type="[[xType]]"></tf-run-selector>
+
+ </div>
+
+ <div class="center">
+ <template is="dom-if" if="[[!categories.length]]">
+ <div class="warning">
+ <p>
+ No histogram tags were found.
+ </p>
+ <p>
+ Maybe data hasn't loaded yet, or maybe you need
+ to add some <code>tf.histogram_summary</code> ops to your graph, and
+ serialize them using the <code>tf.training.summary_io.SummaryWriter</code>.
+ </p>
+ </div>
+ </template>
+ <template is="dom-repeat" items="[[categories]]">
+ <tf-collapsable-pane name="[[item.name]]" count="[[_count(item.tags, selectedRuns.*, runToCompressedHistograms.*)]]">
+ <div class="layout horizontal wrap">
+ <template is="dom-repeat" items="[[item.tags]]" as="tag">
+ <template is="dom-repeat" items="[[selectedRuns]]" as="run">
+ <template is="dom-if" if="[[_exists(run, tag, runToCompressedHistograms.*)]]">
+ <div class="card">
+ <span class="card-title">[[tag]]</span>
+ <div class="card-content">
+ <tf-chart tag="[[tag]]" type="compressedHistogram" id="chart" selected-runs="[[_array(run)]]" x-type="[[xType]]" data-coordinator="[[dataCoordinator]]" color-scale="[[colorScale]]" on-keyup="toggleSelected" tabindex="2" tooltip-updater="[[tooltipUpdater]]"></tf-chart>
+ <paper-icon-button class="expand-button" icon="fullscreen" on-tap="toggleSelected"></paper-icon-button>
+ </div>
+ </div>
+ </template>
+ </template>
+ </template>
+ </div>
+ </tf-collapsable-pane>
+ </template>
+ </div>
+ </tf-dashboard-layout>
+
+ <style include="dashboard-style"></style>
+ <style include="warning-style"></style>
+ </template>
+
+ <script>
+ Polymer({
+ is: "tf-histogram-dashboard",
+ properties: {
+ _runs: {
+ type: Array,
+ computed: "_getRuns(runToCompressedHistograms)",
+ },
+ _visibleTags: {
+ type: Array,
+ computed: "_getVisibleTags(selectedRuns.*, runToCompressedHistograms.*)"
+ }
+ },
+ _exists: function(run, tag, runToCompressedHistogramsChange) {
+ var runToCompressedHistograms = runToCompressedHistogramsChange.base;
+ return runToCompressedHistograms[run].indexOf(tag) !== -1;
+ },
+ _array: function(x) {
+ return [x];
+ },
+ _count: function(tags, selectedRunsChange, runToCompressedHistogramsChange) {
+ var selectedRuns = selectedRunsChange.base;
+ var runToCompressedHistograms = runToCompressedHistogramsChange.base;
+ var targetTags = {};
+ tags.forEach(function(t) {
+ targetTags[t] = true;
+ });
+ var count = 0;
+ selectedRuns.forEach(function(r) {
+ runToCompressedHistograms[r].forEach(function(t) {
+ if (targetTags[t]) {
+ count++;
+ }
+ });
+ });
+ return count;
+ },
+ _getRuns: function(runToCompressedHistograms) {
+ return _.keys(runToCompressedHistograms);
+ },
+ _getVisibleTags: function(selectedRunsChange, runToCompressedHistogramsChange) {
+ var keys = selectedRunsChange.base;
+ var dict = runToCompressedHistogramsChange.base;
+ return _.union.apply(null, keys.map(function(k) {return dict[k]}));
+ },
+ toggleSelected: function(e) {
+ var currentTarget = Polymer.dom(e.currentTarget);
+ var parentDiv = currentTarget.parentNode.parentNode;
+ parentDiv.classList.toggle("selected");
+ var chart = currentTarget.previousElementSibling;
+ if (chart) {
+ chart.redraw();
+ }
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-image-loader" assetpath="../components/tf-image-dashboard/">
+ <style>
+ :host {
+ display: block;
+ }
+ img {
+ width: 100%;
+ height: 100%;
+ }
+ </style>
+ <template>
+ <iron-ajax id="ajax" auto="" url="[[metadataUrl]]" handle-as="json" debounce="50" last-response="{{imageMetadata}}" verbose="true"></iron-ajax>
+ <template is="dom-if" if="[[imageUrl]]">
+ <img src="[[imageUrl]]">
+ </template>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-image-loader",
+ properties: {
+ run: String,
+ tag: String,
+ imagesGenerator: Function,
+ individualImageGenerator: Function,
+ imageMetadata: Array,
+ metadataUrl: {
+ type: String,
+ computed: "apply(imagesGenerator, tag, run)",
+ },
+ imageUrl: {
+ type: String,
+ computed: "getLastImage(imageMetadata, individualImageGenerator)",
+ },
+ },
+ apply: function(imagesGenerator, run, tag) {
+ return imagesGenerator(run, tag);
+ },
+ getLastImage: function(imageMetadata, individualImageGenerator) {
+ if (imageMetadata == null) {
+ return null;
+ }
+ var query = _.last(imageMetadata).query;
+ return individualImageGenerator(query);
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-image-grid" assetpath="../components/tf-image-dashboard/">
+ <template>
+ <style include="scrollbar-style"></style>
+ <div id="fullContainer" class="container scrollbar">
+ <div id="topRow" class="container">
+ <div class="noshrink" id="paddingCell"></div>
+ <template is="dom-repeat" items="[[_runs]]" as="run">
+ <div class="run-name-cell noshrink">
+ <span>[[run]]</span>
+ </div>
+ </template>
+ </div>
+ <div id="bottomContainer" class="container">
+ <template is="dom-repeat" items="[[_tags]]" sort="" as="tag">
+ <div class="image-row container noshrink">
+ <div class="tag-name-cell noshrink">
+ <span class="tag-name">[[tag]]</span>
+ </div>
+ <template is="dom-repeat" items="[[_runs]]" as="run">
+ <div class="image-cell noshrink">
+ <template is="dom-if" if="[[_exists(run, tag, runToImages.*)]]">
+ <tf-image-loader id="loader" run="[[run]]" tag="[[tag]]" images-generator="[[imagesGenerator]]" individual-image-generator="[[individualImageGenerator]]">
+ </tf-image-loader>
+ </template>
+ </div>
+ </template>
+ </div>
+ </template>
+ </div>
+ </div>
+ <style>
+ :host {
+ display: block;
+ height: 100%;
+ }
+ .container {
+ display: flex;
+ flex-wrap: nowrap;
+ }
+ #fullContainer {
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+ padding-top: 20px;
+ overflow: scroll;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ #topRow {
+ flex-direction: row;
+ }
+ #bottomContainer {
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ }
+ .image-row {
+ flex-direction: row;
+ }
+ .image-cell {
+ width: 300px;
+ height: 300px;
+ border: 1px solid black;
+ }
+ .tag-name-cell {
+ height: 300px;
+ width: 300px;
+ display:flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+ .tag-name {
+ word-wrap: break-word;
+ text-align: center;
+ white-space: nowrap;
+ }
+ .run-name-cell {
+ width: 300px;
+ height: 30px;
+ text-align: center;
+ }
+ .noshrink {
+ flex-shrink: 0;
+ }
+ #paddingCell {
+ width: 300px;
+ height: 30px;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-image-grid",
+ properties: {
+ runToImages: Object,
+ _tags: {type: Array, computed: "_getTags(runToImages.*)"},
+ _runs: {type: Array, computed: "_getRuns(runToImages.*)"},
+ imagesGenerator: Function,
+ individualImageGenerator: Function,
+ },
+ _getTags: function(runToImages) {
+ return _.chain(runToImages.base).values().flatten().union().value();
+ },
+ _getRuns(runToImages) {
+ var r2i = runToImages.base;
+ return _.keys(r2i).filter(function(x) {return r2i[x].length > 0;});
+ },
+ _exists: function (run, tag, runToImages) {
+ runToImages = runToImages.base;
+ return runToImages[run].indexOf(tag) !== -1;
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-image-dashboard" assetpath="../components/tf-image-dashboard/">
+ <template>
+ <div id="plumbing">
+ <tf-url-generator out-runs-url="{{runsUrl}}" out-images-url-generator="{{imagesUrlGen}}" out-individual-image-url-generator="{{individualImageUrlGen}}" id="urlGenerator"></tf-url-generator>
+
+ <tf-run-generator id="runGenerator" url="[[runsUrl]]" out-run-to-images="{{runToImages}}"></tf-run-generator>
+ </div>
+
+ <div class="center">
+ <template is="dom-if" if="[[!_hasImages(runToImages.*)]]">
+ <div class="warning">
+ <p>
+ No image tags were found.
+ </p>
+ <p>
+ Maybe data hasn't loaded yet, or maybe you need
+ to add some <code>tf.image_summary</code> ops to your graph, and
+ serialize them using the <code>tf.training.summary_io.SummaryWriter</code>.
+ </p>
+ </div>
+ </template>
+ <tf-image-grid id="imageGrid" run-to-images="[[runToImages]]" images-generator="[[imagesUrlGen]]" individual-image-generator="[[individualImageUrlGen]]"></tf-image-grid>
+ </div>
+
+ <style>
+ .center {
+ padding-left: 10px;
+ padding-right: 10px;
+ height: 100%;
+ width: 100%;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+ :host {
+ height: 100%;
+ display: block;
+ }
+
+ </style>
+ <style include="warning-style"></style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-image-dashboard",
+ properties: {
+ runToImages: Object,
+ imagesUrlGen: Function,
+ individualImageUrlGen: Function,
+ },
+ _hasImages: function(runToImagesChange) {
+ return _.values(runToImagesChange.base).some(function(arr) {
+ return arr.length > 0;
+ });
+ },
+ });
+ </script>
+</dom-module>
+<dom-module id="tf-graph-loader" assetpath="../components/tf-graph-loader/">
+</dom-module>
+
+<script>
+Polymer({
+
+ is: 'tf-graph-loader',
+
+ properties: {
+ /**
+ * @type {value: number, msg: string}
+ *
+ * A number between 0 and 100 denoting the % of progress
+ * for the progress bar and the displayed message.
+ */
+ progress: {
+ type: Object,
+ notify: true,
+ readOnly: true // Produces, does not consume.
+ },
+ datasets: Array,
+ hasStats: {
+ type: Boolean,
+ readOnly: true, // This property produces data.
+ notify: true
+ },
+ selectedDataset: Number,
+ selectedFile: {
+ type: Object,
+ observer: '_selectedFileChanged'
+ },
+ outGraphHierarchy: {
+ type: Object,
+ readOnly: true, //readonly so outsider can't change this via binding
+ notify: true
+ },
+ outGraph: {
+ type: Object,
+ readOnly: true, //readonly so outsider can't change this via binding
+ notify: true
+ },
+ outGraphName: {
+ type: String,
+ readOnly: true,
+ notify: true
+ }
+ },
+ observers: [
+ '_selectedDatasetChanged(selectedDataset, datasets)'
+ ],
+ _parseAndConstructHierarchicalGraph: function(dataset, pbTxtContent) {
+ var self = this;
+ // Reset the progress bar to 0.
+ self._setProgress({
+ value: 0,
+ msg: ''
+ });
+ var tracker = {
+ setMessage: function(msg) {
+ self._setProgress({
+ value: self.progress.value,
+ msg: msg
+ });
+ },
+ updateProgress: function(value) {
+ self._setProgress({
+ value: self.progress.value + value,
+ msg: self.progress.msg
+ });
+ },
+ reportError: function(msg) {
+ self._setProgress({
+ value: self.progress.value,
+ msg: msg,
+ error: true
+ });
+ },
+ };
+ var statsJson;
+ var dataTracker = tf.getSubtaskTracker(tracker, 30, 'Data');
+ tf.graph.parser.readAndParseData(dataset, pbTxtContent, dataTracker)
+ .then(function(result) {
+ // Build the flat graph (consists only of Op nodes).
+ var nodes = result.nodes;
+ statsJson = result.statsJson;
+
+ // This is the whitelist of inputs on op types that are considered
+ // reference edges. "Assign 0" indicates that the first input to
+ // an OpNode with operation type "Assign" is a reference edge.
+ var refEdges = {};
+ refEdges["Assign 0"] = true;
+ refEdges["AssignAdd 0"] = true;
+ refEdges["AssignSub 0"] = true;
+ refEdges["assign 0"] = true;
+ refEdges["assign_add 0"] = true;
+ refEdges["assign_sub 0"] = true;
+ refEdges["count_up_to 0"] = true;
+ refEdges["ScatterAdd 0"] = true;
+ refEdges["ScatterSub 0"] = true;
+ refEdges["ScatterUpdate 0"] = true;
+ refEdges["scatter_add 0"] = true;
+ refEdges["scatter_sub 0"] = true;
+ refEdges["scatter_update 0"] = true;
+ var buildParams = {
+ enableEmbedding: true,
+ inEmbeddingTypes: ['Const'],
+ outEmbeddingTypes: ['^[a-zA-Z]+Summary$'],
+ refEdges: refEdges
+ };
+ var graphTracker = tf.getSubtaskTracker(tracker, 20,
+ 'Graph');
+ return tf.graph.build(nodes, buildParams, graphTracker);
+ })
+ .then(function(graph) {
+ this._setOutGraph(graph);
+ if (statsJson) {
+ // If there are associated stats, join them with the graph.
+ tf.time('Joining stats info with graph...', function() {
+ tf.graph.joinStatsInfoWithGraph(graph, statsJson);
+ });
+ }
+ var hierarchyParams = {
+ verifyTemplate: true,
+ groupSeries: true,
+ };
+ var hierarchyTracker = tf.getSubtaskTracker(tracker, 50,
+ 'Namespace hierarchy');
+ return tf.graph.hierarchy.build(graph, hierarchyParams, hierarchyTracker);
+ }.bind(this))
+ .then(function(graphHierarchy) {
+ // Update the properties which notify the parent with the
+ // graph hierarchy and whether the data has live stats or not.
+ this._setHasStats(statsJson != null);
+ this._setOutGraphHierarchy(graphHierarchy);
+ }.bind(this))
+ .catch(function(reason) {
+ tracker.reportError("Graph visualization failed: " + reason);
+ });
+ },
+ _selectedDatasetChanged: function(datasetIndex, datasets) {
+ var dataset = datasets[datasetIndex];
+ this._parseAndConstructHierarchicalGraph(dataset);
+ this._setOutGraphName(dataset.name);
+ },
+ _selectedFileChanged: function(e) {
+ if (!e) {
+ return;
+ }
+ var file = e.target.files[0];
+ if (!file) {
+ return;
+ }
+
+ // Clear out the value of the file chooser. This ensures that if the user
+ // selects the same file, we'll re-read it.
+ e.target.value = '';
+
+ var reader = new FileReader();
+
+ reader.onload = function(e) {
+ this._parseAndConstructHierarchicalGraph(null, e.target.result);
+ }.bind(this);
+
+ reader.readAsText(file);
+ }
+});
+</script>
+<dom-module id="tf-graph-style" assetpath="../components/tf-graph/">
+<template>
+<style>
+:host {
+ display: flex;
+ width: 100%;
+}
+
+::content #svg {
+ overflow: hidden;
+ flex: 1;
+}
+
+::content #hidden {
+ position: fixed;
+ top: 0px;
+ visibility: hidden;
+}
+
+
+/* --- Node and annotation-node for Metanode --- */
+
+::content .meta > .nodeshape > rect,
+::content .meta > .annotation-node > rect {
+ cursor: pointer;
+ fill: hsl(0, 0%, 70%);
+}
+
+
+::content .node.meta.highlighted > .nodeshape > rect,
+::content .node.meta.highlighted > .annotation-node > rect {
+ stroke-width: 2;
+}
+
+::content .annotation.meta.highlighted > .nodeshape > rect,
+::content .annotation.meta.highlighted > .annotation-node > rect {
+ stroke-width: 1;
+}
+
+::content .meta.selected > .nodeshape > rect,
+::content .meta.selected > .annotation-node > rect {
+ stroke: red;
+ stroke-width: 2;
+}
+
+::content .node.meta.selected.expanded > .nodeshape > rect,
+::content .node.meta.selected.expanded > .annotation-node > rect {
+ stroke: red;
+ stroke-width: 3;
+}
+
+:content .annotation.meta.selected > .nodeshape > rect,
+:content .annotation.meta.selected > .annotation-node > rect {
+ stroke: red;
+ stroke-width: 2;
+}
+
+::content .node.meta.selected.expanded.highlighted > .nodeshape > rect,
+::content .node.meta.selected.expanded.highlighted > .annotation-node > rect {
+ stroke: red;
+ stroke-width: 4;
+}
+
+
+/* --- Op Node --- */
+
+::content .op > .nodeshape > ellipse,
+::content .op > .annotation-node > ellipse {
+ cursor: pointer;
+ fill: #fff;
+ stroke: #ccc;
+}
+
+::content .op.selected > .nodeshape > ellipse,
+::content .op.selected > .annotation-node > ellipse {
+ stroke: red;
+ stroke-width: 2;
+}
+
+::content .op.highlighted > .nodeshape > ellipse,
+::content .op.highlighted > .annotation-node > ellipse {
+ stroke-width: 2;
+}
+
+/* --- Series Node --- */
+
+/* By default, don't show the series background <rect>. */
+::content .series > .nodeshape > rect {
+ fill: hsl(0, 0%, 70%);
+ fill-opacity: 0;
+ stroke-dasharray: 5, 5;
+ stroke-opacity: 0;
+ cursor: pointer;
+}
+
+/* Once expanded, show the series background <rect> and hide the <use>. */
+::content .series.expanded > .nodeshape > rect {
+ fill-opacity: 0.15;
+ stroke: hsl(0, 0%, 70%);
+ stroke-opacity: 1;
+}
+::content .series.expanded > .nodeshape > use {
+ visibility: hidden;
+}
+
+/**
+ * TODO(jimbo): Simplify this by applying a stable class name to all <g>
+ * elements that currently have either the nodeshape or annotation-node classes.
+ */
+::content .series > .nodeshape > use ,
+::content .series > .annotation-node > use {
+ stroke: #ccc;
+}
+::content .series.highlighted > .nodeshape > use ,
+::content .series.highlighted > .annotation-node > use {
+ stroke-width: 2;
+}
+::content .series.selected > .nodeshape > use ,
+::content .series.selected > .annotation-node > use {
+ stroke: red;
+ stroke-width: 2;
+}
+
+::content .series.selected > .nodeshape > rect {
+ stroke: red;
+ stroke-width: 2;
+}
+
+:content .annotation.series.selected > .annotation-node > use {
+ stroke: red;
+ stroke-width: 2;
+}
+
+/* --- Bridge Node --- */
+::content .bridge > .nodeshape > rect {
+ stroke: #f0f;
+ opacity: 0.2;
+ display: none;
+}
+
+/* --- Structural Elements --- */
+::content .edge > path.edgeline.structural {
+ stroke: #f0f;
+ opacity: 0.2;
+ display: none;
+}
+
+/* --- Series Nodes --- */
+
+/* Hide the rect for a series' annotation. */
+::content .series > .annotation-node > rect {
+ display: none;
+}
+
+/* --- Node label --- */
+
+
+::content .node > text.nodelabel {
+ cursor: pointer;
+ fill: #444;
+}
+
+::content .meta.expanded > text.nodelabel {
+ font-size: 9px;
+}
+
+::content .series > text.nodelabel {
+ font-size: 8px;
+}
+
+::content .op > text.nodelabel {
+ font-size: 6px;
+}
+
+::content .bridge > text.nodelabel {
+ display: none;
+}
+
+::content .node.meta.expanded > text.nodelabel{
+ cursor: normal;
+}
+
+::content .annotation.meta.highlighted > text.annotation-label {
+ fill: #50A3F7;
+}
+
+::content .annotation.meta.selected > text.annotation-label {
+ fill: #4285F4;
+}
+
+/* --- Annotation --- */
+
+/* only applied for annotations that are not summary or constant.
+(.summary, .constant gets overriden below) */
+::content .annotation > .annotation-node > * {
+ stroke-width: 0.5;
+ stroke-dasharray: 1, 1;
+}
+
+::content .annotation.summary > .annotation-node > *,
+::content .annotation.constant > .annotation-node > * {
+ stroke-width: 1;
+ stroke-dasharray: none;
+}
+
+::content .annotation > .annotation-edge {
+ fill: none;
+ stroke: #aaa;
+ stroke-width: 0.5;
+ marker-end: url("#annotation-arrowhead");
+}
+
+::content .annotation > .annotation-edge.refline {
+ marker-start: url("#ref-annotation-arrowhead");
+}
+
+::content .annotation > .annotation-control-edge {
+ stroke-dasharray: 1, 1;
+}
+
+::content #annotation-arrowhead {
+ fill: #aaa;
+}
+
+::content #ref-annotation-arrowhead {
+ fill: #aaa;
+}
+
+::content .annotation > .annotation-label {
+ font-size: 5px;
+ cursor: pointer;
+}
+::content .annotation > .annotation-label.annotation-ellipsis {
+ cursor: default;
+}
+
+/* Hide annotations on expanded meta nodes since they're redundant. */
+::content .expanded > .in-annotations,
+::content .expanded > .out-annotations {
+ display: none;
+}
+
+/* --- Annotation: Constant --- */
+
+::content .constant > .annotation-node > ellipse {
+ cursor: pointer;
+ fill: white;
+ stroke: #848484;
+}
+
+::content .constant.selected > .annotation-node > ellipse {
+ fill: white;
+ stroke: red;
+}
+
+::content .constant.highlighted > .annotation-node > ellipse {
+ stroke-width: 1.5;
+}
+
+/* --- Annotation: Summary --- */
+
+::content .summary > .annotation-node > ellipse {
+ cursor: pointer;
+ fill: #DB4437;
+ stroke: #DB4437;
+}
+
+::content .summary.selected > .annotation-node > ellipse {
+ fill: #A52714;
+ stroke: #A52714;
+}
+
+::content .summary.highlighted > .annotation-node > ellipse {
+ stroke-width: 1.5;
+}
+
+/* --- Edge --- */
+
+::content .edge > path.edgeline {
+ fill: none;
+ marker-end: url("#arrowhead");
+ stroke: #bbb;
+ stroke-linecap: round;
+ stroke-width: 0.75;
+}
+
+::content .edge > path.edgeline.refline {
+ marker-start: url("#ref-arrowhead");
+}
+
+::content #arrowhead {
+ fill: #bbb;
+}
+
+::content #ref-arrowhead {
+ fill: #bbb;
+}
+
+::content .edge .control-dep {
+ stroke-dasharray: 2, 2;
+}
+
+/* --- Group node expand/collapse button --- */
+
+/* Hides expand/collapse buttons when a node isn't expanded or highlighted. Using
+ incredibly small opacity so that the bounding box of the <g> parent still takes
+ this container into account even when it isn't visible */
+::content .node:not(.highlighted):not(.expanded) > .nodeshape > .buttoncontainer {
+ opacity: 0.01;
+}
+::content .node.highlighted > .nodeshape > .buttoncontainer {
+ cursor: pointer;
+}
+::content .buttoncircle {
+ fill: #E7811D;
+}
+::content .buttoncircle:hover {
+ fill: #B96717;
+}
+::content .expandbutton,
+::content .collapsebutton {
+ stroke: white;
+}
+/* Do not let the path elements in the button take pointer focus */
+::content .node > .nodeshape > .buttoncontainer > .expandbutton,
+::content .node > .nodeshape > .buttoncontainer > .collapsebutton {
+ pointer-events: none;
+}
+/* Only show the expand button when a node is collapsed and only show the
+ collapse button when a node is expanded. */
+::content .node.expanded > .nodeshape > .buttoncontainer > .expandbutton {
+ display: none;
+}
+::content .node:not(.expanded) > .nodeshape > .buttoncontainer > .collapsebutton {
+ display: none;
+}
+</style>
+</template>
+</dom-module>
+<dom-module id="tf-graph-minimap" assetpath="../components/tf-graph/">
+<template>
+<style>
+:host {
+ background-color:white;
+ transition: opacity .3s linear;
+ pointer-events: auto;
+}
+
+:host.hidden {
+ opacity: 0;
+ pointer-events: none;
+}
+
+canvas {
+ border: 1px solid #999;
+}
+
+rect {
+ fill: white;
+ stroke: #111111;
+ stroke-width: 1px;
+ fill-opacity: 0;
+ filter: url("#minimapDropShadow");
+ cursor: move;
+}
+
+svg {
+ position: absolute;
+}
+</style>
+<svg>
+ <defs>
+ <filter id="minimapDropShadow" x="-20%" y="-20%" width="150%" height="150%">
+ <feOffset result="offOut" in="SourceGraphic" dx="1" dy="1"></feOffset>
+ <feColorMatrix result="matrixOut" in="offOut" type="matrix" values="0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.1 0 0 0 0 0 0.5 0"></feColorMatrix>
+ <feGaussianBlur result="blurOut" in="matrixOut" stdDeviation="2"></feGaussianBlur>
+ <feBlend in="SourceGraphic" in2="blurOut" mode="normal"></feBlend>
+ </filter>
+ </defs>
+ <rect></rect>
+</svg>
+<canvas class="first"></canvas>
+
+<canvas class="second"></canvas>
+</template>
+<script>
+Polymer({
+ is: 'tf-graph-minimap',
+
+ /**
+ * Initializes the minimap and returns a minimap object to notify when
+ * things update.
+ *
+ * @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 maxWandH The maximum width/height for the minimap.
+ * @param labelPadding Padding in pixels due to the main graph labels.
+ */
+ init: function(svg, zoomG, mainZoom, maxWAndH, labelPadding) {
+ return new tf.scene.Minimap(svg, zoomG, mainZoom, this, maxWAndH,
+ labelPadding);
+ }
+});
+</script>
+</dom-module>
+<dom-module id="tf-graph-scene" assetpath="../components/tf-graph/">
+<template>
+<style include="tf-graph-style">
+ :host {
+ font-size: 20px;
+ }
+ .titleContainer {
+ position: relative;
+ }
+ .title {
+ position: absolute;
+ }
+ .auxTitle {
+ position: absolute;
+ }
+ #minimap {
+ position: absolute;
+ right: 20px;
+ bottom: 20px;
+ }
+</style>
+<div class="titleContainer">
+ <div id="title" class="title">Main Graph</div>
+ <div id="auxTitle" class="auxTitle">Auxiliary nodes</div>
+</div>
+<svg id="svg">
+ <defs>
+
+ <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
+ <path d="M 0,0 L 10,5 L 0,10 C 3,7 3,3 0,0"></path>
+ </marker>
+ <marker id="ref-arrowhead" markerWidth="10" markerHeight="10" refX="1" refY="5" orient="auto">
+ <path d="M 10,0 L 0,5 L 10,10 C 7,7 7,3 10,0"></path>
+ </marker>
+
+ <marker id="annotation-arrowhead" markerWidth="5" markerHeight="5" refX="5" refY="2.5" orient="auto">
+ <path d="M 0,0 L 5,2.5 L 0,5 L 0,0"></path>
+ </marker>
+ <marker id="ref-annotation-arrowhead" markerWidth="5" markerHeight="5" refX="0" refY="2.5" orient="auto">
+ <path d="M 5,0 L 0,2.5 L 5,5 L 5,0"></path>
+ </marker>
+
+ <ellipse id="op-node-stamp" rx="7.5" ry="3" stroke="inherit" fill="inherit"></ellipse>
+
+ <ellipse id="op-node-annotation-stamp" rx="5" ry="2" stroke="inherit" fill="inherit"></ellipse>
+
+ <g id="op-series-vertical-stamp">
+ <use xlink:href="#op-node-stamp" x="8" y="9"></use>
+ <use xlink:href="#op-node-stamp" x="8" y="6"></use>
+ <use xlink:href="#op-node-stamp" x="8" y="3"></use>
+ </g>
+
+ <g id="op-series-horizontal-stamp">
+ <use xlink:href="#op-node-stamp" x="16" y="4"></use>
+ <use xlink:href="#op-node-stamp" x="12" y="4"></use>
+ <use xlink:href="#op-node-stamp" x="8" y="4"></use>
+ </g>
+
+ <g id="op-series-annotation-stamp">
+ <use xlink:href="#op-node-annotation-stamp" x="9" y="2"></use>
+ <use xlink:href="#op-node-annotation-stamp" x="7" y="2"></use>
+ <use xlink:href="#op-node-annotation-stamp" x="5" y="2"></use>
+ </g>
+
+ <g id="linearGradients"></g>
+ </defs>
+
+ <rect fill="white" width="10000" height="10000"></rect>
+ <g id="root"></g>
+</svg>
+<tf-graph-minimap id="minimap"></tf-graph-minimap>
+</template>
+</dom-module>
+<script>
+Polymer({
+ is: 'tf-graph-scene',
+ properties: {
+ graphHierarchy: Object,
+ name: String,
+ colorBy: {
+ type: String,
+ observer: '_colorByChanged'
+ },
+ /** @type {d3_zoom} d3 zoom object */
+ _zoom: Object,
+ highlightedNode: {
+ type: String,
+ observer: '_highlightedNodeChanged'
+ },
+ selectedNode: {
+ type: String,
+ observer: '_selectedNodeChanged'
+ },
+ /** Keeps track of if the graph has been zoomed/panned since loading */
+ _zoomed: {
+ type: Boolean,
+ observer: '_onZoomChanged',
+ value: false
+ },
+ /** Keeps track of the starting coordinates of a graph zoom/pan */
+ _zoomStartCoords: {
+ type: Array,
+ value: null
+ },
+ /** Keeps track of the current coordinates of a graph zoom/pan */
+ _zoomCoords: {
+ type: Array,
+ value: null
+ },
+ /** Maximum distance of a zoom event for it to be interpreted as a click */
+ _maxZoomDistanceForClick: {
+ type: Number,
+ value: 20
+ },
+ /**
+ * @type {d3.scale.ordinal}
+ * Scale mapping from template name to a number between 0 and N-1
+ * where N is the number of different template names.
+ */
+ templateIndex: Object,
+ /**
+ * @type {tf.scene.Minimap}
+ * A minimap object to notify for zoom events.
+ */
+ minimap: Object,
+ /*
+ * Dictionary for easily stylizing nodes when state changes.
+ * _nodeGroupIndex[nodeName] = d3_selection of the nodeGroup
+ */
+ _nodeGroupIndex: {
+ type: Object,
+ value: function() { return {}; }
+ },
+ /*
+ * Dictionary for easily stylizing annotation nodes when state changes.
+ * _annotationGroupIndex[nodeName][hostNodeName] =
+ * d3_selection of the annotationGroup
+ */
+ _annotationGroupIndex: {
+ type: Object,
+ value: function() { return {}; }
+ },
+ /*
+ * Dictionary for easily stylizing edges when state changes.
+ * _edgeGroupIndex[edgeName] = d3_selection of the edgeGroup
+ */
+ _edgeGroupIndex: {
+ type: Object,
+ value: function() { return {}; }
+ },
+ /**
+ * Max font size for metanode label strings.
+ */
+ maxMetanodeLabelLengthFontSize: {
+ type: Number,
+ value: 9
+ },
+ /**
+ * Min font size for metanode label strings.
+ */
+ minMetanodeLabelLengthFontSize: {
+ type: Number,
+ value: 6
+ },
+ /**
+ * Metanode label strings longer than this are given smaller fonts.
+ */
+ maxMetanodeLabelLengthLargeFont: {
+ type: Number,
+ value: 11
+ },
+ /**
+ * Metanode label strings longer than this are truncated with ellipses.
+ */
+ maxMetanodeLabelLength: {
+ type: Number,
+ value: 18
+ },
+ progress: Object
+ },
+ observers: [
+ '_buildAndFit(graphHierarchy)'
+ ],
+ getNode: function(nodeName) {
+ return this.graphHierarchy.getRenderNodeByName(nodeName);
+ },
+ isNodeExpanded: function(node) {
+ return node.expanded;
+ },
+ setNodeExpanded: function(renderNode) {
+ this._build(this.graphHierarchy);
+ },
+ /**
+ * Resets the state of the component. Called whenever the whole graph
+ * (dataset) changes.
+ */
+ _resetState: function() {
+ // Reset the state of the component.
+ this._nodeGroupIndex = {};
+ this._annotationGroupIndex = {};
+ this._edgeGroupIndex = {};
+ this._updateLabels(false);
+ // Remove all svg elements under the 'root' svg group.
+ d3.select(this.$.svg).select('#root').selectAll('*').remove();
+ // And the defs.
+ d3.select(this.$.svg).select('defs #linearGradients')
+ .selectAll('*').remove();
+ },
+ /** Main method for building the scene */
+ _build: function(graphHierarchy) {
+ if (!graphHierarchy) { return; } //handle untruthy input
+ var templateNames = d3.keys(graphHierarchy.hierarchy.templates);
+
+ this.templateIndex = d3.scale.ordinal()
+ .domain(templateNames)
+ .range(d3.range(0, templateNames.length));
+ tf.time('tf-graph-scene (layout):', function() {
+ // layout the scene for this meta / series node
+ tf.graph.layout.scene(graphHierarchy.root, this);
+ }.bind(this));
+
+ tf.time('tf-graph-scene (build scene):', function() {
+ tf.graph.scene.buildGroup(d3.select(this.$.root), graphHierarchy.root, this);
+ tf.graph.scene.addGraphClickListener(this.$.svg, this);
+ }.bind(this));
+ // Update the minimap again when the graph is done animating.
+ setTimeout(function() {
+ this.minimap.update();
+ }.bind(this), tf.graph.layout.PARAMS.animation.duration);
+ },
+ ready: function() {
+ this._zoom = d3.behavior.zoom()
+ .on('zoomend', function() {
+ if (this._zoomStartCoords) {
+ // Calculate the total distance dragged during the zoom event.
+ // If it is sufficiently small, then fire an event indicating
+ // that zooming has ended. Otherwise wait to fire the zoom end
+ // event, so that a mouse click registered as part of this zooming
+ // is ignored (as this mouse click was part of a zooming, and should
+ // not be used to indicate an actual click on the graph).
+ var dragDistance = Math.sqrt(
+ Math.pow(this._zoomStartCoords[0] - this._zoomCoords[0], 2) +
+ Math.pow(this._zoomStartCoords[1] - this._zoomCoords[1], 2));
+ if (dragDistance < this._maxZoomDistanceForClick) {
+ this._fireEnableClick();
+ } else {
+ setTimeout(this._fireEnableClick.bind(this), 50);
+ }
+ }
+ this._zoomStartCoords = null;
+ }.bind(this))
+ .on('zoom', function() {
+ // Store the coordinates of the zoom event
+ this._zoomCoords = d3.event.translate;
+
+ // If this is the first zoom event after a zoom-end, then
+ // store the coordinates as the start coordinates as well,
+ // and fire an event to indicate that zooming has started.
+ // This doesn't use the zoomstart event, as d3 sends this
+ // event on mouse-down, even if there has been no dragging
+ // done to translate the graph around.
+ if (!this._zoomStartCoords) {
+ this._zoomStartCoords = this._zoomCoords.slice();
+ this.fire('disable-click');
+ }
+ this._zoomed = true;
+ d3.select(this.$.root).attr('transform',
+ 'translate(' + d3.event.translate + ')' +
+ 'scale(' + d3.event.scale + ')');
+ // Notify the minimap.
+ this.minimap.zoom(d3.event.translate, d3.event.scale);
+ }.bind(this));
+ d3.select(this.$.svg).call(this._zoom)
+ .on('dblclick.zoom', null);
+ d3.select(window).on('resize', function() {
+ // Notify the minimap that the user's window was resized.
+ // The minimap will figure out the new dimensions of the main svg
+ // and will use the existing translate and scale params.
+ this.minimap.zoom();
+ }.bind(this));
+ // Initialize the minimap.
+ this.minimap = this.$.minimap.init(this.$.svg, this.$.root, this._zoom,
+ tf.graph.layout.PARAMS.minimap.size,
+ tf.graph.layout.PARAMS.subscene.meta.labelHeight);
+ },
+ _buildAndFit: function(graphHierarchy) {
+ this._resetState();
+ this._build(graphHierarchy);
+ // Fit to screen after the graph is done animating.
+ setTimeout(this.fit.bind(this), tf.graph.layout.PARAMS.animation.duration);
+ },
+ _updateLabels: function(showLabels) {
+ var titleStyle = this.getElementsByClassName('title')[0].style;
+ var auxTitleStyle = this.getElementsByClassName('auxTitle')[0].style;
+ var core = this.getElementsByClassName(tf.graph.scene.Class.Scene.CORE)[0];
+ // Only show labels if the graph is fully loaded.
+ if (showLabels && core && this.progress && this.progress.value === 100) {
+ var aux =
+ this.getElementsByClassName(tf.graph.scene.Class.Scene.INEXTRACT)[0] ||
+ this.getElementsByClassName(tf.graph.scene.Class.Scene.OUTEXTRACT)[0];
+ var coreX = core.getCTM().e;
+ var auxX = aux ? aux.getCTM().e : null;
+ titleStyle.display = 'inline';
+ titleStyle.left = coreX + 'px';
+ if (auxX !== null && auxX !== coreX) {
+ auxTitleStyle.display = 'inline';
+ auxTitleStyle.left = auxX + 'px';
+ } else {
+ auxTitleStyle.display = 'none';
+ }
+ } else {
+ titleStyle.display='none';
+ auxTitleStyle.display = 'none';
+ }
+ },
+
+
+
+
+ /**
+ * Called whenever the user changed the 'color by' option in the
+ * UI controls.
+ */
+ _colorByChanged: function() {
+ // We iterate through each svg node and update its state.
+ _.each(this._nodeGroupIndex, function(nodeGroup, nodeName) {
+ this._updateNodeState(nodeName);
+ }, this);
+ // Notify also the minimap.
+ this.minimap.update();
+ },
+ fit: function() {
+ tf.graph.scene.fit(this.$.svg, this.$.root, this._zoom, function() {
+ this._zoomed = false;
+ }.bind(this));
+ },
+ isNodeSelected: function(n) {
+ return n === this.selectedNode;
+ },
+ isNodeHighlighted: function(n) {
+ return n === this.highlightedNode;
+ },
+ addAnnotationGroup: function(a, d, selection) {
+ var an = a.node.name;
+ this._annotationGroupIndex[an] = this._annotationGroupIndex[an] || {};
+ this._annotationGroupIndex[an][d.node.name] = selection;
+ },
+ getAnnotationGroupsIndex: function(a) {
+ return this._annotationGroupIndex[a];
+ },
+ removeAnnotationGroup: function(a, d) {
+ delete this._annotationGroupIndex[a.node.name][d.node.name];
+ },
+ addNodeGroup: function(n, selection) {
+ this._nodeGroupIndex[n] = selection;
+ },
+ getNodeGroup: function(n) {
+ return this._nodeGroupIndex[n];
+ },
+ removeNodeGroup: function(n) {
+ delete this._nodeGroupIndex[n];
+ },
+ addEdgeGroup: function(n, selection) {
+ this._edgeGroupIndex[e] = selection;
+ },
+ getEdgeGroup: function(e) {
+ return this._edgeGroupIndex[e];
+ },
+ /**
+ * Update node and annotation node of the given name.
+ * @param {String} n node name
+ */
+ _updateNodeState: function(n) {
+ var node = this.getNode(n);
+ var nodeGroup = this.getNodeGroup(n);
+
+ if (nodeGroup) {
+ tf.graph.scene.node.stylize(nodeGroup, node, this);
+ }
+
+ var annotationGroupIndex = this.getAnnotationGroupsIndex(n);
+ _.each(annotationGroupIndex, function(aGroup, hostName) {
+ tf.graph.scene.node.stylize(aGroup, node, this,
+ tf.graph.scene.Class.Annotation.NODE);
+ }, this);
+ },
+
+ _selectedNodeChanged: function(selectedNode, oldSelectedNode) {
+ if (selectedNode === oldSelectedNode) {
+ return;
+ }
+
+ if (selectedNode) {
+ this._updateNodeState(selectedNode);
+ }
+ if (oldSelectedNode) {
+ this._updateNodeState(oldSelectedNode);
+ }
+
+ if (!selectedNode) {
+ return;
+ }
+ // Update the minimap to reflect the highlighted (selected) node.
+ this.minimap.update();
+ var node = this.graphHierarchy.hierarchy.node(selectedNode);
+ var nodeParents = [];
+ // Create list of all metanode parents of the selected node.
+ while (node.parentNode != null
+ && node.parentNode.name != tf.graph.ROOT_NAME) {
+ node = node.parentNode;
+ nodeParents.push(node.name);
+ }
+ // Ensure each parent metanode is built and expanded.
+ var topParentNodeToBeExpanded;
+ _.forEachRight(nodeParents, function(parentName) {
+ this.graphHierarchy.buildSubhierarchy(parentName);
+ var renderNode = this.graphHierarchy.getRenderNodeByName(parentName);
+ if (renderNode.node.isGroupNode && !renderNode.expanded) {
+ renderNode.expanded = true;
+ if (!topParentNodeToBeExpanded) {
+ topParentNodeToBeExpanded = renderNode;
+ }
+ }
+ }, this);
+ // If any expansion was needed to display this selected node, then
+ // inform the scene of the top-most expansion.
+ if (topParentNodeToBeExpanded) {
+ this.setNodeExpanded(topParentNodeToBeExpanded);
+ this._zoomed = true;
+ }
+
+ if (tf.graph.scene.panToNode(selectedNode, this.$.svg, this.$.root,
+ this._zoom)) {
+ this._zoomed = true;
+ }
+ },
+ _highlightedNodeChanged: function(highlightedNode, oldHighlightedNode) {
+ if (highlightedNode === oldHighlightedNode) {
+ return;
+ }
+
+ if (highlightedNode) {
+ this._updateNodeState(highlightedNode);
+ }
+ if (oldHighlightedNode) {
+ this._updateNodeState(oldHighlightedNode);
+ }
+ },
+ _onZoomChanged: function() {
+ this._updateLabels(!this._zoomed);
+ },
+ _fireEnableClick: function() {
+ this.fire('enable-click');
+ },
+});
+</script>
+<dom-module id="tf-graph-params" assetpath="../components/tf-graph/">
+</dom-module>
+<script>
+ Polymer({
+
+ is: 'tf-graph-params',
+
+ properties: {
+ // PARAMETERS
+
+ enableExtraction: {
+ type: Boolean,
+ value: true
+ },
+
+ /** Maximum in-degree that a node can have without being considered as
+ * high in-degree node. */
+ maxInDegree: {
+ type: Number,
+ value: 4
+ },
+ /** Maximum out-degree that a node can have without being considered as
+ * high out-degree node. */
+ maxOutDegree: {
+ type: Number,
+ value: 4
+ },
+ /** Maximum number of control edges a node can have before they aren't
+ * displayed. */
+ maxControlDegree: {
+ type: Number,
+ value: 4
+ },
+
+ /**
+ * Types patterns for predefined out-extract nodes, which are
+ * sink-like nodes that will be extracted from the main graph.
+ */
+ outExtractTypes: {
+ type: Array,
+ value: function() {
+ return [
+ 'NoOp' // for "sgd", "momentum" group
+ ];
+ }
+ },
+
+ /**
+ * Types patterns for predefined in-extract nodes, which are
+ * source-like nodes that will be extracted from the main graph.
+ */
+ inExtractTypes: {
+ type: Array,
+ value: function() {
+ return ['Variable'];
+ }
+ },
+
+ /**
+ * When removing edges from a high degree node, remove all of its edges if
+ * detachAllEdgesForHighDegree is true. Otherwise remove all in-edges if
+ * the node has high in-degree, or all out-edges if the node has high
+ * out-degree.
+ */
+ detachAllEdgesForHighDegree: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * After extracting high in/out degree nodes and predefined
+ * source-like/sink-like, extract isolated nodes to the side
+ * if this extractIsolatedNodesWithAnnotationsOnOneSide is true.
+ */
+ extractIsolatedNodesWithAnnotationsOnOneSide: {
+ type: Boolean,
+ value: true
+ },
+
+ /**
+ * Whether to draw bridge paths inside of expanded group nodes.
+ */
+ enableBridgegraph: {
+ type: Boolean,
+ value: true
+ },
+
+ /**
+ * Colors for the minimum and maximum values whenever we have a gradient
+ * scale.
+ */
+ minMaxColors: {
+ type: Array,
+ value: function() {
+ return ["#fff5f0", "#fb6a4a"];
+ }
+ },
+
+ /**
+ * Maximum number of annotations to be displayed on a node before an
+ * ellipsis is used.
+ */
+ maxAnnotations: {
+ type: Number,
+ value: 5
+ }
+ }
+ });
+</script>
+<dom-module id="tf-graph" assetpath="../components/tf-graph/">
+<template>
+<style>
+.container {
+ width: 100%;
+ height: 100%;
+}
+
+.vertical {
+ width:100%;
+ height:100%;
+ @apply(--layout-vertical);
+}
+
+.auto {
+ @apply(--layout-flex-auto);
+ @apply(--layout-vertical);
+}
+
+h2 {
+ text-align: center;
+}
+
+paper-button {
+ text-transform: none;
+}
+</style>
+<div class="container">
+ <tf-graph-params id="graphParams"></tf-graph-params>
+ <div class="vertical">
+ <h2>[[title]]</h2>
+ <tf-graph-scene id="scene" class="auto" graph-hierarchy="[[_renderHierarchy]]" highlighted-node="[[_getVisible(highlightedNode)]]" selected-node="[[selectedNode]]" color-by="[[colorBy]]" name="[[graphName]]" progress="[[progress]]"></tf-graph-scene>
+ </div>
+</div>
+</template>
+</dom-module>
+
+<script>
+Polymer({
+
+ is: 'tf-graph',
+
+ properties: {
+ graphHierarchy: {
+ type: Object,
+ notify: true,
+ observer: '_graphChanged'
+ },
+ title: String,
+ selectedNode: {
+ type: String,
+ notify: true,
+ },
+ highlightedNode: {
+ type: String,
+ notify: true
+ },
+ /** What to color the nodes by (compute time, memory, device etc.) */
+ colorBy: String,
+ colorByParams: {
+ type: Object,
+ notify: true,
+ readOnly: true, // Produces and doesn't consume.
+ },
+ // internal properties
+ _graphParams: {
+ type: Object,
+ value: function() {
+ return this.$.graphParams;
+ }
+ },
+ _renderDepth: {
+ type: Number,
+ value: 1
+ },
+ _renderHierarchy: {
+ type: Object,
+ readOnly: true,
+ notify: true,
+ computed: '_buildRenderHierarchy(graphHierarchy, _graphParams)'
+ },
+ _allowGraphSelect: {
+ type: Boolean,
+ value: true
+ }
+ },
+ _buildRenderHierarchy: function(graphHierarchy, params) {
+ return tf.time('new tf.graph.render.Hierarchy', function() {
+ if (graphHierarchy.root.type !== tf.graph.NodeType.META) {
+ // root must be metanode but sometimes Polymer's dom-if has not
+ // remove tf-graph element yet in <tf-node-info>
+ // and thus mistakenly pass non-metanode to this module.
+ return;
+ }
+ var renderGraph = new tf.graph.render.RenderGraphInformation(
+ graphHierarchy, params);
+ // Producing the 'color by' parameters to be consumed
+ // by the tf-graph-controls panel. It contains information about the
+ // min and max values and their respective colors, as well as list
+ // of devices with their respective colors.
+
+ function getColorParamsFromScale(scale) {
+ return {
+ minValue: scale.domain()[0],
+ maxValue: scale.domain()[1],
+ startColor: scale.range()[0],
+ endColor: scale.range()[1]
+ };
+ }
+
+ this._setColorByParams({
+ compute_time: getColorParamsFromScale(renderGraph.computeTimeScale),
+ memory: getColorParamsFromScale(renderGraph.memoryUsageScale),
+ device: _.map(renderGraph.deviceColorMap.domain(),
+ function(deviceName) {
+ return {
+ device: deviceName,
+ color: renderGraph.deviceColorMap(deviceName)
+ };
+ })
+ });
+ return renderGraph;
+ }.bind(this));
+ },
+ _getVisible: function(name) {
+ if (!name) {
+ return name;
+ }
+ return this._renderHierarchy.getNearestVisibleAncestor(name);
+ },
+ listeners: {
+ 'graph-select': '_graphSelected',
+ 'disable-click': '_disableClick',
+ 'enable-click': '_enableClick',
+ // Nodes
+ 'node-toggle-expand': '_nodeToggleExpand',
+ 'node-select': '_nodeSelected',
+ 'node-highlight': '_nodeHighlighted',
+ 'node-unhighlight': '_nodeUnhighlighted',
+
+ // Annotations
+
+ /* Note: currently highlighting/selecting annotation node has the same
+ * behavior as highlighting/selecting actual node so we point to the same
+ * set of event listeners. However, we might redesign this to be a bit
+ * different.
+ */
+ 'annotation-select': '_nodeSelected',
+ 'annotation-highlight': '_nodeHighlighted',
+ 'annotation-unhighlight': '_nodeUnhighlighted',
+ },
+ _graphChanged: function() {
+ // When a new graph is loaded, fire this event so that there is no
+ // info-card being displayed for the previously-loaded graph.
+ this.fire('graph-select');
+ },
+ _graphSelected: function(event) {
+ // Graph selection is not allowed during an active zoom event, as the
+ // click seen during a zoom/pan is part of the zooming and does not
+ // indicate a user desire to click on a specific section of the graph.
+ if (this._allowGraphSelect) {
+ this.set('selectedNode', null);
+ }
+ // Reset this variable as a bug in d3 zoom behavior can cause zoomend
+ // callback not to be called if a right-click happens during a zoom event.
+ this._allowGraphSelect = true;
+ },
+ _disableClick: function(event) {
+ this._allowGraphSelect = false;
+ },
+ _enableClick: function(event) {
+ this._allowGraphSelect = true;
+ },
+ _nodeSelected: function(event) {
+ if (this._allowGraphSelect) {
+ this.set('selectedNode', event.detail.name);
+ }
+ // Reset this variable as a bug in d3 zoom behavior can cause zoomend
+ // callback not to be called if a right-click happens during a zoom event.
+ this._allowGraphSelect = true;
+ },
+ _nodeHighlighted: function(event) {
+ this.set('highlightedNode', event.detail.name);
+ },
+ _nodeUnhighlighted: function(event) {
+ this.set('highlightedNode', null);
+ },
+ _nodeToggleExpand: function(event) {
+ var nodeName = event.detail.name;
+ var renderNode = this._renderHierarchy.getRenderNodeByName(nodeName);
+ // Op nodes are not expandable.
+ if (renderNode.node.type === tf.graph.NodeType.OP) {
+ return;
+ }
+ this._renderHierarchy.buildSubhierarchy(nodeName);
+ renderNode.expanded = !renderNode.expanded;
+ this.querySelector('#scene').setNodeExpanded(renderNode);
+ // Also select the expanded node.
+ this._nodeSelected(event);
+ },
+ not: function(x) {
+ return !x;
+ }
+});
+</script>
+<dom-module id="tf-graph-icon" assetpath="../components/tf-graph/">
+ <template>
+ <template is="dom-if" if="[[_isType(node, type, 'OP')]]">
+ <template is="dom-if" if="[[_isConst(node, const)]]">
+ <svg height$="[[height]]" preserveAspectRatio="xMinYMid meet" viewBox="0 0 10 10">
+ <circle fill="white" stroke="#848484" cx="5" cy="5" r="3"></circle>
+ </svg>
+ </template>
+ <template is="dom-if" if="[[_isSummary(node, summary)]]">
+ <img height$="[[height]]" src="[[resolveUrl('../../lib/svg/summary-icon.svg')]]">
+ </template>
+ <template is="dom-if" if="[[_isRegularOp(node, const, summary)]]">
+ <svg height$="[[height]]" preserveAspectRatio="xMinYMid meet" viewBox="0 0 16 8">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#op-node-stamp" fill="white" stroke="#ccc" x="8" y="4"></use>
+ </svg>
+ </template>
+ </template>
+ <template is="dom-if" if="[[_isType(node, type, 'META')]]">
+ <svg height$="[[height]]" preserveAspectRatio="xMinYMid meet" viewBox="0 0 37 16">
+ <rect x="1" y="1" fill="#d9d9d9" stroke="#ccc" stroke-width="2px" height="14" width="35" rx="5" ry="5"></rect>
+ </svg>
+ </template>
+ <template is="dom-if" if="[[_isType(node, type, 'SERIES')]]">
+ <template is="dom-if" if="[[_isVertical(node, vertical)]]">
+ <svg height$="[[height]]" preserveAspectRatio="xMinYMid meet" viewBox="0 0 16 15">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#op-series-vertical-stamp" fill="white" stroke="#ccc" x="0" y="2"></use>
+ </svg>
+ </template>
+ <template is="dom-if" if="[[!_isVertical(node, vertical)]]">
+ <svg height$="[[height]]" preserveAspectRatio="xMinYMid meet" viewBox="0 0 24 10">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#op-series-horizontal-stamp" fill="white" stroke="#ccc" x="0" y="1"></use>
+ </svg>
+ </template>
+ </template>
+ </template>
+
+ <script>
+ (function() {
+ Polymer({
+ is: 'tf-graph-icon',
+
+ properties: {
+ /**
+ * Node to represent with an icon. Optional, but if specified, its
+ * properties override those defined in the type, vertical, const and
+ * summary properties.
+ * @type {tf.graph.Node}
+ */
+ node: {
+ type: Object,
+ value: null
+ },
+
+ /** Type of node to draw. */
+ type: {
+ type: String,
+ value: null
+ },
+
+ /** Direction for series (ignored for other types). */
+ vertical: {
+ type: Boolean,
+ value: false
+ },
+
+ /** Whether the op is Const (ignored for non-ops). */
+ const: {
+ type: Boolean,
+ value: false
+ },
+
+ /** Whether the op is a Summary (ignored for non-ops). */
+ summary: {
+ type: Boolean,
+ value: false
+ },
+
+ /** Height of the SVG element in pixels, used for scaling. */
+ height: {
+ type: Number,
+ value: 20
+ }
+ },
+
+ /**
+ * Test whether the specified node's type, or the literal type string,
+ * match a particular other type.
+ */
+ _isType: function(inputNode, inputType, targetType) {
+ if (inputNode) {
+ return tf.graph.NodeType[inputNode.type] === targetType;
+ }
+ return inputType === targetType;
+ },
+
+ /**
+ * Test whether the specified node should be represented as a vertical
+ * series. Defaults to the value of the vertical property if node is
+ * not specified.
+ */
+ _isVertical: function(inputNode, inputVertical) {
+ if (inputNode) {
+ return inputNode.hasNonControlEdges;
+ }
+ return !!inputVertical;
+ },
+
+ /**
+ * Test whether the specified node is a constant. Defaults to the value
+ * of the const property if node is not specified.
+ */
+ _isConst: function(inputNode, inputConst) {
+ if (inputNode) {
+ return inputNode.op === 'Const';
+ }
+ return !!inputConst;
+ },
+
+ /**
+ * Test whether the specified node is a summary. Defaults to the value
+ * of the summary property if node is not specified.
+ */
+ _isSummary: function(inputNode, inputSummary) {
+ if (inputNode) {
+ return this._isType(inputNode, null, 'OP') &&
+ inputNode.op.substr(-7) === 'Summary';
+ }
+ return !!inputSummary;
+ },
+
+ /**
+ * Test whether the op node is a regular non-summary non-const node.
+ */
+ _isRegularOp: function(inputNode, inputConst, inputSummary) {
+ return !this._isConst(inputNode, inputConst) &&
+ !this._isSummary(inputNode, inputSummary);
+ }
+ });
+ })();
+ </script>
+</dom-module>
+<dom-module id="tf-node-list-item" assetpath="../components/tf-graph-info/">
+ <style>
+ #list-item {
+ width: 100%;
+ color: #565656;
+ font-size: 11pt;
+ font-weight: 400;
+ position: relative;
+ }
+
+ #list-item:hover {
+ background-color: var(--google-yellow-100);
+ }
+
+ .clickable {
+ cursor: pointer;
+ }
+
+ #list-item span {
+ display: block;
+ margin-left: 40px;
+ }
+
+ #list-item.excluded span {
+ color: #999;
+ }
+
+ .node-icon {
+ position: absolute;
+ top: 1px;
+ left: 2px;
+ }
+ </style>
+ <template>
+ <div id="list-item" on-mouseover="_nodeListener" on-mouseout="_nodeListener" on-click="_nodeListener">
+ <tf-graph-icon class="node-icon" node="[[itemNode]]" height="12"></tf-graph-icon>
+ <span title$="[[name]]">[[name]]</span>
+ </div>
+ </template>
+
+ <script>
+ (function() {
+ Polymer({
+ is: 'tf-node-list-item',
+
+ properties: {
+ /**
+ * The Node for the card itself, on which this item is being drawn.
+ * @type {tf.graph.Node}
+ */
+ cardNode: Object,
+ /**
+ * The Node for the item within the card, somehow related to cardNode.
+ * @type {tf.graph.Node}
+ */
+ itemNode: Object,
+ name: String,
+ itemType: {
+ type: String,
+ observer: '_itemTypeChanged'
+ }
+ },
+
+ _itemTypeChanged: function() {
+ if (this.itemType !== 'subnode') {
+ this.$['list-item'].classList.add('clickable');
+ } else {
+ this.$['list-item'].classList.remove('clickable');
+ }
+ },
+
+ _nodeListener: function(event) {
+ // fire node.click/mouseover/mouseout
+ this.fire('node-list-item-' + event.type, {
+ cardNode: this.cardNode.name,
+ nodeName: this.name,
+ type: this.itemType
+ });
+ }
+
+ });
+ })();
+ </script>
+</dom-module>
+<dom-module id="tf-node-info" assetpath="../components/tf-graph-info/">
+ <style>
+ .sub-list-group {
+ padding: 8px 12px 0px;
+ font-weight: 500;
+ font-size: 12pt;
+ }
+
+ .sub-list {
+ max-height: 300px;
+ overflow-y: scroll;
+ }
+
+ .attr-left {
+ float: left;
+ width: 30%;
+ word-wrap: break-word;
+ color: #565656;
+ font-size: 11pt;
+ font-weight: 400;
+ }
+
+ .attr-right {
+ margin-left: 30%;
+ word-wrap: break-word;
+ color: #565656;
+ font-weight: 400;
+ }
+
+ paper-item {
+ padding: 0;
+ background: #e9e9e9;
+ }
+
+ paper-item-body[two-line] {
+ min-height: 0;
+ padding: 8px 12px 4px;
+ }
+
+ .expandedInfo {
+ padding: 0 0 8px;
+ }
+
+ .controlDeps {
+ padding: 0 0 0 8px;
+ }
+
+ .node-name {
+ white-space: normal;
+ word-wrap: break-word;
+ font-size: 14pt;
+ font-weight: 500;
+ }
+
+ .node-icon {
+ float: right;
+ }
+
+ .subtitle {
+ font-size: 12pt;
+ color: #5e5e5e;
+ }
+
+ .controlLine {
+ font-size: 11pt;
+ font-weight: 400;
+ }
+
+ .toggle-button {
+ float: right;
+ max-height: 20px;
+ max-width: 20px;
+ padding: 0;
+ }
+
+ .control-toggle-button {
+ float: left;
+ max-height: 20px;
+ max-width: 20px;
+ padding: 0;
+ }
+ </style>
+ <template>
+ <paper-item>
+ <paper-item-body two-line="">
+ <div>
+ <paper-icon-button icon="{{_getToggleIcon(_expanded)}}" on-click="_toggleExpanded" class="toggle-button">
+ </paper-icon-button>
+ <div class="node-name">[[_getNodeName(nodeName)]]</div>
+ </div>
+ <div secondary="">
+ <tf-graph-icon class="node-icon" node="[[_node]]"></tf-graph-icon>
+ <template is="dom-if" if="{{_node.op}}">
+ <div class="subtitle">
+ Operation:
+ <span>[[_node.op]]</span>
+ </div>
+ </template>
+ <template is="dom-if" if="{{_node.metagraph}}">
+ <div class="subtitle">
+ Subgraph:
+ <span>[[_node.cardinality]]</span> nodes
+ </div>
+ </template>
+ </div>
+ </paper-item-body>
+ </paper-item>
+ <iron-collapse opened="{{_expanded}}">
+ <template is="dom-if" if="{{_expanded}}" restamp="true">
+ <div class="expandedInfo">
+ <div class="sub-list-group attributes">
+ Attributes
+ (<span>[[_attributes.length]]</span>)
+ <iron-list class="sub-list" id="attributesList" items="[[_attributes]]">
+ <template>
+ <div>
+ <div class="attr-left">[[item.key]]</div>
+ <div class="attr-right">[[item.value]]</div>
+ </div>
+ </template>
+ </iron-list>
+ </div>
+
+ <template is="dom-if" if="{{_device}}">
+ <div class="sub-list-group device">
+ <div class="attr-left">Device</div>
+ <div class="attr-right">[[_device]]</div>
+ </div>
+ </template>
+
+ <div class="sub-list-group predecessors">
+ Inputs
+ (<span>[[_totalPredecessors]]</span>)
+ <iron-list class="sub-list" id="inputsList" items="[[_predecessors.regular]]">
+ <template>
+ <tf-node-list-item card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" name="[[item]]" item-type="predecessors">
+ </tf-node-list-item>
+ </template>
+ </iron-list>
+ <template is="dom-if" if="[[_predecessors.control.length]]">
+ <div class="controlDeps">
+ <div class="controlLine">
+ <paper-icon-button icon="{{_getToggleIcon(_openedControlPred)}}" on-click="_toggleControlPred" class="control-toggle-button">
+ </paper-icon-button>
+ Control dependencies
+ </div>
+ <iron-collapse opened="{{_openedControlPred}}">
+ <template is="dom-if" if="{{_openedControlPred}}" restamp="true">
+ <iron-list class="sub-list" items="[[_predecessors.control]]">
+ <template>
+ <tf-node-list-item card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" name="[[item]]" item-type="predecessors">
+ </tf-node-list-item>
+ </template>
+ </iron-list>
+ </template>
+ </iron-collapse>
+ </div>
+ </template>
+ </div>
+
+ <div class="sub-list-group successors">
+ Outputs
+ (<span>[[_totalSuccessors]]</span>)
+ <iron-list class="sub-list" id="outputsList" items="[[_successors.regular]]">
+ <template>
+ <tf-node-list-item card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" name="[[item]]" item-type="successor">
+ </tf-node-list-item>
+ </template>
+ </iron-list>
+ <template is="dom-if" if="[[_successors.control.length]]">
+ <div class="controlDeps">
+ <div class="controlLine">
+ <paper-icon-button icon="{{_getToggleIcon(_openedControlSucc)}}" on-click="_toggleControlSucc" class="control-toggle-button">
+ </paper-icon-button>
+ Control dependencies
+ </div>
+ <iron-collapse opened="{{_openedControlSucc}}">
+ <template is="dom-if" if="{{_openedControlSucc}}" restamp="true">
+ <iron-list class="sub-list" items="[[_successors.control]]">
+ <template>
+ <tf-node-list-item card-node="[[_node]]" item-node="[[_getNode(item, graphHierarchy)]]" name="[[item]]" item-type="successors">
+ </tf-node-list-item>
+ </template>
+ </iron-list>
+ </template>
+ </iron-collapse>
+ </div>
+ </template>
+ </div>
+ </div>
+ </template>
+ </iron-collapse>
+ </template>
+
+ <script>
+ (function() {
+ Polymer({
+ is: 'tf-node-info',
+
+ properties: {
+ nodeName: String,
+ graphHierarchy: Object,
+ _node: {
+ type: Object,
+ computed: '_getNode(nodeName, graphHierarchy)',
+ observer: '_resetState'
+ },
+ _attributes: {
+ type: Array,
+ computed: '_getAttributes(_node)'
+ },
+ _device: {
+ type: String,
+ computed: '_getDevice(_node)'
+ },
+ _successors: {
+ type: Object,
+ computed: '_getSuccessors(_node, graphHierarchy)'
+ },
+ _predecessors: {
+ type: Object,
+ computed: '_getPredecessors(_node, graphHierarchy)'
+ },
+ _subnodes: {
+ type: Array,
+ computed: '_getSubnodes(_node)'
+ },
+ _expanded: {
+ type: Boolean,
+ value: true
+ },
+ _totalPredecessors: {
+ type: Number,
+ computed: '_getTotalPred(_predecessors)'
+ },
+ _totalSuccessors: {
+ type: Number,
+ computed: '_getTotalSucc(_successors)'
+ },
+ _openedControlPred: {
+ type: Boolean,
+ value: false
+ },
+ _openedControlSucc: {
+ type: Boolean,
+ value: false
+ },
+ },
+ expandNode: function() {
+ this.fire('_node.expand', this.node);
+ },
+ _getNode: function(n, graphHierarchy) {
+ return graphHierarchy.node(n);
+ },
+ _getNodeName: function(nodeName) {
+ // Insert a zero-width whitespace character before each slash so that
+ // long node names wrap cleanly at path boundaries.
+ return (nodeName || '').replace(/\//g, '\u200B/');
+ },
+ _getAttributes: function(node) {
+ this.async(this._resizeList.bind(this, "#attributesList"));
+ return node && node.attr ? node.attr.map(function(entry) {
+ return {key: entry.key, value: JSON.stringify(entry.value)};
+ }) : [];
+
+ },
+ _getDevice: function(node) {
+ return node ? node.device : null;
+ },
+ _getSuccessors: function(node, hierarchy) {
+ this.async(this._resizeList.bind(this, "#inputsList"));
+ return node ? hierarchy.getSuccessors(node.name) : [[], []];
+ },
+ _getPredecessors: function(node, hierarchy) {
+ this.async(this._resizeList.bind(this, "#outputsList"));
+ return node ? hierarchy.getPredecessors(node.name) : [[], []];
+ },
+ _getSubnodes: function(node) {
+ return node && node.metagraph ? node.metagraph.nodes() : null;
+ },
+ _getTotalPred: function(predecessors) {
+ return predecessors.regular.length + predecessors.control.length;
+ },
+ _getTotalSucc: function(successors) {
+ return successors.regular.length + successors.control.length;
+ },
+ _toggleControlPred: function() {
+ this._openedControlPred = !this._openedControlPred;
+ },
+ _toggleControlSucc: function() {
+ this._openedControlSucc = !this._openedControlSucc;
+ },
+ _toggleExpanded: function() {
+ this._expanded = !this._expanded;
+ },
+ _getToggleIcon: function(expanded) {
+ return expanded ? "expand-less" : "expand-more";
+ },
+ _resetState: function() {
+ this._openedControlPred = false;
+ this._openedControlSucc = false;
+ },
+ _resizeList: function(selector) {
+ var list = document.querySelector(selector);
+ if (list) {
+ list.fire('iron-resize');
+ }
+ }
+ });
+ })();
+ </script>
+</dom-module>
+<dom-module id="tf-graph-info" assetpath="../components/tf-graph-info/">
+<template>
+<style>
+:host {
+ font-size: 12px;
+ margin: 0;
+ padding: 0;
+ display: block;
+}
+
+h2 {
+ padding: 0;
+ text-align: center;
+ margin: 0;
+}
+</style>
+<template is="dom-if" if="{{selectedNode}}">
+ <paper-material elevation="1" class="card">
+ <tf-node-info graph-hierarchy="[[graphHierarchy]]" flat-graph="[[graph]]" node-name="[[selectedNode]]" highlighted-node="{{highlightedNode}}">
+ </tf-node-info>
+ </paper-material>
+</template>
+</template>
+<script>
+(function() {
+ Polymer({
+ is: 'tf-graph-info',
+
+ properties: {
+ title: String,
+ graphHierarchy: Object,
+ graph: Object,
+ // Two-ways
+ selectedNode: {
+ type: String,
+ notify: true
+ },
+ highlightedNode: {
+ type: String,
+ notify: true
+ }
+ },
+ listeners: {
+ 'node-list-item-click': '_nodeListItemClicked',
+ 'node-list-item-mouseover': '_nodeListItemMouseover',
+ 'node-list-item-mouseout': '_nodeListItemMouseout'
+ },
+ _nodeListItemClicked: function(event) {
+ this.selectedNode = event.detail.nodeName;
+ },
+ _nodeListItemMouseover: function(event) {
+ this.highlightedNode = event.detail.nodeName;
+ },
+ _nodeListItemMouseout: function() {
+ this.highlightedNode = null;
+ }
+ });
+})();
+</script>
+</dom-module>
+<dom-module id="tf-graph-board" assetpath="../components/tf-graph-board/">
+<template>
+<style>
+::host {
+ display: block;
+}
+
+/deep/ .close {
+ position: absolute;
+ cursor: pointer;
+ left: 15px;
+ bottom: 15px;
+}
+
+.container {
+ width: 100%;
+ height: 100%;
+ opacity: 1;
+}
+
+.container.loading {
+ cursor: progress;
+ opacity: 0.1;
+}
+
+.container.loading.error {
+ cursor: auto;
+}
+
+#info {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ padding: 0px;
+ max-width: 380px;
+ min-width: 320px;
+ background-color: rgba(255,255,255,0.9);
+ @apply(--shadow-elevation-2dp);
+}
+
+#main {
+ width: 100%;
+ height: 100%;
+}
+
+#progress-bar {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ position: absolute;
+ top: 40px;
+ left: 0;
+ font-size: 13px;
+}
+
+#progress-msg {
+ width: 400px;
+ margin-bottom: 5px;
+}
+
+paper-progress {
+ width: 400px;
+ --paper-progress-height: 6px;
+ --paper-progress-active-color: #f3913e;
+}
+</style>
+<template is="dom-if" if="[[_isNotComplete(progress)]]">
+ <div id="progress-bar">
+ <div id="progress-msg">[[progress.msg]]</div>
+ <paper-progress value="[[progress.value]]"></paper-progress>
+ </div>
+</template>
+<div class$="[[_getContainerClass(progress)]]">
+ <div id="main">
+ <tf-graph id="graph" graph-hierarchy="[[graphHierarchy]]" selected-node="{{_selectedNode}}" highlighted-node="{{_highlightedNode}}" color-by="[[colorBy]]" color-by-params="{{colorByParams}}" graph-name="[[graphName]]" progress="[[progress]]"></tf-graph>
+ </div>
+ <div id="info">
+ <tf-graph-info id="graph-info" title="selected" graph-hierarchy="[[graphHierarchy]]" graph="[[graph]]" selected-node="{{_selectedNode}}" highlighted-node="{{_highlightedNode}}"></tf-graph-info>
+ </div>
+</div>
+</template>
+</dom-module>
+
+<script>
+Polymer({
+ is: 'tf-graph-board',
+ properties: {
+ // Public API.
+ graphHierarchy: Object,
+ graph: Object,
+ graphName: String,
+ // True if the graph data has also run-time stats.
+ hasStats: Boolean,
+ /**
+ * @type {value: number, msg: string}
+ *
+ * A number between 0 and 100 denoting the % of progress
+ * for the progress bar and the displayed message.
+ */
+ progress: Object,
+ colorByParams: {
+ type: Object,
+ notify: true,
+ },
+ // Private API: Data routing between child components.
+ _selectedNode: String,
+ _highlightedNode: String,
+ },
+ /** True if the progress is not complete yet (< 100 %). */
+ _isNotComplete: function(progress) {
+ return progress.value < 100;
+ },
+ _getContainerClass: function(progress) {
+ var result = 'container';
+ if (progress.error) {
+ result += ' error';
+ }
+ if (this._isNotComplete(progress)) {
+ result += ' loading';
+ }
+ return result;
+ }
+});
+</script>
+<dom-module id="tf-graph-controls" assetpath="../components/tf-graph/">
+<template>
+<style>
+:host {
+ font-size: 12px;
+ color: gray;
+ --paper-font-subhead: {
+ font-size: 14px;
+ color: gray;
+ };
+ --paper-dropdown-menu-icon: {
+ width: 15px;
+ height: 15px;
+ };
+ --paper-dropdown-menu-button: {
+ padding: 0;
+ };
+ --paper-dropdown-menu-input: {
+ padding: 0;
+ };
+ --paper-item-min-height: 30px;
+}
+
+paper-button[raised].keyboard-focus {
+ font-weight: normal;
+}
+
+.run-dropdown {
+ --paper-input-container: {
+ padding: 9px 0 0 25px;
+ };
+}
+
+.color-dropdown {
+ --paper-input-container: {
+ padding: 9px 0 0 13px;
+ };
+}
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+table td {
+ padding: 0;
+ margin: 0;
+}
+
+.allcontrols {
+ padding: 10px;
+}
+
+.legend-holder {
+ position: absolute;
+ bottom: 0;
+ padding-bottom: 10px;
+}
+
+#fit {
+ color: var(--paper-orange-500);
+}
+
+paper-radio-button {
+ padding: 5px;
+}
+svg.icon {
+ width: 60px;
+ height: 18px;
+}
+.icon ellipse {
+ rx: 10px;
+ ry: 5px;
+ stroke: #CCC;
+ stroke-width: 1px;
+ fill: #FFFFFF;
+ cy: 10px;
+}
+.icon rect {
+ height: 14px;
+ width: 35px;
+ rx: 5px;
+ ry: 5px;
+ stroke: #CCC;
+ stroke-width: 2px;
+ fill: #D9D9D9;
+}
+.domainValues {
+ width: 165px;
+}
+.domainStart {
+ float: left;
+}
+.domainEnd {
+ float: right;
+}
+.colorBox {
+ width: 20px;
+}
+
+.image-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.gray {
+ color: #666;
+}
+
+.title {
+ font-size: 16px;
+ margin: 8px 5px 8px 0;
+ color: black;
+}
+.title small {
+ font-weight: normal;
+}
+.deviceList {
+ max-height: 100px;
+ overflow-y: auto;
+}
+
+#file {
+ padding: 8px 0;
+}
+
+.color-text {
+ padding: 0 0 0 55px;
+}
+
+.fit-button-text {
+ text-transform: none;
+ padding: 8px 18px 0 18px;
+ font-size: 14px
+}
+
+.upload-button {
+ width: 165px;
+ height: 25px;
+ text-transform: none;
+ margin-top: 4px;
+}
+
+.fit-button {
+ padding: 2px;
+ width: 30px;
+ height: 30px;
+}
+
+.hidden-input {
+ height: 0px;
+ width: 0px;
+ overflow:hidden;
+}
+
+.allcontrols .control-holder {
+ display: flex;
+ clear: both;
+}
+</style>
+<div class="allcontrols">
+ <div class="control-holder">
+ <paper-icon-button id="fit" icon="aspect-ratio" class="fit-button" on-click="fit" alt="Fit to screen">
+ </paper-icon-button>
+ <paper-button class="fit-button-text" on-click="fit">Fit to screen
+ </paper-button>
+ </div>
+ <div class="control-holder">
+ <div class="title">Run</div>
+ <paper-dropdown-menu no-label-float="" no-animations="" noink="" class="run-dropdown">
+ <paper-menu id="select" class="dropdown-content" selected="{{selectedDataset}}">
+ <template is="dom-repeat" items="[[datasets]]">
+ <paper-item>[[item.name]]</paper-item>
+ </template>
+ </paper-menu>
+ </paper-dropdown-menu>
+ </div>
+ <div class="control-holder">
+ <div class="title">Upload</div>
+ <paper-button raised="" class="text-button upload-button" on-click="_getFile">Choose File</paper-button>
+ <div class="hidden-input">
+ <input type="file" id="file" name="file" on-change="_updateFileInput">
+ </div>
+ </div>
+ <div class="control-holder">
+ <div class="title">Color</div>
+ <paper-dropdown-menu no-label-float="" no-animations="" noink="" class="color-dropdown">
+ <paper-menu class="dropdown-content" selected="{{_colorByIndex}}">
+ <paper-item>Structure</paper-item>
+ <paper-item>Device</paper-item>
+ <template is="dom-if" if="[[hasStats]]">
+ <paper-item>Compute time</paper-item>
+ <paper-item>Memory</paper-item>
+ </template>
+ </paper-menu>
+ </paper-dropdown-menu>
+ </div>
+ <div>
+ <template is="dom-if" if="[[_isGradientColoring(colorBy)]]">
+ <svg width="160" height="20" style="margin: 0 5px" class="color-text">
+ <defs>
+ <linearGradient id="linearGradient" x1="0%" y1="0%" x2="100%" y2="0%">
+ <stop class="start" offset="0%" stop-color$="[[_currentGradientParams.startColor]]"></stop>
+ <stop class="end" offset="100%" stop-color$="[[_currentGradientParams.endColor]]"></stop>
+ </linearGradient>
+ </defs>
+ <rect x="0" y="0" width="160" height="20" fill="url(#linearGradient)" stroke="black"></rect>
+ </svg>
+ <div class="domainValues color-text">
+ <div class="domainStart">[[_currentGradientParams.minValue]]</div>
+ <div class="domainEnd">[[_currentGradientParams.maxValue]]</div>
+ </div>
+ </template>
+ <template is="dom-if" if="[[_equals(colorBy, 'structure')]]">
+ <div class="color-text">
+ color: same substructure<br>
+ gray: unique substructure
+ </div>
+ </template>
+ <template is="dom-if" if="[[_equals(colorBy, 'device')]]">
+ <div class="color-text">
+ <div class="deviceList">
+ <table>
+ <template is="dom-repeat" items="[[colorByParams.device]]">
+ <tr>
+ <td style$="[[_getBackgroundColor(item.color)]]">
+ <div class="colorBox"></div>
+ </td>
+ <td>
+ <div>[[item.device]]</div>
+ </td>
+ </tr>
+ </template>
+ </table>
+ </div>
+ <br>
+ gray: unknown device
+ </div>
+ </template>
+ </div>
+ <div class="legend-holder">
+ <table>
+ <tbody><tr>
+ <td><div class="title">Graph</div></td>
+ <td>(* = expandable)</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon">
+ <rect transform="translate(3, 1)" height="14" width="35" rx="5" ry="5"></rect>
+ </svg>
+ </td>
+ <td>Namespace<span class="gray">*</span></td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" preserveAspectRatio="xMinYMid meet" viewBox="0 0 10 10">
+ <use xlink:href="#op-node-stamp" fill="white" stroke="#ccc" x="9.5" y="6"></use>
+ </svg>
+ </td>
+ <td>OpNode</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" height="15px" preserveAspectRatio="xMinYMid meet" viewBox="0 0 12 12">
+ <use xlink:href="#op-series-horizontal-stamp" fill="white" stroke="#ccc" x="2" y="2"></use>
+ </svg>
+ </td>
+ <td>Unconnected series<span class="gray">*</span></td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" height="15px" preserveAspectRatio="xMinYMid meet" viewBox="0 0 15 15">
+ <use xlink:href="#op-series-vertical-stamp" fill="white" stroke="#ccc" x="2" y="2"></use>
+ </svg>
+ </td>
+ <td>Connected series<span class="gray">*</span></td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon">
+ <circle fill="white" stroke="#848484" cx="10" cy="10" r="5"></circle>
+ </svg>
+ </td>
+ <td>Constant</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="image-icon">
+ <image id="summary-icon" width="24" height="24" x="0" y="0" class="image-icon"></image>
+ </svg>
+ </td>
+ <td>Summary</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" height="15px" preserveAspectRatio="xMinYMid meet" viewBox="0 0 15 15">
+ <defs>
+ <marker id="arrowhead-legend" fill="#bbb" markerWidth="10" markerHeight="10" refX="9" refY="5" orient="auto">
+ <path d="M 0,0 L 10,5 L 0,10 C 3,7 3,3 0,0"></path>
+ </marker>
+ <marker id="ref-arrowhead-legend" fill="#bbb" markerWidth="10" markerHeight="10" refX="1" refY="5" orient="auto">
+ <path d="M 10,0 L 0,5 L 10,10 C 7,7 7,3 10,0"></path>
+ </marker>
+ </defs>
+ <path marker-end="url(#arrowhead-legend)" stroke="#bbb" d="M2 9 l 23 0" stroke-linecap="round"></path>
+ </svg>
+ </td>
+ <td>Dataflow edge</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" height="15px" preserveAspectRatio="xMinYMid meet" viewBox="0 0 15 15">
+ <path marker-end="url(#arrowhead-legend)" stroke="#bbb" d="M2 9 l 23 0" stroke-linecap="round" stroke-dasharray="2, 2"></path>
+ </svg>
+ </td>
+ <td>Control dependency edge</td>
+ </tr>
+ <tr>
+ <td>
+ <svg class="icon" height="15px" preserveAspectRatio="xMinYMid meet" viewBox="0 0 15 15">
+ <path marker-start="url(#ref-arrowhead-legend)" marker-end="url(#arrowhead-legend)" stroke="#bbb" d="M2 9 l 23 0" stroke-linecap="round"></path>
+ </svg>
+ </td>
+ <td>Reference edge</td>
+ </tr>
+ </tbody></table>
+ </div>
+ </div>
+</template>
+<script>
+(function() { // Private scope.
+Polymer({
+ is: 'tf-graph-controls',
+ ready: function() {
+ // Set the url to download the summary icon.
+ d3.select(this.$['summary-icon'])
+ .attr('xlink:href', this.resolveUrl('../../lib/svg/summary-icon.svg'));
+ },
+ properties: {
+ // Public API.
+ hasStats: {
+ type: Boolean
+ },
+ colorBy: {
+ type: String,
+ notify: true,
+ computed: '_getColorBy(_colorByIndex)'
+ },
+ colorByParams: Object,
+ datasets: {
+ type: Array,
+ observer: '_datasetsChanged'
+ },
+ selectedDataset: {
+ type: Number,
+ notify: true,
+ value: 0,
+ },
+ selectedFile: {
+ type: Object,
+ notify: true
+ },
+ // Private API.
+ _colorByIndex: {
+ type: Number,
+ value: 0 // Defaults to 'structure'.
+ },
+ _currentGradientParams: {
+ type: Object,
+ computed: '_getCurrentGradientParams(colorByParams, colorBy)'
+ }
+ },
+ _getColorBy: function(colorByIndex) {
+ return ["structure", "device", "compute_time", "memory"][colorByIndex];
+ },
+ _getBackgroundColor: function(color) {
+ return 'background-color:' + color;
+ },
+ fit: function() {
+ document.querySelector('#scene').fit();
+ },
+ _isGradientColoring: function(colorBy) {
+ return ["compute_time", "memory"].indexOf(colorBy) !== -1;
+ },
+ _equals: function(a, b) {
+ return a === b;
+ },
+ _getCurrentGradientParams: function(colorByParams, colorBy) {
+ if (!this._isGradientColoring(colorBy)) {
+ return;
+ }
+ var params = colorByParams[colorBy];
+ var minValue = params.minValue;
+ var maxValue = params.maxValue;
+ if (colorBy === 'memory') {
+ minValue = convertToHumanReadable(minValue, MEMORY_UNITS);
+ maxValue = convertToHumanReadable(maxValue, MEMORY_UNITS);
+ } else if (colorBy === 'compute_time') {
+ minValue = convertToHumanReadable(minValue, TIME_UNITS);
+ maxValue = convertToHumanReadable(maxValue, TIME_UNITS);
+ }
+ return {
+ minValue: minValue,
+ maxValue: maxValue,
+ startColor: params.startColor,
+ endColor: params.endColor
+ };
+ },
+ _updateFileInput: function(e) {
+ this.set('selectedFile', e);
+ },
+ _datasetsChanged: function(newDatasets, oldDatasets) {
+ if (oldDatasets != null || this.selected == null) {
+ // Select the first dataset by default.
+ this.set('selectedDataset', 0);
+ }
+ },
+ _getFile: function() {
+ this.$.file.click();
+ }
+});
+
+// Private methods.
+var MEMORY_UNITS = [
+ // Atomic unit.
+ {symbol: 'B'},
+ // numUnits specifies how many previous units this unit contains.
+ {symbol: 'KB', numUnits: 1024},
+ {symbol: 'MB', numUnits: 1024},
+ {symbol: 'GB', numUnits: 1024},
+ {symbol: 'TB', numUnits: 1024},
+ {symbol: 'PB', numUnits: 1024}
+];
+var TIME_UNITS = [
+ // Atomic unit. Finest granularity in TensorFlow stat collection.
+ {symbol: 'µs'},
+ // numUnits specifies how many previous units this unit contains.
+ {symbol: 'ms', numUnits: 1000},
+ {symbol: 's', numUnits: 1000},
+ {symbol: 'min', numUnits: 60},
+ {symbol: 'hr', numUnits: 60},
+ {symbol: 'days', numUnits: 24}
+];
+
+/**
+ * Returns the human readable version of the unit.
+ * (e.g. 1.35 GB, 23 MB, 34 ms, 6.53 min etc).
+ */
+function convertToHumanReadable(value, units, unitIndex) {
+ unitIndex = unitIndex == null ? 0 : unitIndex;
+ if (unitIndex + 1 < units.length && value >= units[unitIndex + 1].numUnits) {
+ return convertToHumanReadable(value / units[unitIndex + 1].numUnits,
+ units, unitIndex + 1);
+ }
+ // toPrecision() has the tendency to return a number in scientific
+ // notation and (number - 0) brings it back to normal notation.
+ return (value.toPrecision(3) - 0) + ' ' + units[unitIndex].symbol;
+}
+})(); // Closing private scope.
+</script>
+</dom-module>
+<dom-module id="tf-graph-dashboard" assetpath="../components/tf-graph-dashboard/">
+<template>
+<div id="plumbing">
+ <tf-url-generator out-runs-url="{{_runsUrl}}" out-graph-url-generator="{{_graphUrlGen}}" id="urlGenerator"></tf-url-generator>
+ <tf-run-generator id="runGenerator" url="[[_runsUrl]]" out-runs-with-graph="{{_runsWithGraph}}"></tf-run-generator>
+</div>
+<template is="dom-if" if="[[_datasetsEmpty(_datasets)]]">
+<div class="warning">
+ <p>
+ No graph definition files were found.
+ </p>
+ <p>
+ To store a graph, create a
+ <code>tf.python.training.summary_io.SummaryWriter</code>
+ and pass the graph either via the constructor, or by calling its
+ <code>add_graph()</code> method.
+ </p>
+</div>
+</template>
+<template is="dom-if" if="[[!_datasetsEmpty(_datasets)]]">
+<tf-dashboard-layout>
+<div class="sidebar">
+ <tf-graph-controls id="controls" color-by-params="[[_colorByParams]]" has-stats="[[_hasStats]]" color-by="{{_colorBy}}" ,="" datasets="[[_datasets]]" selected-dataset="{{_selectedDataset}}" selected-file="{{_selectedFile}}"></tf-graph-controls>
+ <tf-graph-loader id="loader" datasets="[[_datasets]]" ,="" selected-dataset="[[_selectedDataset]]" selected-file="[[_selectedFile]]" out-graph-hierarchy="{{_graphHierarchy}}" out-graph="{{_graph}}" out-graph-name="{{_graphName}}" has-stats="{{_hasStats}}" progress="{{_progress}}"></tf-graph-loader>
+</div>
+<div class="center">
+ <tf-graph-board id="graphboard" graph-hierarchy="[[_graphHierarchy]]" graph="[[_graph]]" has-stats="[[_hasStats]]" graph-name="[[_graphName]]" progress="[[_progress]]" color-by="[[_colorBy]]" color-by-params="{{_colorByParams}}">
+ </tf-graph-board>
+</div>
+</tf-dashboard-layout></template>
+<style>
+
+:host /deep/ {
+ font-family: 'Roboto', sans-serif;
+}
+
+.center {
+ height: 100%;
+}
+
+</style>
+<style include="warning-style"></style>
+</template>
+</dom-module>
+
+<script>
+(function() {
+Polymer({
+ is: 'tf-graph-dashboard',
+ properties: {
+ _runsWithGraph: Array,
+ _datasets: {
+ type: Object,
+ computed: '_getDatasets(_runsWithGraph, _graphUrlGen)'
+ }
+ },
+ _getDatasets: function(runsWithGraph, graphUrlGen) {
+ return _.map(runsWithGraph, function(runName) {
+ return {
+ name: runName,
+ path: graphUrlGen(runName)
+ };
+ });
+ },
+ _datasetsEmpty: function(datasets) {
+ return !datasets || !datasets.length;
+ }
+});
+})();
+</script>
+</div><dom-module id="tf-tensorboard">
+ <template>
+ <paper-header-panel>
+ <paper-toolbar id="toolbar">
+ <div id="toolbar-content">
+ <div class="toolbar-title">
+ TensorBoard
+ </div>
+ <div class="right-buttons">
+ <paper-button class="link-button" on-click="chooseEvents" active$="[[eventDashboard(mode)]]" noink="">Events</paper-button>
+ <paper-button class="link-button" on-click="chooseImages" active$="[[imageDashboard(mode)]]" noink="">Images</paper-button>
+ <paper-button class="link-button" on-click="chooseGraphs" active$="[[graphDashboard(mode)]]" noink="">Graph</paper-button>
+ <paper-button class="link-button" on-click="chooseHistograms" active$="[[histogramDashboard(mode)]]" noink="">Histograms</paper-button>
+ </div>
+ </div>
+ </paper-toolbar>
+ <div id="content" class="fit">
+ <template is="dom-if" if="[[eventDashboard(mode)]]">
+ <tf-event-dashboard id="eventDash"></tf-event-dashboard>
+ </template>
+
+ <template is="dom-if" if="[[imageDashboard(mode)]]">
+ <tf-image-dashboard id="imageDash"></tf-image-dashboard>
+ </template>
+
+ <template is="dom-if" if="[[graphDashboard(mode)]]">
+ <tf-graph-dashboard id="graphDash"></tf-graph-dashboard>
+ </template>
+
+ <template is="dom-if" if="[[histogramDashboard(mode)]]">
+ <tf-histogram-dashboard id="histogramDash"></tf-histogram-dashboard>
+ </template>
+ </div>
+ </paper-header-panel>
+ <style>
+ #toolbar {
+ background-color: var(--tb-orange-strong);
+ background-image: radial-gradient(ellipse, var(--tb-orange-weak), var(--tb-orange-strong));
+ }
+ #toolbar-content {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .toolbar-title {
+ font-size: 30px;
+ }
+ #content {
+ height: 100%;
+ }
+ .link-button {
+ height: 30px;
+ }
+ [active] {
+ font-weight: bold;
+ }
+ :host {
+ height: 100%;
+ display: block;
+ }
+ </style>
+ </template>
+ <script>
+ Polymer({
+ is: "tf-tensorboard",
+ properties: {
+ mode: {
+ type: String,
+ value: "events",
+ },
+ },
+ chooseEvents: function() {
+ this.mode = "events";
+ },
+ chooseImages: function() {
+ this.mode = "images";
+ },
+ chooseGraphs: function() {
+ this.mode = "graphs";
+ },
+ chooseHistograms: function() {
+ this.mode = "histograms";
+ },
+ eventDashboard: function(mode) {
+ return mode === "events";
+ },
+ imageDashboard: function(mode) {
+ return mode === "images";
+ },
+ graphDashboard: function(mode) {
+ return mode === "graphs";
+ },
+ histogramDashboard: function(mode) {
+ return mode === "histograms";
+ }
+ });
+ </script>
+</dom-module>
+</body></html> \ No newline at end of file