diff options
Diffstat (limited to 'tensorflow/tensorboard/components/tf-graph')
8 files changed, 2053 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html b/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html new file mode 100644 index 0000000000..d6e736d185 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/demo/tf-graph-demo.html @@ -0,0 +1,185 @@ +<link rel="import" href="../../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../tf-graph-board/tf-graph-board.html"> +<link rel="import" href="../../tf-graph-loader/tf-graph-loader.html"> +<link rel="import" href="../../tf-graph/tf-graph-controls.html"> +<!-- Element for tf-graph demo page + +Example + +<tf-graph-demo></tf-graph-demo> + +--> + +<dom-module id="tf-graph-demo"> +<template> +<style> + +:host /deep/ { + font-family: 'Roboto', sans-serif; +} + +.main { + position: absolute; + right: 0; + left: 250px; + height: 100%; +} + +.side { + position: absolute; + left: 0; + width: 250px; + height: 100%; + border: 1px solid black; + box-sizing: border-box; +} + +.all { + position: relative; + width: 100%; + height: 100% +} + +</style> +<div class="all"> + <div class="side"> + <tf-graph-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="main"> + <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> +</div> +</template> +</dom-module> + +<script> +(function(){ + +var datasets = [ + { + name: "Mnist Eval", + path: "mnist_eval.pbtxt", + }, + { + name: "Mnist Train (with stats)", + path: "mnist_train.pbtxt", + statsPath: "mnist_train_stats.json" + }, + { + name: "Inception Train (huge)", + path: "inception_train.pbtxt", + }, + { + name: "Inception Train Eval", + path: "inception_train_eval.pbtxt", + }, + { + name: "Inception Test", + path: "inception_test_eval.pbtxt", + }, + { + name: "PTB Word LSTM Train", + path: "ptb_word_lstm_train.pbtxt", + }, + { + name: "PTB Word LSTM Train Eval", + path: "ptb_word_lstm_train_eval.pbtxt", + }, + { + name: "PTB Word LSTM Test", + path: "ptb_word_lstm_test_eval.pbtxt", + }, + { + name: "Cifar10 Train", + path: "cifar10_train.pbtxt", + }, + { + name: "Cifar10 Multi-GPU Train", + path: "cifar10_multi_gpu_train.pbtxt", + }, + { + name: "Cifar10 Eval", + path: "cifar10_eval.pbtxt", + }, + { + name: "Fatcat LSTM", + path: "fatcat_lstm.pbtxt", + }, + { + name: "Legacy Inception Renamed", + path: "legacy_inception_renamed.pbtxt", + }, + { + name: "Wolfe (Broken)", + path: "wolfe1.pbtxt", + }, + { + name: "Wolfe (Fixed)", + path: "wolfe2.pbtxt", + }, + { + name: "AlexNet", + path: "alexnet.pbtxt", + }, + { + name: "TestError404", + path: "nofile" + } +]; + +Polymer({ + is: 'tf-graph-demo', + properties: { + hasStats: Boolean, + datasets: { + type: Object, + value: function() { + return this._getDatasets(); + } + }, + selectedDataset: { + type: Number, + value: 1 + }, + _progress: Object + }, + _getDatasets: function() { + return _.map(datasets, function(dataset) { + var result = { + name: dataset.name, + path: this.resolveUrl('tf_model_zoo/' + dataset.path) + }; + if (dataset.statsPath != null) { + result.statsPath = this.resolveUrl('tf_model_zoo/' + dataset.statsPath); + } + return result; + }, this); + } +}); +})(); +</script> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-controls.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-controls.html new file mode 100644 index 0000000000..4a6901e911 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-controls.html @@ -0,0 +1,487 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-menu/paper-menu.html"> +<link rel="import" href="../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html"> + +<dom-module id="tf-graph-controls"> +<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 class="end" offset="100%" + stop-color$="[[_currentGradientParams.endColor]]"/> + </linearGradient> + </defs> + <rect x="0" y="0" width="160" height="20" fill="url(#linearGradient)" + stroke="black" /> + </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> + <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"/> + </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" /> + </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"/> + </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"/> + </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"/> + </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"/> + </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"/> + </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"/> + </marker> + </defs> + <path marker-end="url(#arrowhead-legend)" stroke="#bbb" + d="M2 9 l 23 0" stroke-linecap="round" /> + </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" /> + </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" /> + </svg> + </td> + <td>Reference edge</td> + </tr> + </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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-icon.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-icon.html new file mode 100644 index 0000000000..fafaa3b954 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-icon.html @@ -0,0 +1,164 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<dom-module id="tf-graph-icon"> + <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" /> + </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" /> + </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" /> + </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" /> + </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" /> + </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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-minimap.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-minimap.html new file mode 100644 index 0000000000..2b6beeaded --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-minimap.html @@ -0,0 +1,69 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<script src="../tf-graph-common/lib/scene/minimap.js"></script> +<dom-module id="tf-graph-minimap"> +<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> +<!-- Additional canvas to use as buffer to avoid flickering between updates --> +<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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-params.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-params.html new file mode 100644 index 0000000000..576816ddd0 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-params.html @@ -0,0 +1,113 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<!-- +Module for adjusting render graph building parameter. +--> +<dom-module id="tf-graph-params"> +</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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-scene.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-scene.html new file mode 100644 index 0000000000..34c2d3dc3d --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-scene.html @@ -0,0 +1,475 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="tf-graph-style.html"> +<link rel="import" href="tf-graph-minimap.html"> +<script src="../tf-graph-common/lib/layout.js"></script> +<script src="../../bower_components/dagre/dist/dagre.core.js"></script> +<!-- + A module that takes graph-hierarchy as input and produce + a svg dom using dagre and d3. +--> +<dom-module id="tf-graph-scene"> +<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> + <!-- Arrow head for edge paths. --> + <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"/> + </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"/> + </marker> + <!-- Arrow head for annotation edge paths. --> + <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"/> + </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"/> + </marker> + <!-- Template for an Op node ellipse. --> + <ellipse id="op-node-stamp" + rx="7.5" ry="3" stroke="inherit" fill="inherit" /> + <!-- Template for an Op node annotation ellipse (smaller). --> + <ellipse id="op-node-annotation-stamp" + rx="5" ry="2" stroke="inherit" fill="inherit" /> + <!-- Vertically stacked series of Op nodes when unexpanded. --> + <g id="op-series-vertical-stamp"> + <use xlink:href="#op-node-stamp" x="8" y="9" /> + <use xlink:href="#op-node-stamp" x="8" y="6" /> + <use xlink:href="#op-node-stamp" x="8" y="3" /> + </g> + <!-- Horizontally stacked series of Op nodes when unexpanded. --> + <g id="op-series-horizontal-stamp"> + <use xlink:href="#op-node-stamp" x="16" y="4" /> + <use xlink:href="#op-node-stamp" x="12" y="4" /> + <use xlink:href="#op-node-stamp" x="8" y="4" /> + </g> + <!-- Horizontally stacked series of Op nodes for annotation. --> + <g id="op-series-annotation-stamp"> + <use xlink:href="#op-node-annotation-stamp" x="9" y="2" /> + <use xlink:href="#op-node-annotation-stamp" x="7" y="2" /> + <use xlink:href="#op-node-annotation-stamp" x="5" y="2" /> + </g> + <!-- + Where the linearGradient for each node is stored. Used when coloring + by proportions of devices. + --> + <g id="linearGradients"></g> + </defs> + <!-- Make a large rectangle that fills the svg space so that + zoom events get captured on safari --> + <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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html b/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html new file mode 100644 index 0000000000..3e6f7f2112 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph-style.html @@ -0,0 +1,339 @@ +<dom-module id="tf-graph-style"> +<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> diff --git a/tensorflow/tensorboard/components/tf-graph/tf-graph.html b/tensorflow/tensorboard/components/tf-graph/tf-graph.html new file mode 100644 index 0000000000..905d96e237 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph/tf-graph.html @@ -0,0 +1,221 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/iron-flex-layout/iron-flex-layout.html"> +<link rel="import" href="../../bower_components/iron-icons/iron-icons.html"> +<link rel="import" href="../../bower_components/paper-button/paper-button.html"> +<link rel="import" href="../../bower_components/paper-input/paper-input.html"> +<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html"> +<link rel="import" href="../tf-graph-common/tf-graph-common.html"> +<link rel="import" href="tf-graph-scene.html"> +<link rel="import" href="tf-graph-params.html"> +<dom-module id="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> |