diff options
Diffstat (limited to 'tensorflow/tensorboard/dist/tf-tensorboard.html')
-rw-r--r-- | tensorflow/tensorboard/dist/tf-tensorboard.html | 10484 |
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 |