diff options
Diffstat (limited to 'tensorflow/tensorboard/components')
85 files changed, 13195 insertions, 0 deletions
diff --git a/tensorflow/tensorboard/components/hydrogen-join/demo/index.html b/tensorflow/tensorboard/components/hydrogen-join/demo/index.html new file mode 100644 index 0000000000..238cff294d --- /dev/null +++ b/tensorflow/tensorboard/components/hydrogen-join/demo/index.html @@ -0,0 +1,118 @@ +<!doctype html> +<!-- +@license +Copyright (c) 2015 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +--> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> + <title>tf-graph Demo</title> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../hydrogen-join.html"> + <link rel="import" href="../../../bower_components/paper-button/paper-button.html"> + <link rel="import" href="../../../bower_components/paper-slider/paper-slider.html"> + </head> + + <style> + body { + font-family: "RobotoDraft","Roboto",sans-serif; + font-size: 14; + } + </style> + + <body unresolved> + <dom-module id="button-press-counter"> + <style> + paper-button { + background-color: #4DB6AC; + color: white; + } + </style> + <template> + <paper-button raised on-click="increment"><span>[[count]]<span></paper-button> + </template> + <script> + Polymer({ + is: "button-press-counter", + properties: {count: {notify: true, value: 0}}, + increment: function() {this.count++;} + }); + </script> + </dom-module> + + <dom-module id="args-demo"> + <template> + <h1>args-demo</h1> + <button-press-counter count="{{in1}}"></button-press-counter> + <button-press-counter count="{{in2}}"></button-press-counter> + <button-press-counter count="{{in3}}"></button-press-counter> + <hydrogen-join + in1="[[in1]]" + in2="[[in2]]" + in3="[[in3]]" + out="{{out}}" + id="argsjoin" + ></hydrogen-join> + <p>Output from the hydrogen-join: <span>[[out]]</span></p> + </template> + <script> + Polymer({ + is: "args-demo", + properties: { + in1: Number, + in2: Number, + in3: Number, + out: Array, + }, + }); + </script> + </dom-module> + + <dom-module id="repeat-demo"> + <style> + .button { + padding: 3px; + margin-bottom: 4px; + display: inline-block; + } + </style> + <template> + <h1>repeat-demo</h1> + <hydrogen-join id="repeatjoin" in-join-property="count" out="{{out}}"> + <template is="dom-repeat" items="[[counters]]"> + <button-press-counter class="button" count="[[item]]"></button-press-counter> + </template> + </hydrogen-join> + <br> + <p> Move this slider to add/remove buttons. It stays synced! What magic! </p> + <paper-slider min="0" max="20" value="{{nCounters}}"></paper-slider> + <p>Output from the hydrogen-join: <span>[[out]]</span></p> + </template> + <script> + Polymer({ + is: "repeat-demo", + properties: { + nCounters: {type: Number, value: 10}, + counters: {type: Array, computed: "_makeCounters(nCounters)"}, + }, + _makeCounters: function(nCounters) { + var c = []; + for (var i=0; i<nCounters; i++) { + c.push(i); + } + return c; + } + }); + </script> + </dom-module> + + <args-demo id="argsdemo"></args-demo> + <repeat-demo id="repeatdemo"></repeat-demo> + </body> +</html> diff --git a/tensorflow/tensorboard/components/hydrogen-join/hydrogen-join.html b/tensorflow/tensorboard/components/hydrogen-join/hydrogen-join.html new file mode 100644 index 0000000000..9f8dcfe2af --- /dev/null +++ b/tensorflow/tensorboard/components/hydrogen-join/hydrogen-join.html @@ -0,0 +1,118 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<!-- +A plumber component which joins a number of input bindings. It outputs a list +consisting of all of the inputs that are not null or undefined. + +It can take explicit arguments [in0...in98] or it can pull its arguments from +a dom-repeat template, provided that it is given the property name it is looking +for on the repeated children, and that property has {notify: true}. The repeat +binding involves some magic and may not be totally reliable; ping danmane@ if +something goes wrong. + +Example: + <hydrogen-join + in1="[[foo1]]" + in2="[[foo2]]" + in3="[[foo3]]" + out="{{foos}}" // equivalent to [foo1].push(foo2).push(foo3) + ></hydrogen-join> + + <hydrogen-join out="{{foo}}" in-join-property="out"> + <template is="dom-repeat" items="[[foos]]"> + <foo item="[[item]]"></foo> //foo has a property out: {type: Array, notify: true} + </template> + </hydrogen-join> + +Swapping the inJoinProperty is not currently supported, it will warn if you try. + +There's a possible bug in repeat mode if an element is removed from the dom-repeat, +but continues to exist somewhere else, and continues to fire property-changed events. +Then the hydrogen-join will inappropriately record its value, even though it is not +still connected in hydrogen-join's DOM. + +@demo +--> +<dom-module id="hydrogen-join"> + <script> + var declaration = { + is: 'hydrogen-join', + properties: { + out: {type: Array, readOnly: true, notify: true}, + _items: {type: Array, value: function() {return [];}}, + /* Property to pull from dom-repeated child nodes and then pull and join */ + inJoinProperty: {type: String, observer: "_modifyJoinProperty"}, + }, + listeners: { + "dom-change": "_syncListenersAndState", + }, + /* If we are in repeat-mode, ensure all event listeners are setup, and pull + * items out of whatever children currently exist + */ + _syncListenersAndState: function() { + if (this.inJoinProperty == null) { + // this codepath is for pulling properties out of children in a repeat + return; + } + function repeatUpdateFunction(i) { + return function(e) { + this._items[i] = e.detail.value; + // Debounce just in case something else thrashes + this.debounce("updateOut", this._updateOutput) + } + } + // create a new items array to replace the old one, otherwise old items + // might never get removed when their corresponding element leaves + this._items = []; + // Sadly, we need to bind an update function onto every child node, + // because the property-changed event does not bubble. + for (var i=0; i<this.childNodes.length; i++) { + var child = this.childNodes[i]; + if (child.properties != null && child.properties[this.inJoinProperty] != null) { + child.addEventListener(this.inJoinProperty + "-changed", repeatUpdateFunction(i).bind(this)); + this._items[i] = child[this.inJoinProperty]; + } + } + this._updateOutput(); + }, + _modifyJoinProperty: function(newJoinProperty, oldJoinProperty) { + if (oldJoinProperty != null) { + console.warning("Changing the join property may be unsafe. Have fun!"); + } + this._syncListenersAndState(); + }, + _updateOutput: function() { + var out = []; + for (var i=0; i<99; i++) { + if (this._items[i] != null) { + out.push(this._items[i]); + } + } + this._setOut(out); + } + }; + + /* Programatically add properties in0-in98, with associated observers */ + function argsUpdateFunction(i) { + return function(newval) { + this._items[i] = newval; + if (i === 98) { + console.warn("You're updating the last arg (98). Possibly some values are being lost"); + } + if (this.inJoinProperty != null) { + console.warn("It looks like you're providing a join property and also arguments. This is not supported.") + } + this.debounce("updateOut", this._updateOutput) + } + } + // I got 99 arguments and ain't off by 1! + for (var i = 0; i < 99; i++) { + var propName = "in" + i; + var updateName = "_update" + i; + var property = {type: Object, observer: updateName}; + declaration.properties[propName] = property; + declaration[updateName] = argsUpdateFunction(i); + } + Polymer(declaration); + </script> +</dom-module> diff --git a/tensorflow/tensorboard/components/hydrogen-set/demo/index.html b/tensorflow/tensorboard/components/hydrogen-set/demo/index.html new file mode 100644 index 0000000000..6a1735a699 --- /dev/null +++ b/tensorflow/tensorboard/components/hydrogen-set/demo/index.html @@ -0,0 +1,106 @@ +<!doctype html> +<!-- +@license +Copyright (c) 2016 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +--> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> + <title>hydrogen-set demo</title> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../hydrogen-set.html"> + <link rel="import" href="../../../bower_components/paper-checkbox/paper-checkbox.html"> + </head> + + <style> + body { + margin: 0; + font-family: "RobotoDraft","Roboto",sans-serif; + font-size: 14; + } + </style> + + <body unresolved> + <dom-module id="x-a"> + <template> + <template is="dom-repeat" items="[[items]]"> + <span>[[item]]</span> + <paper-checkbox + name="[[item]]" + on-iron-change="_onChange" + checked="[[_isSelected(item, selected.*)]]" + ></paper-checkbox> + </template> + </template> + </dom-module> + <script> + Polymer({ + is: 'x-a', + properties: { + selected: { + type: Array + }, + items: { + type: Array, + value: function() { + return ["a","b","c","d","e"]; + } + } + }, + _onChange: function(e) { + var name = e.srcElement.name; + if (name) { + if (e.srcElement.checked) { + this.fire('select', name); + } else { + this.fire('unselect', name); + } + } + }, + _isSelected: function(item, selected) { + return selected.base.indexOf(item) >= 0; + } + }); + </script> + <template is="dom-bind"> + <hydrogen-set + id="set" + event-add="{{add}}" + event-delete="{{del}}" + out-value="{{selected}}" + ></hydrogen-set> + <div> + Mutate the two sets below. + </div> + <br/> + <div>First Set</div> + <x-a + id="a" + on-select="add" + on-unselect="del" + selected="[[selected]]" + ></x-a> + <br/> + <br/> + <div>Second Set</div> + <x-a + id="b" + on-select="add" + on-unselect="del" + selected="[[selected]]" + ></x-a> + <br/> + <br/> + <div>List Selected:</div> + <template is="dom-repeat" items="[[selected]]"> + <div>[[item]]</div> + </template> + </template> + </body> +</html> diff --git a/tensorflow/tensorboard/components/hydrogen-set/hydrogen-set.html b/tensorflow/tensorboard/components/hydrogen-set/hydrogen-set.html new file mode 100644 index 0000000000..28fb8bd2a1 --- /dev/null +++ b/tensorflow/tensorboard/components/hydrogen-set/hydrogen-set.html @@ -0,0 +1,174 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<!-- +hydrogen-set is a plumber component that is driven by events +and produces data in the form of an upward-bindable set. +It provides event handler functions like event-add and +event-del, which may be attached elsewhere as event listeners. +The hydrogen-set then captures those events and adds or +removes their details to an internal set, which it +publishes as out-value. + +As an example, you may have a list of input widgets that +generate strings by firing an event. If you attach +the hydrogen-set `event-add` function as the handler +for the input widgets' input event, then the hydrogen-set +will collect and deduplicate all of the strings the +user generated. + +Thus, hydrogen-set is useful for capturing semantic +events from across an application, and organizing +the data generated into a single store. + +Example: + + <hydrogen-set + event-add="{{add}} + event-delete="{{del}} + out-value="{{selected}}" + ></hydrogen-set> + <element-one + selected="{{selected}}" + on-select="add" + on-deselect="del" + ></element-one> + <element-two + selected="{{selected}}" + on-select="add" + on-deselect="del" + ></element-two> + +@demo demo/index.html +--> +<script> +Polymer({ + is: 'hydrogen-set', + properties: { + /** + * A function to bind to event callback where + * the detail value is the item to add. + * + * @property eventAdd + * @type Function + */ + eventAdd: { + readOnly: true, + notify: true, + type: Function, + value: function() { + return function(e) { + this.add(e.detail); + }.bind(this); + } + }, + /** + * A function to bind to event callback where + * the detail value is the item to remove. + * + * @property eventDelete + * @type Function + */ + eventDelete: { + readOnly: true, + notify: true, + type: Function, + value: function() { + return function(e) { + this.delete(e.detail); + }.bind(this); + } + }, + /** + * A function to bind to event callback where + * the detail value is the list of items that should + * replace the current set. + * + * @property eventUpdate + * @type Function + */ + eventUpdate: { + readOnly: true, + notify: true, + type: Function, + value: function() { + return function(e) { + this.update(e.detail); + }.bind(this); + } + }, + /** + * A function to bind to event callback + * which when called reset the set to the + * empty set ([]). + * + * @property eventClear + * @type Function + */ + eventClear: { + readOnly: true, + notify: true, + type: Function, + value: function() { + return function(e) { + this.clear(e.detail); + }.bind(this); + } + }, + /** + * The read-only array representing the set of + * items in this set. + * + * @property outValue + * @type Array + * @default [] + */ + outValue: { + type: Array, + readOnly: true, + notify: true, + value: function() { + return []; + } + } + }, + /** + * Adds an item to the set. + */ + add: function(value) { + if (this.outValue.indexOf(value) >= 0) { return; } + this.push('outValue', value); + }, + /** + * Removes an item from the set. + */ + delete: function(value) { + var i = this.outValue.indexOf(value); + if (i < 0) { return; } + this.splice('outValue', i, 1); + }, + /** + * Sets the set to a specific array of items. + */ + update: function(value) { + if (value.constructor === Array) { + var uniq = {}; + var list = []; + for (var i=0, l = value.length; i < l; ++i) { + var item = value[i]; + if (uniq.hasOwnProperty(item)) { + continue; + } + list.push(item); + uniq[item] = true; + } + this._setOutValue(list) + } + }, + /** + * Resets the set to the empty set. + */ + clear: function() { + this.update([]); + } +}); +</script> diff --git a/tensorflow/tensorboard/components/imports/d3.html b/tensorflow/tensorboard/components/imports/d3.html new file mode 100644 index 0000000000..d63c480fd9 --- /dev/null +++ b/tensorflow/tensorboard/components/imports/d3.html @@ -0,0 +1 @@ +<script src="../../bower_components/d3/d3.min.js"></script> diff --git a/tensorflow/tensorboard/components/imports/lodash.html b/tensorflow/tensorboard/components/imports/lodash.html new file mode 100644 index 0000000000..1e94d2c1c4 --- /dev/null +++ b/tensorflow/tensorboard/components/imports/lodash.html @@ -0,0 +1 @@ +<script src="../../bower_components/lodash/lodash.min.js"></script> diff --git a/tensorflow/tensorboard/components/imports/plottable.html b/tensorflow/tensorboard/components/imports/plottable.html new file mode 100644 index 0000000000..08e636886a --- /dev/null +++ b/tensorflow/tensorboard/components/imports/plottable.html @@ -0,0 +1,3 @@ +<link rel="import" href="d3.html"> +<script src="../../bower_components/plottable/plottable.min.js"></script> +<link rel="stylesheet" type="text/css" href="../../bower_components/plottable/plottable.css"> diff --git a/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts b/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts new file mode 100644 index 0000000000..e05078279e --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/categorizer.ts @@ -0,0 +1,133 @@ +/// <reference path="../../typings/tsd.d.ts" /> + +module Categorizer { + /** + * This module contains methods that allow sorting tags into "categories". + * A category contains a name and a list of tags. + * The sorting strategy is defined by a "CustomCategorization", which contains + * "categoryDefinitions" which are regex rules used to construct a category. + * E.g. the regex rule "xent" will create a category called "xent" that + * contains values whose tags match the regex. + * + * After custom categories are evaluated, the tags are sorted by a hardcoded + * fallback categorizer, which may, for example, group tags into categories + * based on their top namespace. + */ + + export interface Category { + // Categories that data is sorted into + name: string; + tags: string[]; + } + + export interface CustomCategorization { + // Defines a categorization strategy + categoryDefinitions: string[]; + fallbackCategorizer: string; + /* {"TopLevelNamespaceCategorizer", + "LegacyUnderscoreCategorizer"} */ + } + + export interface Categorizer { + // Function that generates categories + (tags: string[]): Category[]; + } + + /* Canonical TensorFlow ops are namespaced using forward slashes. + * This fallback categorizer categorizes by the top-level namespace. + */ + export var topLevelNamespaceCategorizer: Categorizer = 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 + export var legacyUnderscoreCategorizer: Categorizer = splitCategorizer(/[\/_]/); + + export function fallbackCategorizer(s: string): Categorizer { + switch (s) { + case "TopLevelNamespaceCategorizer": + return topLevelNamespaceCategorizer; + case "LegacyUnderscoreCategorizer": + return legacyUnderscoreCategorizer; + default: + throw new Error("Unrecognized categorization strategy: " + s); + } + } + + /* 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: (s: string) => string): Categorizer { + return (tags: string[]): Category[] => { + if (tags.length === 0) { + return []; + } + var sortedTags = tags.slice().sort(); + var categories: Category[] = []; + var currentCategory = { + name: extractor(sortedTags[0]), + tags: [], + }; + sortedTags.forEach((t: string) => { + 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: RegExp): Categorizer { + var extractor = (t: string) => { + return t.split(r)[0]; + }; + return extractorToCategorizer(extractor); + } + + export interface CategoryDefinition { + name: string; + matches: (t: string) => boolean; + } + + export function defineCategory(ruledef: string): CategoryDefinition { + var r = new RegExp(ruledef); + var f = function(tag: string): boolean { + return r.test(tag); + }; + return { name: ruledef, matches: f }; + } + + export function _categorizer(rules: CategoryDefinition[], fallback: Categorizer) { + return function(tags: string[]): Category[] { + var remaining: d3.Set = d3.set(tags); + var userSpecified = rules.map((def: CategoryDefinition) => { + var tags: string[] = []; + remaining.forEach((t: string) => { + 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); + }; + } + + export function categorizer(s: CustomCategorization): Categorizer { + var rules = s.categoryDefinitions.map(defineCategory); + var fallback = fallbackCategorizer(s.fallbackCategorizer); + return _categorizer(rules, fallback); + }; +} diff --git a/tensorflow/tensorboard/components/tf-categorizer/demo/index.html b/tensorflow/tensorboard/components/tf-categorizer/demo/index.html new file mode 100644 index 0000000000..ea3f162aa5 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/demo/index.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <script src="../../../bower_components/d3/d3.js"></script> + <link rel="import" href="../tf-categorizer.html"> + <link rel="import" href="../../../bower_components/iron-flex-layout/classes/iron-flex-layout.html"> + + </head> + <body> + <style> + </style> + <dom-module id="x-demo"> + <style> + .container { + width: 255px; + padding: 10px; + border: 1px solid var(--paper-indigo-900); + border-radius: 5px; + position: fixed; + } + :host { + margin: 0px; + } + + .categories { + font-family: "RobotoDraft",Helvetica; + margin-left: 300px; + width: 500px; + border: 1px solid var(--paper-indigo-500); + border-radius: 5px; + } + + .category { + background-color: var(--paper-indigo-50); + margin: 20px; + padding: 20px; + border-radius: 5px; + } + + .cat-name { + font-size: 20px; + } + + .tag { + border-radius: 5px; + padding: 5px; + margin: 5px; + background-color: var(--paper-indigo-900); + color: white; + } + </style> + <template> + <div class="container"> + <tf-categorizer categories="{{categories}}" tags="[[tags]]" id="demo"></tf-categorizer> + </div> + <div class="categories"> + <template is="dom-repeat" items="[[categories]]"> + <div class="category"> + <p class="cat-name">Category: <span>[[item.name]]</span></p> + <div class="tags-container layout horizontal wrap"> + <template is="dom-repeat" items="[[item.tags]]"> + <span class="tag layout vertical center-center">[[item]]</span> + </template> + </div> + </template> + </div> + </template> + <script> + + function tagsGenerator() { + var tags = ["special1", "special2", "special3", "special4", "special5"]; + ["l1", "l2", "l3", "l4", "l5"].forEach(function(l) { + ["foo", "bar", "baz", "boink", "zod", "specialx"].forEach(function(x) { + tags.push(l + "/" + x); + }); + }); + return tags; + } + + Polymer({ + is: "x-demo", + properties: { + tags: { type: Array, value: tagsGenerator }, + }, + }); + </script> + </dom-module> + + <x-demo id="demo"></x-demo> + </body> + <script> + HTMLImports.whenReady(function() { + window.demo = document.getElementById("demo"); + }) + </script> +</html> diff --git a/tensorflow/tensorboard/components/tf-categorizer/index.html b/tensorflow/tensorboard/components/tf-categorizer/index.html new file mode 100644 index 0000000000..f08a125f7c --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + + <title>tf-categorizer</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../../bower_components/iron-component-page/iron-component-page.html"> + +</head> +<body> + + <iron-component-page></iron-component-page> + +</body> +</html> diff --git a/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts b/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts new file mode 100644 index 0000000000..be09c56c41 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/test/categorizerTest.ts @@ -0,0 +1,139 @@ +/// <reference path="../../../typings/tsd.d.ts" /> +/// <reference path="../categorizer.ts" /> +var assert = chai.assert; + +module Categorizer { + describe("categorizer", () => { + describe("topLevelNamespaceCategorizer", () => { + it("returns empty array on empty tags", () => { + assert.lengthOf(topLevelNamespaceCategorizer([]), 0); + }); + + it("handles a simple case", () => { + var simple = ["foo1/bar", "foo1/zod", "foo2/bar", "foo2/zod", + "gosh/lod/mar", "gosh/lod/ned"]; + var expected = [ + { name: "foo1", tags: ["foo1/bar", "foo1/zod"] }, + { name: "foo2", tags: ["foo2/bar", "foo2/zod"] }, + { name: "gosh", tags: ["gosh/lod/mar", "gosh/lod/ned"] }, + ]; + assert.deepEqual(topLevelNamespaceCategorizer(simple), expected); + }); + + it("orders the categories", () => { + var test = ["e", "f", "g", "a", "b", "c"]; + var expected = [ + { name: "a", tags: ["a"] }, + { name: "b", tags: ["b"] }, + { name: "c", tags: ["c"] }, + { name: "e", tags: ["e"] }, + { name: "f", tags: ["f"] }, + { name: "g", tags: ["g"] }, + ]; + assert.deepEqual(topLevelNamespaceCategorizer(test), expected); + }); + + it("handles cases where category names overlap node names", () => { + var test = ["a", "a/a", "a/b", "a/c", "b", "b/a"]; + var actual = topLevelNamespaceCategorizer(test); + var expected = [ + { name: "a", tags: ["a", "a/a", "a/b", "a/c"] }, + { name: "b", tags: ["b", "b/a"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("handles singleton case", () => { + assert.deepEqual(topLevelNamespaceCategorizer(["a"]), [{ name: "a", tags: ["a"] }]); + }); + }); + + describe("legacyUnderscoreCategorizer", () => { + it("splits by shorter of first _ or /", () => { + var tags = ["l0_bar/foo", "l0_bar/baz", "l0_foo/wob", "l1_zoink/bla", + "l1_wibble/woz", "l1/foo_woink", "l2/wozzle_wizzle"]; + var actual = legacyUnderscoreCategorizer(tags); + var expected = [ + { name: "l0", tags: ["l0_bar/baz", "l0_bar/foo", "l0_foo/wob"] }, + { name: "l1", tags: ["l1/foo_woink", "l1_wibble/woz", "l1_zoink/bla"] }, + { name: "l2", tags: ["l2/wozzle_wizzle"] }, + ]; + assert.deepEqual(actual, expected); + }); + }); + + describe("customCategorizer", () => { + function noFallbackCategorizer(tags: string[]): Category[] { + return []; + } + + function testCategorizer(defs: string[], + fallback: Categorizer, tags: string[]): Category[] { + var catDefs = defs.map(defineCategory); + return _categorizer(catDefs, fallback)(tags); + } + + it("categorizes by regular expression", () => { + var defs = ["foo..", "bar.."]; + var tags = ["fooab", "fooxa", "barts", "barms"]; + var actual = testCategorizer(defs, noFallbackCategorizer, tags); + var expected = [ + { name: "foo..", tags: ["fooab", "fooxa"] }, + { name: "bar..", tags: ["barms", "barts"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("matches non-exclusively", () => { + var tags = ["abc", "bar", "zod"]; + var actual = testCategorizer(["...", "bar"], noFallbackCategorizer, tags); + var expected = [ + { name: "...", tags: ["abc", "bar", "zod"] }, + { name: "bar", tags: ["bar"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("creates categories for unmatched rules", () => { + var actual = testCategorizer(["a", "b", "c"], noFallbackCategorizer, []); + var expected = [ + { name: "a", tags: [] }, + { name: "b", tags: [] }, + { name: "c", tags: [] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("category regexs work with special characters", () => { + var defs = ["^\\w+$", "^\\d+$", "^\\/..$"]; + var tags = ["foo", "3243", "/xa"]; + var actual = testCategorizer(defs, noFallbackCategorizer, tags); + var expected = [ + { name: "^\\w+$", tags: ["3243", "foo"] }, + { name: "^\\d+$", tags: ["3243"] }, + { name: "^\\/..$", tags: ["/xa"] }, + ]; + assert.deepEqual(actual, expected); + }); + + it("category tags are sorted", () => { + var tags = ["a", "z", "c", "d", "e", "x", "f", "y", "g"]; + var sorted = tags.slice().sort(); + var expected = [{ name: ".*", tags: sorted}]; + var actual = testCategorizer([".*"], noFallbackCategorizer, tags); + assert.deepEqual(actual, expected); + }); + + it("if nonexclusive: all tags passed to fallback", () => { + var passedToDefault = null; + function defaultCategorizer(tags: string[]): Category[] { + passedToDefault = tags; + return []; + } + var tags = ["foo", "bar", "foo123"]; + testCategorizer(["foo"], defaultCategorizer, tags); + assert.deepEqual(passedToDefault, tags); + }); + }); + }); +} diff --git a/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html b/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html new file mode 100644 index 0000000000..3672db38a2 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-categorizer/tf-categorizer.html @@ -0,0 +1,103 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html"> + +<link rel="import" href="../tf-regex-group/tf-regex-group.html"> +<link rel="import" href="../tf-dashboard-common/tensorboard-color.html"> + +<!-- +`tf-categorizer` turns an array of tags into an array of categories + +The transformation from tags to categories is controlled by the user, through +interacting with the categorizer widget. + +(See type signatures in categorizer.ts) + +Example: + <tf-categorizer tags="[[tags]]" categories="{{categories}}"></tf-categorizer> + +Public Properties: +`tags` - Array of strings that are the tags to categorize. Should be one-way bound downward. +`categories` - Array of Categorizer.Category objects that are generated by the Categorizer. + Are readOnly and notify: True. Expected to be one-way bound upward. + +The categorizer provides inputs for adding regular expression rules and toggling whether +categories are exclusive. +--> +<dom-module id="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 src="categorizer.js"></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> diff --git a/tensorflow/tensorboard/components/tf-collapsable-pane/demo/index.html b/tensorflow/tensorboard/components/tf-collapsable-pane/demo/index.html new file mode 100644 index 0000000000..8906b0f3da --- /dev/null +++ b/tensorflow/tensorboard/components/tf-collapsable-pane/demo/index.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../tf-collapsable-pane.html"> + </head> + <body> + <style> + </style> + <tf-collapsable-pane name="foo"> + <h1>This is content inside the pane.</h1> + </tf-collapsable-pane> + </body> + <script> + + </script> +</html> diff --git a/tensorflow/tensorboard/components/tf-collapsable-pane/index.html b/tensorflow/tensorboard/components/tf-collapsable-pane/index.html new file mode 100644 index 0000000000..032e5be8c8 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-collapsable-pane/index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + + <title>tf-collapsable-pane</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../../bower_components/iron-component-page/iron-component-page.html"> + +</head> +<body> + + <iron-component-page></iron-component-page> + +</body> +</html> diff --git a/tensorflow/tensorboard/components/tf-collapsable-pane/tf-collapsable-pane.html b/tensorflow/tensorboard/components/tf-collapsable-pane/tf-collapsable-pane.html new file mode 100644 index 0000000000..c06d40a2ec --- /dev/null +++ b/tensorflow/tensorboard/components/tf-collapsable-pane/tf-collapsable-pane.html @@ -0,0 +1,90 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/iron-collapse/iron-collapse.html"> + +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/dashboard-style.html b/tensorflow/tensorboard/components/tf-dashboard-common/dashboard-style.html new file mode 100644 index 0000000000..795cbbcac3 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/dashboard-style.html @@ -0,0 +1,97 @@ +<link rel="import" href="../../bower_components/paper-styles/paper-styles.html"> +<link rel="import" href="../tf-dashboard-common/tensorboard-color.html"> + +<dom-module id="dashboard-style"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/run-color-style.html b/tensorflow/tensorboard/components/tf-dashboard-common/run-color-style.html new file mode 100644 index 0000000000..d9b12f366a --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/run-color-style.html @@ -0,0 +1,62 @@ +<link rel="import" href="../../bower_components/paper-styles/paper-styles.html"> + +<dom-module id="run-color-style"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/scrollbar-style.html b/tensorflow/tensorboard/components/tf-dashboard-common/scrollbar-style.html new file mode 100644 index 0000000000..90fc184e8d --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/scrollbar-style.html @@ -0,0 +1,28 @@ +<link rel="import" href="../../bower_components/paper-styles/paper-styles.html"> + +<dom-module id="scrollbar-style"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/tensorboard-color.html b/tensorflow/tensorboard/components/tf-dashboard-common/tensorboard-color.html new file mode 100644 index 0000000000..c3a59b7a31 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/tensorboard-color.html @@ -0,0 +1,11 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<style is="custom-style"> + + :root { + --tb-orange-weak: #fcb938; + --tb-orange-strong: #f3913e; + --tb-grey-darker: #e2e2e2; + --tb-grey-lighter: #f3f3f3; + } + +</style> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/tf-dashboard-layout.html b/tensorflow/tensorboard/components/tf-dashboard-common/tf-dashboard-layout.html new file mode 100644 index 0000000000..89c51342fe --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/tf-dashboard-layout.html @@ -0,0 +1,50 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="scrollbar-style.html"> +<link rel="import" href="tensorboard-color.html"> +<!-- +Generic layout for a dashboard. +--> +<dom-module id="tf-dashboard-layout"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/tf-downloader.html b/tensorflow/tensorboard/components/tf-dashboard-common/tf-downloader.html new file mode 100644 index 0000000000..c7251ec578 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/tf-downloader.html @@ -0,0 +1,85 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-dropdown-menu/paper-dropdown-menu.html"> +<link rel="import" href="../../bower_components/paper-menu/paper-menu.html"> +<link rel="import" href="../../bower_components/paper-item/paper-item.html"> + +<dom-module id="tf-downloader"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/tf-run-generator.html b/tensorflow/tensorboard/components/tf-dashboard-common/tf-run-generator.html new file mode 100644 index 0000000000..4d72552049 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/tf-run-generator.html @@ -0,0 +1,97 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/iron-ajax/iron-ajax.html"> +<link rel="import" href="../imports/lodash.html"> + +<!-- +tf-run-generator is a plumbing component that takes in a url to load runs from, and + produces the following upward-bindable properties: + + outRunToScalars: Maps from run name (string) to an array of scalar tags (strings). + outRunToHistograms: Maps from run name (string) to an array of histogram tags (strings). + outRunToImages: Maps from run name (string) to an array of image tags (strings). +--> +<dom-module id="tf-run-generator"> + <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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/tf-url-generator.html b/tensorflow/tensorboard/components/tf-dashboard-common/tf-url-generator.html new file mode 100644 index 0000000000..803998daeb --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/tf-url-generator.html @@ -0,0 +1,50 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<!-- tf-url-generator is a plumbing component that provides two upward bindable properties: + outRunsUrl and outValuesUrlGenerator. These may be used by the rest of the application to communicate + with the backend, and by overriding the TF.Urls code that backs it, can be modified to load data from + a demo data source instead. + --> +<dom-module id="tf-url-generator"> + <script src="urlGenerator.js"></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> diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/urlGenerator.ts b/tensorflow/tensorboard/components/tf-dashboard-common/urlGenerator.ts new file mode 100644 index 0000000000..c7bbcbf434 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/urlGenerator.ts @@ -0,0 +1,33 @@ +/// <reference path="../../typings/tsd.d.ts" /> +/// <reference path="../../bower_components/plottable/plottable.d.ts" /> + +module TF { + export module Urls { + + export var routes = ["runs", "scalars", "histograms", + "compressedHistograms", "images", + "individualImage", "graph"]; + + function router(route: string): ((tag: string, run: string) => string) { + return function(tag: string, run: string): string { + return "/" + route + "?tag=" + encodeURIComponent(tag) + + "&run=" + encodeURIComponent(run); + }; + } + + export function runsUrl() { + return "/runs"; + } + export var scalarsUrl = router("scalars"); + export var histogramsUrl = router("histograms"); + export var compressedHistogramsUrl = router("compressedHistograms"); + export var imagesUrl = router("images"); + export function individualImageUrl(query: string) { + return "/individualImage?" + query; + } + export function graphUrl(run: string) { + return "/graph?run=" + encodeURIComponent(run); + } + + } +} diff --git a/tensorflow/tensorboard/components/tf-dashboard-common/warning-style.html b/tensorflow/tensorboard/components/tf-dashboard-common/warning-style.html new file mode 100644 index 0000000000..c4103a7248 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-dashboard-common/warning-style.html @@ -0,0 +1,10 @@ +<dom-module id="warning-style"> + <template> + <style> + .warning { + max-width: 540px; + margin: 80px auto 0 auto; + } + </style> + </template> +</dom-module> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/dataCoordinator.ts b/tensorflow/tensorboard/components/tf-event-dashboard/dataCoordinator.ts new file mode 100644 index 0000000000..c489eca17c --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/dataCoordinator.ts @@ -0,0 +1,57 @@ +/// <reference path="../../typings/tsd.d.ts" /> +/// <reference path="../../bower_components/plottable/plottable.d.ts" /> + +module 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. + */ + export class DataCoordinator { + private urlGenerator: (tag: string, run: string) => string; + private datasets: {[key: string]: TF.Dataset}; + private runToTag: {[run: string]: string[]}; + + constructor(urlGenerator: (tag: string, run: string) => string, + runToTag: {[run: string]: string[]}) { + 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. + */ + public getDatasets(tag: string, runs: string[]) { + var usableRuns = runs.filter((r) => { + var tags = this.runToTag[r]; + return tags.indexOf(tag) !== -1; + }); + return usableRuns.map((r) => this.getDataset(tag, r)); + } + + /* Create or return a Dataset for given tag and run. + * Calling this triggers a load on the dataset. + */ + public getDataset(tag: string, run: string): TF.Dataset { + var dataset = this._getDataset(tag, run); + dataset.load(); + return dataset; + } + + private _getDataset(tag: string, run: string): TF.Dataset { + var key = [tag, run].toString(); + var dataset: TF.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; + } + } +} diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/dataset.ts b/tensorflow/tensorboard/components/tf-event-dashboard/dataset.ts new file mode 100644 index 0000000000..de814583d3 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/dataset.ts @@ -0,0 +1,41 @@ +/// <reference path="../../typings/tsd.d.ts" /> +/// <reference path="../../bower_components/plottable/plottable.d.ts" /> + +module TF { + /* An extension of Plottable.Dataset that knows how to load data from a backend. + */ + export class Dataset extends Plottable.Dataset { + public tag: string; + public run: string; + private lastLoadTime: number; + private lastRequest; + private urlGenerator: Function; + + constructor(tag: string, run: string, urlGenerator: (run: string, tag: string) => string) { + super([], {tag: tag, run: run}); + this.tag = tag; + this.run = run; + this.urlGenerator = urlGenerator; + } + + public load = _.debounce(this._load, 10); + + private _load() { + var url = this.urlGenerator(this.tag, this.run); + if (this.lastRequest != null) { + this.lastRequest.abort(); + } + this.lastRequest = d3.json(url, (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); + } + }); + } + } +} diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d1.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d1.json new file mode 100644 index 0000000000..af17f5c328 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d1.json @@ -0,0 +1 @@ +[[1436926051.074826, 84, 0.6990088224411011], [1436926530.99861, 2289, 6.9384379386901855], [1436927011.134076, 7611, 13.698328971862793], [1436927490.984256, 16147, 20.168190002441406], [1436927970.957234, 26087, 20.877344131469727], [1436928450.977514, 36241, 21.269058227539062], [1436928930.989548, 46432, 21.329505920410156], [1436929410.976308, 56629, 21.220420837402344], [1436929890.966395, 66791, 21.190065383911133], [1436930370.958199, 76936, 21.108604431152344], [1436930850.985301, 87083, 21.157001495361328], [1436931331.009261, 97161, 21.02127456665039], [1436931810.966042, 107210, 20.891658782958984], [1436932290.955417, 117262, 20.930112838745117], [1436932770.964496, 127333, 20.986324310302734], [1436933250.962592, 137430, 20.981359481811523], [1436933730.992022, 147528, 21.083036422729492], [1436934210.959831, 157635, 21.092649459838867], [1436934690.97072, 167749, 21.11568832397461], [1436935170.957944, 177869, 21.145965576171875], [1436935650.959987, 188025, 21.215585708618164], [1436936130.997541, 198206, 21.227184295654297], [1436936610.965526, 208395, 21.226459503173828], [1436937090.965581, 218592, 21.264968872070312], [1436937570.964874, 228818, 21.335866928100586], [1436938050.965706, 239021, 21.286521911621094], [1436938531.013159, 249210, 21.20963478088379], [1436939010.957926, 259415, 21.28431510925293], [1436939490.96341, 269637, 21.326831817626953], [1436939970.959372, 279876, 21.38308334350586], [1436940450.963802, 290127, 21.355499267578125], [1436940931.004537, 300349, 21.31337547302246], [1436941410.979614, 310601, 21.405778884887695], [1436941890.979674, 320872, 21.368688583374023], [1436942370.975153, 331131, 21.39077377319336], [1436942850.980459, 341399, 21.41745948791504], [1436943331.000808, 351651, 21.384023666381836], [1436943810.968736, 361904, 21.326438903808594], [1436944290.95947, 372158, 21.367351531982422], [1436944770.955783, 382430, 21.476247787475586], [1436945250.966321, 392684, 21.36678695678711], [1436945731.008667, 402950, 21.349145889282227], [1436946210.977922, 413210, 21.373897552490234], [1436946690.975303, 423463, 21.322399139404297], [1436947170.964596, 433723, 21.341150283813477], [1436947650.955017, 443991, 21.366348266601562], [1436948130.992501, 454271, 21.43844223022461], [1436948610.960555, 464519, 21.36829948425293], [1436949090.961079, 474758, 21.266357421875], [1436949570.971528, 484987, 21.316511154174805], [1436950050.977787, 495228, 21.356050491333008], [1436950531.020035, 505458, 21.31462860107422], [1436951010.959775, 515682, 21.277490615844727], [1436951490.967418, 525910, 21.289737701416016], [1436951970.969778, 536112, 21.2515811920166], [1436952450.956291, 546320, 21.254491806030273], [1436952931.005547, 556541, 21.297870635986328], [1436953410.955758, 566755, 21.320045471191406], [1436953890.959151, 576957, 21.23529624938965], [1436954370.959553, 587165, 21.25132179260254], [1436954850.960546, 597371, 21.23470115661621], [1436955330.989932, 607582, 21.19434356689453], [1436955810.957128, 617790, 21.258535385131836], [1436956290.9763, 627991, 21.221921920776367], [1436956770.957785, 638208, 21.309843063354492], [1436957250.974143, 648404, 21.252185821533203], [1436957731.012441, 658613, 21.265626907348633], [1436958210.980787, 668824, 21.239660263061523], [1436958690.973474, 679034, 21.2642765045166], [1436959170.95825, 689249, 21.303138732910156], [1436959650.959345, 699454, 21.24073600769043], [1436960131.008682, 709664, 21.217615127563477], [1436960610.958074, 719876, 21.251184463500977], [1436961090.963638, 730100, 21.290971755981445], [1436961570.979029, 740316, 21.305265426635742], [1436962050.974645, 750534, 21.27857208251953], [1436962531.055479, 760757, 21.329837799072266], [1436963010.975299, 770964, 21.248849868774414], [1436963490.963107, 781164, 21.19978904724121], [1436963970.965936, 791382, 21.30535888671875], [1436964450.959947, 801590, 21.226255416870117], [1436964931.00587, 811785, 21.242237091064453], [1436965410.977997, 821977, 21.226497650146484], [1436965890.988465, 832189, 21.31219482421875], [1436966370.965612, 842399, 21.283390045166016], [1436966850.965794, 852612, 21.273908615112305], [1436967331.009476, 862825, 21.260452270507812], [1436967810.96767, 873037, 21.315444946289062], [1436968290.959107, 883248, 21.28677749633789], [1436968770.9681, 893452, 21.265335083007812], [1436969250.959332, 903655, 21.252891540527344], [1436969731.055609, 913856, 21.233684539794922], [1436970210.961426, 924047, 21.191429138183594], [1436970690.962999, 934250, 21.23288345336914], [1436971170.989107, 944430, 21.17190170288086], [1436971650.956015, 954634, 21.275972366333008], [1436972131.006841, 964844, 21.278474807739258], [1436972610.981754, 975045, 21.25553321838379], [1436973090.961548, 985239, 21.21686553955078], [1436973570.960013, 995439, 21.26004981994629], [1436974050.975653, 1005642, 21.25356101989746], [1436974530.988571, 1015842, 21.23944664001465], [1436975010.95851, 1026048, 21.293363571166992], [1436975490.97355, 1036253, 21.277101516723633], [1436975970.960916, 1046451, 21.242155075073242], [1436976450.990263, 1056636, 21.182037353515625], [1436976930.999578, 1066834, 21.21113395690918], [1436977410.962637, 1077031, 21.230762481689453], [1436977890.970389, 1087222, 21.232444763183594], [1436978370.959059, 1097405, 21.202342987060547], [1436978850.956562, 1107601, 21.23992156982422], [1436979331.021134, 1117786, 21.197628021240234], [1436979810.958593, 1127973, 21.2270565032959], [1436980290.958763, 1138163, 21.250303268432617], [1436980770.967171, 1148348, 21.215538024902344], [1436981250.960473, 1158540, 21.277185440063477], [1436981731.009465, 1168733, 21.268449783325195], [1436982210.960797, 1178930, 21.268077850341797], [1436982690.959709, 1189129, 21.243141174316406], [1436983170.961963, 1199327, 21.21793556213379], [1436983650.958504, 1209524, 21.2817440032959], [1436984130.998057, 1219726, 21.261478424072266], [1436984610.958945, 1229936, 21.300107955932617], [1436985090.978825, 1240145, 21.326183319091797], [1436985570.993741, 1250311, 21.115875244140625], [1436986050.965608, 1260436, 21.19010353088379], [1436986531.026713, 1270611, 21.183719635009766], [1436987010.969056, 1280784, 21.273176193237305], [1436987490.975071, 1290959, 21.182931900024414], [1436987970.96007, 1301147, 21.260244369506836], [1436988450.966092, 1311328, 21.225025177001953], [1436988931.004917, 1321514, 21.242164611816406], [1436989410.980351, 1331709, 21.19801139831543], [1436989890.975192, 1341910, 21.273555755615234], [1436990370.964941, 1352090, 21.175983428955078], [1436990850.973647, 1362240, 21.13412094116211], [1436991330.999346, 1372396, 21.153064727783203], [1436991811.003573, 1382550, 21.155475616455078], [1436992290.962706, 1392710, 21.17011833190918], [1436992770.999149, 1402862, 21.128713607788086], [1436993250.965124, 1413020, 21.1361026763916], [1436993731.020464, 1423164, 21.157777786254883], [1436994210.966935, 1433312, 21.119478225708008], [1436994690.962803, 1443468, 21.161104202270508], [1436995170.972952, 1453657, 21.11492919921875], [1436995650.976233, 1463820, 21.194231033325195], [1436996130.990524, 1473980, 21.169816970825195], [1436996610.97302, 1484152, 21.18223762512207], [1436997090.958457, 1494308, 21.1954402923584], [1436997570.980333, 1504463, 21.140769958496094], [1436998050.969869, 1514618, 21.162744522094727], [1436998530.99688, 1524770, 21.139591217041016], [1436999010.970375, 1534905, 21.107114791870117], [1436999490.960775, 1545070, 21.233396530151367], [1436999970.965087, 1555223, 21.201074600219727], [1437000450.969008, 1565370, 21.147083282470703], [1437000931.007425, 1575517, 21.108510971069336], [1437001410.962798, 1585666, 21.11674690246582], [1437001890.966192, 1595826, 21.17819595336914], [1437002370.961814, 1605980, 21.157669067382812], [1437002850.962206, 1616145, 21.212690353393555], [1437003330.994816, 1626291, 21.177446365356445], [1437003810.966017, 1636448, 21.17884063720703], [1437004290.959479, 1646599, 21.150310516357422], [1437004770.965083, 1656754, 21.21011734008789], [1437005250.958234, 1666902, 21.14912986755371], [1437005731.003528, 1677043, 21.125459671020508], [1437006210.961371, 1687192, 21.124374389648438], [1437006690.962663, 1697338, 21.150362014770508], [1437007170.961639, 1707484, 21.16637420654297], [1437007650.972242, 1717625, 21.163259506225586], [1437008131.003191, 1727767, 21.167280197143555], [1437008610.962644, 1737913, 21.174945831298828], [1437009090.964129, 1748068, 21.17894172668457], [1437009570.962582, 1758219, 21.116622924804688], [1437010050.984863, 1768384, 21.23469352722168], [1437010531.002295, 1778534, 21.143510818481445], [1437011010.961803, 1788677, 21.159791946411133], [1437011490.974074, 1798822, 21.119792938232422], [1437011970.959982, 1808958, 21.10943603515625], [1437012450.95932, 1819091, 21.123899459838867], [1437012931.004909, 1829227, 21.094532012939453], [1437013410.957751, 1839374, 21.200057983398438], [1437013890.960506, 1849509, 21.10895538330078], [1437014370.96113, 1859653, 21.108680725097656], [1437014850.962876, 1869791, 21.141136169433594], [1437015331.009875, 1879944, 21.160165786743164], [1437015810.960671, 1890090, 21.158742904663086], [1437016290.970743, 1900242, 21.16562271118164], [1437016770.961673, 1910391, 21.141860961914062], [1437017250.96735, 1920551, 21.19420051574707], [1437017731.000324, 1930702, 21.16814422607422], [1437018210.967878, 1940856, 21.125978469848633], [1437018690.962742, 1951005, 21.15043067932129], [1437019170.975774, 1961158, 21.157419204711914], [1437019650.964573, 1971309, 21.150177001953125], [1437020130.999343, 1981461, 21.124492645263672], [1437020610.960696, 1991611, 21.109933853149414], [1437021090.958597, 2001766, 21.169754028320312], [1437021570.964477, 2011919, 21.13479995727539], [1437022050.966522, 2022063, 21.131561279296875], [1437022531.005607, 2032219, 21.135629653930664], [1437023010.970667, 2042380, 21.207313537597656], [1437023490.964885, 2052534, 21.108623504638672], [1437023970.965596, 2062691, 21.14097023010254], [1437024450.962296, 2072837, 21.129037857055664], [1437024931.00395, 2082982, 21.077030181884766], [1437025410.96602, 2093128, 21.13152503967285], [1437025890.961753, 2103274, 21.117740631103516], [1437026370.962022, 2113424, 21.141584396362305], [1437026850.975475, 2123570, 21.143577575683594], [1437027331.009277, 2133721, 21.175586700439453], [1437027810.97206, 2143857, 21.099014282226562], [1437028290.961523, 2154015, 21.141523361206055], [1437028770.964366, 2164168, 21.141345977783203], [1437029250.962109, 2174320, 21.14827537536621], [1437029731.003068, 2184453, 21.086946487426758], [1437030210.960946, 2194602, 21.1590576171875], [1437030690.966681, 2204754, 21.17353057861328], [1437031170.961207, 2214899, 21.133989334106445], [1437031650.962809, 2225062, 21.14800453186035], [1437032130.997644, 2235215, 21.15397834777832], [1437032610.962999, 2245366, 21.15763282775879], [1437033090.962192, 2255521, 21.133577346801758], [1437033570.963341, 2265657, 21.058490753173828], [1437034050.979501, 2275787, 21.079614639282227], [1437034531.003514, 2285923, 21.12677574157715], [1437035010.960984, 2296058, 21.100793838500977], [1437035490.97325, 2306176, 21.10753059387207], [1437035970.969759, 2316297, 21.100393295288086], [1437036450.962305, 2326428, 21.041208267211914], [1437036931.001785, 2336571, 21.15167999267578], [1437037410.967681, 2346709, 21.09291648864746], [1437037890.963194, 2356854, 21.18524932861328], [1437038370.96445, 2366985, 21.116247177124023], [1437038850.960718, 2377124, 21.125469207763672], [1437039331.003148, 2387259, 21.132274627685547], [1437039810.974007, 2397400, 21.119945526123047], [1437040290.983415, 2407539, 21.154672622680664], [1437040770.961836, 2417667, 21.066741943359375], [1437041250.964281, 2427791, 21.126564025878906], [1437041731.0196, 2437923, 21.1062068939209], [1437042210.962927, 2448056, 21.124549865722656], [1437042690.964392, 2458193, 21.13232421875], [1437043170.972024, 2468318, 21.066423416137695], [1437043650.966111, 2478449, 21.123788833618164], [1437044131.030028, 2488576, 21.138349533081055], [1437044610.962532, 2498717, 21.11895179748535], [1437045090.965094, 2508839, 21.019609451293945], [1437045570.963352, 2518972, 21.079254150390625], [1437046050.96194, 2529106, 21.15033531188965], [1437046530.995016, 2539243, 21.11912727355957], [1437047010.963313, 2549369, 21.08464813232422], [1437047490.963943, 2559509, 21.133895874023438], [1437047970.958612, 2569646, 21.108659744262695], [1437048450.962392, 2579776, 21.084848403930664], [1437048931.005408, 2589906, 21.092708587646484], [1437049410.984115, 2600033, 21.130634307861328], [1437049890.964103, 2610162, 21.074010848999023], [1437050370.960886, 2620282, 21.086149215698242], [1437050850.959795, 2630402, 21.08969497680664], [1437051331.008292, 2640533, 21.134498596191406], [1437051810.96622, 2650643, 21.065444946289062], [1437052290.98584, 2660774, 21.120830535888672], [1437052770.967707, 2670900, 21.085134506225586], [1437053250.978851, 2681021, 21.037155151367188], [1437053731.021686, 2691151, 21.09203338623047], [1437054210.971744, 2701273, 21.048450469970703], [1437054690.966686, 2711425, 21.048809051513672], [1437055170.964463, 2721564, 21.13330078125], [1437055650.97301, 2731694, 21.097095489501953], [1437056130.997053, 2741810, 21.031536102294922], [1437056610.968681, 2751927, 21.04400634765625], [1437057090.976676, 2762049, 21.114444732666016], [1437057570.962334, 2772169, 21.06243896484375], [1437058050.969524, 2782292, 21.12563133239746], [1437058531.012918, 2792420, 21.12433433532715], [1437059010.972868, 2802545, 21.067407608032227], [1437059490.96188, 2812684, 21.099285125732422], [1437059970.965083, 2822806, 21.08357810974121], [1437060450.964845, 2832940, 21.142192840576172], [1437060931.011947, 2843080, 21.109895706176758], [1437061410.963414, 2853223, 21.13360023498535], [1437061890.969303, 2863361, 21.152849197387695], [1437062370.963703, 2873490, 21.08356285095215], [1437062850.964392, 2883627, 21.115087509155273], [1437063331.025516, 2893758, 21.13198471069336], [1437063810.962087, 2903877, 21.084623336791992], [1437064290.973818, 2914013, 21.14010238647461], [1437064770.967792, 2924145, 21.108346939086914], [1437065250.95886, 2934291, 21.1142635345459], [1437065731.01002, 2944434, 21.17418670654297], [1437066210.959306, 2954576, 21.084075927734375], [1437066690.960644, 2964724, 21.125164031982422], [1437067170.969539, 2974890, 21.200775146484375], [1437067650.960018, 2985036, 21.14740562438965], [1437068130.990731, 2995179, 21.11964225769043], [1437068610.960429, 3005322, 21.141313552856445], [1437069090.95752, 3015461, 21.082963943481445], [1437069570.974879, 3025595, 21.12288475036621], [1437070050.95761, 3035734, 21.107513427734375], [1437070531.0013, 3045868, 21.171630859375], [1437071010.961705, 3056004, 21.066505432128906], [1437071490.961495, 3066137, 21.10834312438965], [1437071970.978122, 3076267, 21.08027458190918], [1437072450.963299, 3086399, 21.089733123779297], [1437072931.018382, 3096524, 21.133176803588867], [1437073050.962102, 3099048, 21.041847229003906], [1437073170.96983, 3101584, 21.131967544555664], [1437073290.957895, 3104118, 21.118793487548828]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d2.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d2.json new file mode 100644 index 0000000000..92bb414348 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d2.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 0.04500000178813934], [1436926413.945391, 1476, 0.04500000178813934], [1436926893.945037, 6006, 0.04500000178813934], [1436927373.995472, 13786, 0.04500000178813934], [1436927853.989794, 23650, 0.04500000178813934], [1436928334.132361, 33755, 0.04500000178813934], [1436928813.973288, 43941, 0.04500000178813934], [1436929293.975949, 54146, 0.04500000178813934], [1436929773.992781, 64316, 0.04500000178813934], [1436930253.997415, 74465, 0.04500000178813934], [1436930734.203004, 84611, 0.04230000078678131], [1436931214.03644, 94700, 0.04230000078678131], [1436931694.094564, 104766, 0.04230000078678131], [1436932174.114955, 114817, 0.04230000078678131], [1436932654.161382, 124880, 0.04230000078678131], [1436933133.960214, 134977, 0.04230000078678131], [1436933614.044337, 145062, 0.04230000078678131], [1436934094.166206, 155169, 0.04230000078678131], [1436934574.106036, 165284, 0.03976200148463249], [1436935054.150647, 175402, 0.03976200148463249], [1436935533.819562, 185538, 0.03976200148463249], [1436936013.710422, 195712, 0.03976200148463249], [1436936493.609025, 205906, 0.03976200148463249], [1436936973.683892, 216099, 0.03976200148463249], [1436937454.138383, 226331, 0.03976200148463249], [1436937933.838475, 236532, 0.03976200148463249], [1436938413.89688, 246724, 0.0373762808740139], [1436938894.018652, 256925, 0.0373762808740139], [1436939373.69067, 267137, 0.0373762808740139], [1436939853.673692, 277369, 0.0373762808740139], [1436940333.651346, 287620, 0.0373762808740139], [1436940813.599579, 297848, 0.0373762808740139], [1436941293.596313, 308088, 0.0373762808740139], [1436941773.659172, 318362, 0.0373762808740139], [1436942253.648479, 328621, 0.03513370454311371], [1436942733.752284, 338892, 0.03513370454311371], [1436943213.621881, 349144, 0.03513370454311371], [1436943693.698743, 359399, 0.03513370454311371], [1436944173.578463, 369649, 0.03513370454311371], [1436944653.692217, 379912, 0.03513370454311371], [1436945133.677298, 390180, 0.03513370454311371], [1436945613.572411, 400445, 0.03302568197250366], [1436946093.56123, 410703, 0.03302568197250366], [1436946573.542364, 420958, 0.03302568197250366], [1436947053.616578, 431216, 0.03302568197250366], [1436947533.636973, 441483, 0.03302568197250366], [1436948013.541574, 451751, 0.03302568197250366], [1436948493.560223, 462015, 0.03302568197250366], [1436948973.512541, 472260, 0.03302568197250366], [1436949453.550055, 482483, 0.031044140458106995], [1436949933.828011, 492731, 0.031044140458106995], [1436950413.603177, 502957, 0.031044140458106995], [1436950893.563009, 513185, 0.031044140458106995], [1436951373.620887, 523410, 0.031044140458106995], [1436951853.61941, 533618, 0.031044140458106995], [1436952333.694447, 543828, 0.031044140458106995], [1436952813.621004, 554042, 0.031044140458106995], [1436953293.588156, 564251, 0.02918149158358574], [1436953773.599734, 574464, 0.02918149158358574], [1436954253.621309, 584672, 0.02918149158358574], [1436954733.738119, 594882, 0.02918149158358574], [1436955213.56617, 605091, 0.02918149158358574], [1436955693.585366, 615296, 0.02918149158358574], [1436956173.626395, 625501, 0.02918149158358574], [1436956653.601937, 635705, 0.02918149158358574], [1436957133.665878, 645915, 0.02743060328066349], [1436957613.584762, 656116, 0.02743060328066349], [1436958093.549783, 666331, 0.02743060328066349], [1436958573.646778, 676543, 0.02743060328066349], [1436959053.585655, 686750, 0.02743060328066349], [1436959533.679696, 696961, 0.02743060328066349], [1436960013.633292, 707173, 0.02743060328066349], [1436960493.578778, 717383, 0.02743060328066349], [1436960973.596715, 727598, 0.025784766301512718], [1436961453.625644, 737818, 0.025784766301512718], [1436961933.740339, 748040, 0.025784766301512718], [1436962413.573845, 758252, 0.025784766301512718], [1436962893.610678, 768470, 0.025784766301512718], [1436963373.642878, 778674, 0.025784766301512718], [1436963853.558388, 788877, 0.025784766301512718], [1436964333.658419, 799099, 0.025784766301512718], [1436964813.573319, 809289, 0.024237681180238724], [1436965293.542098, 819484, 0.024237681180238724], [1436965773.545453, 829687, 0.024237681180238724], [1436966253.586517, 839901, 0.024237681180238724], [1436966733.639348, 850120, 0.024237681180238724], [1436967213.697288, 860330, 0.024237681180238724], [1436967693.617172, 870539, 0.024237681180238724], [1436968173.593885, 880748, 0.024237681180238724], [1436968653.560836, 890955, 0.022783419117331505], [1436969133.676337, 901164, 0.022783419117331505], [1436969613.506638, 911358, 0.022783419117331505], [1436970093.595964, 921560, 0.022783419117331505], [1436970573.541227, 931756, 0.022783419117331505], [1436971053.624316, 941945, 0.022783419117331505], [1436971533.655543, 952138, 0.022783419117331505], [1436972013.604738, 962349, 0.02141641452908516], [1436972493.613199, 972551, 0.02141641452908516], [1436972973.501155, 982746, 0.02141641452908516], [1436973453.64842, 992945, 0.02141641452908516], [1436973933.689516, 1003147, 0.02141641452908516], [1436974413.577769, 1013350, 0.02141641452908516], [1436974893.542281, 1023545, 0.02141641452908516], [1436975373.638453, 1033759, 0.02141641452908516], [1436975853.524388, 1043955, 0.02013142965734005], [1436976333.625792, 1054148, 0.02013142965734005], [1436976813.610661, 1064342, 0.02013142965734005], [1436977293.601581, 1074539, 0.02013142965734005], [1436977773.575627, 1084733, 0.02013142965734005], [1436978253.564972, 1094914, 0.02013142965734005], [1436978733.673144, 1105109, 0.02013142965734005], [1436979213.540585, 1115293, 0.02013142965734005], [1436979693.699591, 1125483, 0.018923543393611908], [1436980173.613012, 1135670, 0.018923543393611908], [1436980653.575769, 1145862, 0.018923543393611908], [1436981133.719264, 1156045, 0.018923543393611908], [1436981613.563551, 1166236, 0.018923543393611908], [1436982093.553233, 1176436, 0.018923543393611908], [1436982573.577846, 1186636, 0.018923543393611908], [1436983053.605749, 1196837, 0.018923543393611908], [1436983533.684994, 1207025, 0.017788130789995193], [1436984013.561492, 1217233, 0.017788130789995193], [1436984493.629873, 1227437, 0.017788130789995193], [1436984973.606714, 1237643, 0.017788130789995193], [1436985453.690084, 1247835, 0.017788130789995193], [1436985933.711388, 1257951, 0.017788130789995193], [1436986413.598807, 1268125, 0.017788130789995193], [1436986893.631797, 1278290, 0.017788130789995193], [1436987373.596962, 1288473, 0.016720842570066452], [1436987853.555549, 1298650, 0.016720842570066452], [1436988333.722032, 1308841, 0.016720842570066452], [1436988813.55697, 1319018, 0.016720842570066452], [1436989293.756905, 1329221, 0.016720842570066452], [1436989773.665141, 1339417, 0.016720842570066452], [1436990253.768302, 1349610, 0.016720842570066452], [1436990733.708919, 1359759, 0.016720842570066452], [1436991213.663033, 1369914, 0.01571759209036827], [1436991693.730925, 1380074, 0.01571759209036827], [1436992173.751791, 1390224, 0.01571759209036827], [1436992653.758682, 1400383, 0.01571759209036827], [1436993133.835604, 1410542, 0.01571759209036827], [1436993613.674655, 1420684, 0.01571759209036827], [1436994093.747454, 1430832, 0.01571759209036827], [1436994573.768973, 1440986, 0.01571759209036827], [1436995053.666661, 1451174, 0.014774537645280361], [1436995533.83439, 1461345, 0.014774537645280361], [1436996013.556996, 1471495, 0.014774537645280361], [1436996493.635477, 1481663, 0.014774537645280361], [1436996973.668684, 1491822, 0.014774537645280361], [1436997453.59326, 1501979, 0.014774537645280361], [1436997933.774019, 1512139, 0.014774537645280361], [1436998413.575162, 1522290, 0.01388806477189064], [1436998893.640468, 1532431, 0.01388806477189064], [1436999373.551661, 1542579, 0.01388806477189064], [1436999853.57906, 1552734, 0.01388806477189064], [1437000333.680409, 1562888, 0.01388806477189064], [1437000813.602383, 1573037, 0.01388806477189064], [1437001293.610337, 1583190, 0.01388806477189064], [1437001773.618199, 1593341, 0.01388806477189064], [1437002253.572966, 1603497, 0.013054781593382359], [1437002733.67994, 1613657, 0.013054781593382359], [1437003213.583266, 1623809, 0.013054781593382359], [1437003693.639943, 1633966, 0.013054781593382359], [1437004173.568287, 1644113, 0.013054781593382359], [1437004653.610772, 1654268, 0.013054781593382359], [1437005133.663045, 1664424, 0.013054781593382359], [1437005613.580984, 1674567, 0.013054781593382359], [1437006093.601019, 1684715, 0.01227149460464716], [1437006573.625314, 1694857, 0.01227149460464716], [1437007053.584514, 1704999, 0.01227149460464716], [1437007533.719303, 1715150, 0.01227149460464716], [1437008013.604962, 1725282, 0.01227149460464716], [1437008493.655091, 1735432, 0.01227149460464716], [1437008973.640165, 1745584, 0.01227149460464716], [1437009453.715067, 1755742, 0.01227149460464716], [1437009933.765712, 1765896, 0.011535204015672207], [1437010413.632128, 1776052, 0.011535204015672207], [1437010893.66766, 1786195, 0.011535204015672207], [1437011373.636164, 1796346, 0.011535204015672207], [1437011853.631224, 1806481, 0.011535204015672207], [1437012333.706205, 1816617, 0.011535204015672207], [1437012813.61987, 1826754, 0.011535204015672207], [1437013293.479904, 1836883, 0.011535204015672207], [1437013773.604574, 1847029, 0.010843091644346714], [1437014253.618884, 1857175, 0.010843091644346714], [1437014733.756419, 1867312, 0.010843091644346714], [1437015213.638607, 1877459, 0.010843091644346714], [1437015693.625763, 1887608, 0.010843091644346714], [1437016173.63194, 1897759, 0.010843091644346714], [1437016653.609074, 1907909, 0.010843091644346714], [1437017133.717601, 1918074, 0.010843091644346714], [1437017613.716011, 1928220, 0.010192506946623325], [1437018093.626005, 1938377, 0.010192506946623325], [1437018573.626522, 1948523, 0.010192506946623325], [1437019053.648174, 1958678, 0.010192506946623325], [1437019533.803011, 1968831, 0.010192506946623325], [1437020013.667751, 1978978, 0.010192506946623325], [1437020493.659028, 1989133, 0.010192506946623325], [1437020973.657346, 1999287, 0.010192506946623325], [1437021453.650634, 2009437, 0.00958095584064722], [1437021933.848661, 2019588, 0.00958095584064722], [1437022413.674963, 2029736, 0.00958095584064722], [1437022893.69086, 2039894, 0.00958095584064722], [1437023373.68883, 2050054, 0.00958095584064722], [1437023853.686116, 2060205, 0.00958095584064722], [1437024333.763876, 2070362, 0.00958095584064722], [1437024813.707845, 2080507, 0.00958095584064722], [1437025293.483294, 2090645, 0.009006098844110966], [1437025773.695712, 2100793, 0.009006098844110966], [1437026253.672994, 2110943, 0.009006098844110966], [1437026733.780775, 2121094, 0.009006098844110966], [1437027213.617849, 2131235, 0.009006098844110966], [1437027693.694451, 2141382, 0.009006098844110966], [1437028173.68596, 2151537, 0.009006098844110966], [1437028653.584833, 2161685, 0.009006098844110966], [1437029133.792483, 2171839, 0.00846573244780302], [1437029613.661672, 2181977, 0.00846573244780302], [1437030093.641009, 2192118, 0.00846573244780302], [1437030573.656274, 2202268, 0.00846573244780302], [1437031053.643631, 2212416, 0.00846573244780302], [1437031533.777478, 2222583, 0.00846573244780302], [1437032013.704008, 2232736, 0.00846573244780302], [1437032493.638393, 2242882, 0.007957788184285164], [1437032973.684986, 2253041, 0.007957788184285164], [1437033453.699562, 2263183, 0.007957788184285164], [1437033933.918074, 2273320, 0.007957788184285164], [1437034413.596351, 2283443, 0.007957788184285164], [1437034893.640496, 2293579, 0.007957788184285164], [1437035373.637761, 2303701, 0.007957788184285164], [1437035853.669947, 2313823, 0.007957788184285164], [1437036333.78905, 2323961, 0.0074803209863603115], [1437036813.699727, 2334089, 0.0074803209863603115], [1437037293.662592, 2344235, 0.0074803209863603115], [1437037773.66716, 2354364, 0.0074803209863603115], [1437038253.603687, 2364507, 0.0074803209863603115], [1437038733.78864, 2374644, 0.0074803209863603115], [1437039213.641799, 2384782, 0.0074803209863603115], [1437039693.687078, 2394923, 0.0074803209863603115], [1437040173.635717, 2405058, 0.0070315017364919186], [1437040653.673331, 2415194, 0.0070315017364919186], [1437041133.764768, 2425322, 0.0070315017364919186], [1437041613.629279, 2435449, 0.0070315017364919186], [1437042093.703985, 2445575, 0.0070315017364919186], [1437042573.496029, 2455712, 0.0070315017364919186], [1437043053.686022, 2465844, 0.0070315017364919186], [1437043533.731929, 2475974, 0.0070315017364919186], [1437044013.636245, 2486095, 0.006609611678868532], [1437044493.69923, 2496238, 0.006609611678868532], [1437044973.652155, 2506373, 0.006609611678868532], [1437045453.691467, 2516497, 0.006609611678868532], [1437045933.935804, 2526637, 0.006609611678868532], [1437046413.635583, 2536770, 0.006609611678868532], [1437046893.626337, 2546896, 0.006609611678868532], [1437047373.67437, 2557029, 0.006609611678868532], [1437047853.652939, 2567169, 0.0062130349688231945], [1437048333.778436, 2577306, 0.0062130349688231945], [1437048813.654248, 2587433, 0.0062130349688231945], [1437049293.610609, 2597552, 0.0062130349688231945], [1437049773.646573, 2607690, 0.0062130349688231945], [1437050253.667925, 2617808, 0.0062130349688231945], [1437050733.735291, 2627933, 0.0062130349688231945], [1437051213.620222, 2638053, 0.0062130349688231945], [1437051693.601978, 2648171, 0.005840253084897995], [1437052173.634985, 2658299, 0.005840253084897995], [1437052653.687176, 2668425, 0.005840253084897995], [1437053133.762819, 2678556, 0.005840253084897995], [1437053613.643698, 2688671, 0.005840253084897995], [1437054093.673047, 2698804, 0.005840253084897995], [1437054573.667371, 2708956, 0.005840253084897995], [1437055053.650441, 2719087, 0.005840253084897995], [1437055533.778469, 2729219, 0.005489837843924761], [1437056013.694082, 2739343, 0.005489837843924761], [1437056493.674871, 2749458, 0.005489837843924761], [1437056973.700234, 2759575, 0.005489837843924761], [1437057453.666129, 2769697, 0.005489837843924761], [1437057933.848506, 2779821, 0.005489837843924761], [1437058413.643799, 2789941, 0.005489837843924761], [1437058893.715386, 2800076, 0.005489837843924761], [1437059373.62596, 2810207, 0.005160447675734758], [1437059853.650848, 2820334, 0.005160447675734758], [1437060333.792248, 2830465, 0.005160447675734758], [1437060813.682955, 2840600, 0.005160447675734758], [1437061293.681795, 2850745, 0.005160447675734758], [1437061773.691182, 2860880, 0.005160447675734758], [1437062253.662987, 2871013, 0.005160447675734758], [1437062733.760419, 2881153, 0.005160447675734758], [1437063213.651969, 2891278, 0.004850820638239384], [1437063693.723523, 2901406, 0.004850820638239384], [1437064173.68663, 2911533, 0.004850820638239384], [1437064653.547643, 2921667, 0.004850820638239384], [1437065133.62645, 2931813, 0.004850820638239384], [1437065613.566569, 2941947, 0.004850820638239384], [1437066093.537804, 2952102, 0.004850820638239384], [1437066573.529332, 2962243, 0.004850820638239384], [1437067053.520098, 2972400, 0.004559771623462439], [1437067533.605733, 2982561, 0.004559771623462439], [1437068013.535467, 2992698, 0.004559771623462439], [1437068493.559976, 3002839, 0.004559771623462439], [1437068973.558743, 3012983, 0.004559771623462439], [1437069453.562661, 3023116, 0.004559771623462439], [1437069933.627071, 3033256, 0.004559771623462439], [1437070413.574131, 3043386, 0.004286185372620821], [1437070893.658803, 3053528, 0.004286185372620821], [1437071373.638711, 3063659, 0.004286185372620821], [1437071853.621384, 3073794, 0.004286185372620821], [1437072333.665269, 3083926, 0.004286185372620821], [1437072813.584388, 3094040, 0.004286185372620821], [1437073293.569178, 3104172, 0.004286185372620821]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d3.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d3.json new file mode 100644 index 0000000000..69191b9154 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d3.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 0.0], [1436927853.989794, 23650, 7360.0], [1436929773.992781, 64316, 7360.0], [1436931694.094564, 104766, 7360.0], [1436933614.044337, 145062, 7360.0], [1436935533.819562, 185538, 7360.0], [1436937454.138383, 226331, 7360.0], [1436939373.69067, 267137, 7360.0], [1436941293.596313, 308088, 7360.0], [1436943213.621881, 349144, 7360.0], [1436945133.677298, 390180, 7360.0], [1436947053.616578, 431216, 7360.0], [1436948973.512541, 472260, 7360.0], [1436950893.563009, 513185, 7360.0], [1436952813.621004, 554042, 7360.0], [1436954733.738119, 594882, 7360.0], [1436956653.601937, 635705, 7360.0], [1436958573.646778, 676543, 7360.0], [1436960493.578778, 717383, 7360.0], [1436962413.573845, 758252, 7360.0], [1436964333.658419, 799099, 7360.0], [1436966253.586517, 839901, 7360.0], [1436968173.593885, 880748, 7360.0], [1436970093.595964, 921560, 7360.0], [1436972013.604738, 962349, 7360.0], [1436973933.689516, 1003147, 7360.0], [1436975853.524388, 1043955, 7360.0], [1436977773.575627, 1084733, 7360.0], [1436979693.699591, 1125483, 7360.0], [1436981613.563551, 1166236, 7360.0], [1436983533.684994, 1207025, 7360.0], [1436985453.690084, 1247835, 7360.0], [1436987373.596962, 1288473, 7360.0], [1436989293.756905, 1329221, 7360.0], [1436991213.663033, 1369914, 7360.0], [1436993133.835604, 1410542, 7360.0], [1436995053.666661, 1451174, 7360.0], [1436996973.668684, 1491822, 7360.0], [1436998893.640468, 1532431, 7360.0], [1437000813.602383, 1573037, 7360.0], [1437002733.67994, 1613657, 7360.0], [1437004653.610772, 1654268, 7360.0], [1437006573.625314, 1694857, 7360.0], [1437008493.655091, 1735432, 7360.0], [1437010413.632128, 1776052, 7360.0], [1437012333.706205, 1816617, 7360.0], [1437014253.618884, 1857175, 7360.0], [1437016173.63194, 1897759, 7360.0], [1437018093.626005, 1938377, 7360.0], [1437020013.667751, 1978978, 7360.0], [1437021933.848661, 2019588, 7360.0], [1437023853.686116, 2060205, 7360.0], [1437025773.695712, 2100793, 7360.0], [1437027693.694451, 2141382, 7360.0], [1437029613.661672, 2181977, 7360.0], [1437031533.777478, 2222583, 7360.0], [1437033453.699562, 2263183, 7360.0], [1437035373.637761, 2303701, 7360.0], [1437037293.662592, 2344235, 7360.0], [1437039213.641799, 2384782, 7360.0], [1437041133.764768, 2425322, 7360.0], [1437043053.686022, 2465844, 7360.0], [1437044973.652155, 2506373, 7360.0], [1437046893.626337, 2546896, 7862.0], [1437048813.654248, 2587433, 7862.0], [1437050733.735291, 2627933, 7862.0], [1437052653.687176, 2668425, 7862.0], [1437054573.667371, 2708956, 7862.0], [1437056493.674871, 2749458, 7862.0], [1437058413.643799, 2789941, 7862.0], [1437060333.792248, 2830465, 7862.0], [1437062253.662987, 2871013, 7862.0], [1437064173.68663, 2911533, 7862.0], [1437066093.537804, 2952102, 7862.0], [1437068013.535467, 2992698, 7862.0], [1437069933.627071, 3033256, 7862.0], [1437071853.621384, 3073794, 7862.0], [1437072333.665269, 3083926, 7862.0], [1437072813.584388, 3094040, 7862.0], [1437073293.569178, 3104172, 7862.0]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d4.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d4.json new file mode 100644 index 0000000000..caf1ae6e7f --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/alpha/d4.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 2.461352825164795], [1436926413.945391, 1476, 12.772720336914062], [1436926893.945037, 6006, 12.195232391357422], [1436927373.995472, 13786, 11.528279304504395], [1436927853.989794, 23650, 10.722719192504883], [1436928334.132361, 33755, 10.215253829956055], [1436928813.973288, 43941, 9.730447769165039], [1436929293.975949, 54146, 9.399007797241211], [1436929773.992781, 64316, 9.1018648147583], [1436930253.997415, 74465, 8.961446762084961], [1436930734.203004, 84611, 8.757476806640625], [1436931214.03644, 94700, 8.4615478515625], [1436931694.094564, 104766, 8.506814956665039], [1436932174.114955, 114817, 8.246719360351562], [1436932654.161382, 124880, 8.329349517822266], [1436933133.960214, 134977, 7.90853214263916], [1436933614.044337, 145062, 8.192558288574219], [1436934094.166206, 155169, 7.865443229675293], [1436934574.106036, 165284, 7.910976886749268], [1436935054.150647, 175402, 7.925509929656982], [1436935533.819562, 185538, 7.866455078125], [1436936013.710422, 195712, 7.9123406410217285], [1436936493.609025, 205906, 7.748654842376709], [1436936973.683892, 216099, 7.849164009094238], [1436937454.138383, 226331, 7.784902572631836], [1436937933.838475, 236532, 7.749933242797852], [1436938413.89688, 246724, 7.777050971984863], [1436938894.018652, 256925, 7.663984775543213], [1436939373.69067, 267137, 7.602056980133057], [1436939853.673692, 277369, 7.539070129394531], [1436940333.651346, 287620, 7.575552463531494], [1436940813.599579, 297848, 7.47900390625], [1436941293.596313, 308088, 7.403858184814453], [1436941773.659172, 318362, 7.589539527893066], [1436942253.648479, 328621, 7.511919975280762], [1436942733.752284, 338892, 7.31054162979126], [1436943213.621881, 349144, 7.261094570159912], [1436943693.698743, 359399, 7.552957534790039], [1436944173.578463, 369649, 7.449452877044678], [1436944653.692217, 379912, 7.177209854125977], [1436945133.677298, 390180, 7.308793067932129], [1436945613.572411, 400445, 7.229344844818115], [1436946093.56123, 410703, 7.129981994628906], [1436946573.542364, 420958, 7.127549171447754], [1436947053.616578, 431216, 7.538583755493164], [1436947533.636973, 441483, 7.030594825744629], [1436948013.541574, 451751, 6.98097038269043], [1436948493.560223, 462015, 7.213271141052246], [1436948973.512541, 472260, 7.1727519035339355], [1436949453.550055, 482483, 6.985068321228027], [1436949933.828011, 492731, 7.051283836364746], [1436950413.603177, 502957, 7.082402229309082], [1436950893.563009, 513185, 7.1637864112854], [1436951373.620887, 523410, 7.193849086761475], [1436951853.61941, 533618, 7.1212921142578125], [1436952333.694447, 543828, 7.208009719848633], [1436952813.621004, 554042, 7.28671932220459], [1436953293.588156, 564251, 6.941026210784912], [1436953773.599734, 574464, 7.230144500732422], [1436954253.621309, 584672, 6.815900802612305], [1436954733.738119, 594882, 7.060589790344238], [1436955213.56617, 605091, 7.079995155334473], [1436955693.585366, 615296, 7.300849437713623], [1436956173.626395, 625501, 6.927395343780518], [1436956653.601937, 635705, 6.893837928771973], [1436957133.665878, 645915, 6.965301990509033], [1436957613.584762, 656116, 6.902514457702637], [1436958093.549783, 666331, 7.2444868087768555], [1436958573.646778, 676543, 6.784783840179443], [1436959053.585655, 686750, 6.800273418426514], [1436959533.679696, 696961, 6.743415355682373], [1436960013.633292, 707173, 7.012747764587402], [1436960493.578778, 717383, 6.548677921295166], [1436960973.596715, 727598, 6.638228416442871], [1436961453.625644, 737818, 6.884350776672363], [1436961933.740339, 748040, 6.797428607940674], [1436962413.573845, 758252, 6.815422058105469], [1436962893.610678, 768470, 6.7392377853393555], [1436963373.642878, 778674, 6.8375959396362305], [1436963853.558388, 788877, 6.7254252433776855], [1436964333.658419, 799099, 6.765130996704102], [1436964813.573319, 809289, 6.7060980796813965], [1436965293.542098, 819484, 6.63279390335083], [1436965773.545453, 829687, 6.587352752685547], [1436966253.586517, 839901, 6.4957275390625], [1436966733.639348, 850120, 6.765798091888428], [1436967213.697288, 860330, 6.681786060333252], [1436967693.617172, 870539, 6.696804523468018], [1436968173.593885, 880748, 6.571035385131836], [1436968653.560836, 890955, 6.29492712020874], [1436969133.676337, 901164, 6.679598331451416], [1436969613.506638, 911358, 6.548522472381592], [1436970093.595964, 921560, 6.585646629333496], [1436970573.541227, 931756, 6.589619159698486], [1436971053.624316, 941945, 6.333208084106445], [1436971533.655543, 952138, 6.582470417022705], [1436972013.604738, 962349, 6.289045810699463], [1436972493.613199, 972551, 6.360206127166748], [1436972973.501155, 982746, 6.567287921905518], [1436973453.64842, 992945, 6.246123313903809], [1436973933.689516, 1003147, 6.44004487991333], [1436974413.577769, 1013350, 6.315634727478027], [1436974893.542281, 1023545, 6.289544105529785], [1436975373.638453, 1033759, 6.412042140960693], [1436975853.524388, 1043955, 6.165371894836426], [1436976333.625792, 1054148, 6.403027534484863], [1436976813.610661, 1064342, 6.37597131729126], [1436977293.601581, 1074539, 6.336863994598389], [1436977773.575627, 1084733, 6.377552032470703], [1436978253.564972, 1094914, 6.28995943069458], [1436978733.673144, 1105109, 6.28420352935791], [1436979213.540585, 1115293, 6.277828216552734], [1436979693.699591, 1125483, 6.185207843780518], [1436980173.613012, 1135670, 6.186310768127441], [1436980653.575769, 1145862, 5.922095775604248], [1436981133.719264, 1156045, 6.141305923461914], [1436981613.563551, 1166236, 6.10508394241333], [1436982093.553233, 1176436, 5.967081069946289], [1436982573.577846, 1186636, 5.960882186889648], [1436983053.605749, 1196837, 6.2222185134887695], [1436983533.684994, 1207025, 6.051136493682861], [1436984013.561492, 1217233, 6.087917804718018], [1436984493.629873, 1227437, 5.95945405960083], [1436984973.606714, 1237643, 5.971570014953613], [1436985453.690084, 1247835, 5.969781398773193], [1436985933.711388, 1257951, 6.040994644165039], [1436986413.598807, 1268125, 6.142050743103027], [1436986893.631797, 1278290, 6.03120231628418], [1436987373.596962, 1288473, 5.921470642089844], [1436987853.555549, 1298650, 5.921937942504883], [1436988333.722032, 1308841, 6.050085067749023], [1436988813.55697, 1319018, 5.837893486022949], [1436989293.756905, 1329221, 5.927487850189209], [1436989773.665141, 1339417, 6.117348670959473], [1436990253.768302, 1349610, 6.052918434143066], [1436990733.708919, 1359759, 5.8977789878845215], [1436991213.663033, 1369914, 5.903198719024658], [1436991693.730925, 1380074, 5.85245418548584], [1436992173.751791, 1390224, 5.902153968811035], [1436992653.758682, 1400383, 5.822136878967285], [1436993133.835604, 1410542, 5.88037633895874], [1436993613.674655, 1420684, 5.778636932373047], [1436994093.747454, 1430832, 5.876591682434082], [1436994573.768973, 1440986, 6.196285724639893], [1436995053.666661, 1451174, 5.7718634605407715], [1436995533.83439, 1461345, 5.931266784667969], [1436996013.556996, 1471495, 5.9706597328186035], [1436996493.635477, 1481663, 5.589694023132324], [1436996973.668684, 1491822, 5.787637233734131], [1436997453.59326, 1501979, 5.634321689605713], [1436997933.774019, 1512139, 5.699962615966797], [1436998413.575162, 1522290, 5.807012557983398], [1436998893.640468, 1532431, 5.559602737426758], [1436999373.551661, 1542579, 5.918235778808594], [1436999853.57906, 1552734, 5.745569229125977], [1437000333.680409, 1562888, 5.59443473815918], [1437000813.602383, 1573037, 5.703190326690674], [1437001293.610337, 1583190, 5.468636512756348], [1437001773.618199, 1593341, 5.610755920410156], [1437002253.572966, 1603497, 5.4396867752075195], [1437002733.67994, 1613657, 5.7537946701049805], [1437003213.583266, 1623809, 5.7613725662231445], [1437003693.639943, 1633966, 5.439754009246826], [1437004173.568287, 1644113, 5.4889116287231445], [1437004653.610772, 1654268, 5.39843225479126], [1437005133.663045, 1664424, 5.576738357543945], [1437005613.580984, 1674567, 5.662004470825195], [1437006093.601019, 1684715, 5.3926777839660645], [1437006573.625314, 1694857, 5.464866638183594], [1437007053.584514, 1704999, 5.40261173248291], [1437007533.719303, 1715150, 5.23733377456665], [1437008013.604962, 1725282, 5.448479652404785], [1437008493.655091, 1735432, 5.684703826904297], [1437008973.640165, 1745584, 5.400024890899658], [1437009453.715067, 1755742, 5.378822326660156], [1437009933.765712, 1765896, 5.45297384262085], [1437010413.632128, 1776052, 5.248030185699463], [1437010893.66766, 1786195, 5.3377580642700195], [1437011373.636164, 1796346, 5.292956352233887], [1437011853.631224, 1806481, 5.438100814819336], [1437012333.706205, 1816617, 5.148743629455566], [1437012813.61987, 1826754, 5.319127559661865], [1437013293.479904, 1836883, 5.1646199226379395], [1437013773.604574, 1847029, 5.494720458984375], [1437014253.618884, 1857175, 5.17764949798584], [1437014733.756419, 1867312, 5.14331579208374], [1437015213.638607, 1877459, 5.309914588928223], [1437015693.625763, 1887608, 5.542352676391602], [1437016173.63194, 1897759, 5.075393199920654], [1437016653.609074, 1907909, 5.249225616455078], [1437017133.717601, 1918074, 5.392384052276611], [1437017613.716011, 1928220, 5.38590669631958], [1437018093.626005, 1938377, 5.229607105255127], [1437018573.626522, 1948523, 5.287610054016113], [1437019053.648174, 1958678, 5.2798333168029785], [1437019533.803011, 1968831, 5.151246070861816], [1437020013.667751, 1978978, 5.118294715881348], [1437020493.659028, 1989133, 5.327050685882568], [1437020973.657346, 1999287, 5.174264430999756], [1437021453.650634, 2009437, 5.1660661697387695], [1437021933.848661, 2019588, 5.089689254760742], [1437022413.674963, 2029736, 5.06661319732666], [1437022893.69086, 2039894, 5.031608581542969], [1437023373.68883, 2050054, 4.874476432800293], [1437023853.686116, 2060205, 5.107512474060059], [1437024333.763876, 2070362, 5.135380268096924], [1437024813.707845, 2080507, 5.087984561920166], [1437025293.483294, 2090645, 5.240448474884033], [1437025773.695712, 2100793, 4.930302619934082], [1437026253.672994, 2110943, 4.914392471313477], [1437026733.780775, 2121094, 5.182378768920898], [1437027213.617849, 2131235, 4.93843412399292], [1437027693.694451, 2141382, 4.924433708190918], [1437028173.68596, 2151537, 4.957921028137207], [1437028653.584833, 2161685, 5.040386199951172], [1437029133.792483, 2171839, 5.01956033706665], [1437029613.661672, 2181977, 4.987490177154541], [1437030093.641009, 2192118, 4.960195064544678], [1437030573.656274, 2202268, 5.0094523429870605], [1437031053.643631, 2212416, 4.83445930480957], [1437031533.777478, 2222583, 4.922268390655518], [1437032013.704008, 2232736, 5.113382339477539], [1437032493.638393, 2242882, 4.881488800048828], [1437032973.684986, 2253041, 4.953296661376953], [1437033453.699562, 2263183, 4.865671157836914], [1437033933.918074, 2273320, 4.829331874847412], [1437034413.596351, 2283443, 4.777036190032959], [1437034893.640496, 2293579, 4.864566326141357], [1437035373.637761, 2303701, 4.988693714141846], [1437035853.669947, 2313823, 5.016432285308838], [1437036333.78905, 2323961, 4.651939868927002], [1437036813.699727, 2334089, 4.767807960510254], [1437037293.662592, 2344235, 4.628738880157471], [1437037773.66716, 2354364, 4.929834842681885], [1437038253.603687, 2364507, 4.739555835723877], [1437038733.78864, 2374644, 4.821824073791504], [1437039213.641799, 2384782, 4.853730201721191], [1437039693.687078, 2394923, 4.581423759460449], [1437040173.635717, 2405058, 4.452754497528076], [1437040653.673331, 2415194, 4.837629318237305], [1437041133.764768, 2425322, 4.752482891082764], [1437041613.629279, 2435449, 4.730231761932373], [1437042093.703985, 2445575, 4.5618896484375], [1437042573.496029, 2455712, 4.673112869262695], [1437043053.686022, 2465844, 4.565918922424316], [1437043533.731929, 2475974, 4.7191481590271], [1437044013.636245, 2486095, 4.589008331298828], [1437044493.69923, 2496238, 4.599475383758545], [1437044973.652155, 2506373, 4.544175624847412], [1437045453.691467, 2516497, 4.4221673011779785], [1437045933.935804, 2526637, 4.44448709487915], [1437046413.635583, 2536770, 4.647110939025879], [1437046893.626337, 2546896, 4.768988609313965], [1437047373.67437, 2557029, 4.5318827629089355], [1437047853.652939, 2567169, 4.501277923583984], [1437048333.778436, 2577306, 4.6167216300964355], [1437048813.654248, 2587433, 4.66096305847168], [1437049293.610609, 2597552, 4.529193878173828], [1437049773.646573, 2607690, 4.455351829528809], [1437050253.667925, 2617808, 4.51211404800415], [1437050733.735291, 2627933, 4.803231716156006], [1437051213.620222, 2638053, 4.645476341247559], [1437051693.601978, 2648171, 4.419768810272217], [1437052173.634985, 2658299, 4.48175048828125], [1437052653.687176, 2668425, 4.397725582122803], [1437053133.762819, 2678556, 4.188413619995117], [1437053613.643698, 2688671, 4.291479110717773], [1437054093.673047, 2698804, 4.321218013763428], [1437054573.667371, 2708956, 4.311710834503174], [1437055053.650441, 2719087, 4.481810092926025], [1437055533.778469, 2729219, 4.452049255371094], [1437056013.694082, 2739343, 4.455989360809326], [1437056493.674871, 2749458, 4.415104866027832], [1437056973.700234, 2759575, 4.259828567504883], [1437057453.666129, 2769697, 4.510563373565674], [1437057933.848506, 2779821, 4.221935272216797], [1437058413.643799, 2789941, 4.437899112701416], [1437058893.715386, 2800076, 4.302872657775879], [1437059373.62596, 2810207, 4.228428363800049], [1437059853.650848, 2820334, 4.220061779022217], [1437060333.792248, 2830465, 4.138088703155518], [1437060813.682955, 2840600, 4.2196125984191895], [1437061293.681795, 2850745, 4.1594085693359375], [1437061773.691182, 2860880, 4.179514408111572], [1437062253.662987, 2871013, 4.202476978302002], [1437062733.760419, 2881153, 4.282044887542725], [1437063213.651969, 2891278, 4.200533866882324], [1437063693.723523, 2901406, 4.263350486755371], [1437064173.68663, 2911533, 4.378939628601074], [1437064653.547643, 2921667, 4.202810287475586], [1437065133.62645, 2931813, 4.193121910095215], [1437065613.566569, 2941947, 4.132870197296143], [1437066093.537804, 2952102, 4.35767936706543], [1437066573.529332, 2962243, 4.211732864379883], [1437067053.520098, 2972400, 4.020431041717529], [1437067533.605733, 2982561, 4.342063903808594], [1437068013.535467, 2992698, 4.197565078735352], [1437068493.559976, 3002839, 3.8806259632110596], [1437068973.558743, 3012983, 3.871702194213867], [1437069453.562661, 3023116, 4.064865589141846], [1437069933.627071, 3033256, 3.817744731903076], [1437070413.574131, 3043386, 4.106888294219971], [1437070893.658803, 3053528, 4.235474586486816], [1437071373.638711, 3063659, 4.127055644989014], [1437071853.621384, 3073794, 4.176018238067627], [1437072333.665269, 3083926, 4.048959732055664], [1437072813.584388, 3094040, 4.178991794586182], [1437073293.569178, 3104172, 3.8385396003723145]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d1.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d1.json new file mode 100644 index 0000000000..27ff64e5dd --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d1.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 1.009283951483666897], [1436926413.945391, 1476, 0.932567862421274185], [1436926893.945037, 6006, 0.02773338556289673], [1436927373.995472, 13786, 0.021291319280862808], [1436927853.989794, 23650, 0.515582754276692867], [1436928334.132361, 33755, 0.011689444072544575], [1436928813.973288, 43941, 0.009183925576508045], [1436929293.975949, 54146, 0.007850822061300278], [1436929773.992781, 64316, 0.007189035415649414], [1436930253.997415, 74465, 0.007230754010379314], [1436930734.203004, 84611, 0.007685001939535141], [1436931214.03644, 94700, 0.008264732547104359], [1436931694.094564, 104766, 0.008946491405367851], [1436932174.114955, 114817, 0.00966302677989006], [1436932654.161382, 124880, 0.010994276031851768], [1436933133.960214, 134977, 0.01196141354739666], [1436933614.044337, 145062, 0.012673594057559967], [1436934094.166206, 155169, 0.013639944605529308], [1436934574.106036, 165284, 0.014305333606898785], [1436935054.150647, 175402, 0.014946178533136845], [1436935533.819562, 185538, 0.015736915171146393], [1436936013.710422, 195712, 0.01633097417652607], [1436936493.609025, 205906, 0.01669587567448616], [1436936973.683892, 216099, 0.017459288239479065], [1436937454.138383, 226331, 0.018532060086727142], [1436937933.838475, 236532, 0.01949254982173443], [1436938413.89688, 246724, 0.01951725408434868], [1436938894.018652, 256925, 0.019763393327593803], [1436939373.69067, 267137, 0.02008610963821411], [1436939853.673692, 277369, 0.021090799942612648], [1436940333.651346, 287620, 0.021408839151263237], [1436940813.599579, 297848, 0.021988894790410995], [1436941293.596313, 308088, 0.02236073836684227], [1436941773.659172, 318362, 0.022547174245119095], [1436942253.648479, 328621, 0.02303086407482624], [1436942733.752284, 338892, 0.023787079378962517], [1436943213.621881, 349144, 0.024007514119148254], [1436943693.698743, 359399, 0.02414763905107975], [1436944173.578463, 369649, 0.024576496332883835], [1436944653.692217, 379912, 0.02469169721007347], [1436945133.677298, 390180, 0.024951916188001633], [1436945613.572411, 400445, 0.025548970326781273], [1436946093.56123, 410703, 0.025769377127289772], [1436946573.542364, 420958, 0.02602097950875759], [1436947053.616578, 431216, 0.026028109714388847], [1436947533.636973, 441483, 0.026348495855927467], [1436948013.541574, 451751, 0.02621930092573166], [1436948493.560223, 462015, 0.02671053633093834], [1436948973.512541, 472260, 0.0272178016602993], [1436949453.550055, 482483, 0.02734796144068241], [1436949933.828011, 492731, 0.027217809110879898], [1436950413.603177, 502957, 0.027318621054291725], [1436950893.563009, 513185, 0.027304155752062798], [1436951373.620887, 523410, 0.027759933844208717], [1436951853.61941, 533618, 0.028056234121322632], [1436952333.694447, 543828, 0.028620803728699684], [1436952813.621004, 554042, 0.028957637026906013], [1436953293.588156, 564251, 0.029187509790062904], [1436953773.599734, 574464, 0.028960268944501877], [1436954253.621309, 584672, 0.02891424670815468], [1436954733.738119, 594882, 0.029211293905973434], [1436955213.56617, 605091, 0.029444213956594467], [1436955693.585366, 615296, 0.02974688820540905], [1436956173.626395, 625501, 0.03026159666478634], [1436956653.601937, 635705, 0.03039497137069702], [1436957133.665878, 645915, 0.03041839227080345], [1436957613.584762, 656116, 0.030588043853640556], [1436958093.549783, 666331, 0.030284974724054337], [1436958573.646778, 676543, 0.030354496091604233], [1436959053.585655, 686750, 0.030551007017493248], [1436959533.679696, 696961, 0.03068561479449272], [1436960013.633292, 707173, 0.030921893194317818], [1436960493.578778, 717383, 0.031080031767487526], [1436960973.596715, 727598, 0.030773505568504333], [1436961453.625644, 737818, 0.03084484674036503], [1436961933.740339, 748040, 0.03110458515584469], [1436962413.573845, 758252, 0.03114113211631775], [1436962893.610678, 768470, 0.03101053647696972], [1436963373.642878, 778674, 0.03110116347670555], [1436963853.558388, 788877, 0.031342316418886185], [1436964333.658419, 799099, 0.03130127117037773], [1436964813.573319, 809289, 0.031288161873817444], [1436965293.542098, 819484, 0.031435444951057434], [1436965773.545453, 829687, 0.03166936710476875], [1436966253.586517, 839901, 0.03169429674744606], [1436966733.639348, 850120, 0.03191458433866501], [1436967213.697288, 860330, 0.03205746412277222], [1436967693.617172, 870539, 0.03206293657422066], [1436968173.593885, 880748, 0.031957853585481644], [1436968653.560836, 890955, 0.0316658616065979], [1436969133.676337, 901164, 0.031929533928632736], [1436969613.506638, 911358, 0.03174331784248352], [1436970093.595964, 921560, 0.03157960623502731], [1436970573.541227, 931756, 0.03176721930503845], [1436971053.624316, 941945, 0.031810544431209564], [1436971533.655543, 952138, 0.031946416944265366], [1436972013.604738, 962349, 0.03205405920743942], [1436972493.613199, 972551, 0.031924981623888016], [1436972973.501155, 982746, 0.03199697285890579], [1436973453.64842, 992945, 0.03204970061779022], [1436973933.689516, 1003147, 0.032020214945077896], [1436974413.577769, 1013350, 0.03207497298717499], [1436974893.542281, 1023545, 0.03221454098820686], [1436975373.638453, 1033759, 0.032191887497901917], [1436975853.524388, 1043955, 0.03240729123353958], [1436976333.625792, 1054148, 0.032219529151916504], [1436976813.610661, 1064342, 0.03200426697731018], [1436977293.601581, 1074539, 0.03198647499084473], [1436977773.575627, 1084733, 0.0320645235478878], [1436978253.564972, 1094914, 0.0322980061173439], [1436978733.673144, 1105109, 0.032482605427503586], [1436979213.540585, 1115293, 0.032628435641527176], [1436979693.699591, 1125483, 0.032744552940130234], [1436980173.613012, 1135670, 0.03268158435821533], [1436980653.575769, 1145862, 0.0324023962020874], [1436981133.719264, 1156045, 0.03237328305840492], [1436981613.563551, 1166236, 0.03202575817704201], [1436982093.553233, 1176436, 0.03216284513473511], [1436982573.577846, 1186636, 0.03232415020465851], [1436983053.605749, 1196837, 0.0324099175632], [1436983533.684994, 1207025, 0.03245137259364128], [1436984013.561492, 1217233, 0.032246463000774384], [1436984493.629873, 1227437, 0.032042667269706726], [1436984973.606714, 1237643, 0.0318642184138298], [1436985453.690084, 1247835, 0.03191140666604042], [1436985933.711388, 1257951, 0.032287366688251495], [1436986413.598807, 1268125, 0.03226638585329056], [1436986893.631797, 1278290, 0.03252791240811348], [1436987373.596962, 1288473, 0.03241675719618797], [1436987853.555549, 1298650, 0.032103829085826874], [1436988333.722032, 1308841, 0.031904906034469604], [1436988813.55697, 1319018, 0.03179024159908295], [1436989293.756905, 1329221, 0.03168707340955734], [1436989773.665141, 1339417, 0.03160175681114197], [1436990253.768302, 1349610, 0.03161788731813431], [1436990733.708919, 1359759, 0.031772397458553314], [1436991213.663033, 1369914, 0.031758904457092285], [1436991693.730925, 1380074, 0.031629469245672226], [1436992173.751791, 1390224, 0.03154703974723816], [1436992653.758682, 1400383, 0.031527940183877945], [1436993133.835604, 1410542, 0.03169580549001694], [1436993613.674655, 1420684, 0.03182605654001236], [1436994093.747454, 1430832, 0.03185024857521057], [1436994573.768973, 1440986, 0.03199737146496773], [1436995053.666661, 1451174, 0.03156095743179321], [1436995533.83439, 1461345, 0.03150693327188492], [1436996013.556996, 1471495, 0.031496383249759674], [1436996493.635477, 1481663, 0.0313432440161705], [1436996973.668684, 1491822, 0.031145794317126274], [1436997453.59326, 1501979, 0.03106667660176754], [1436997933.774019, 1512139, 0.03143244981765747], [1436998413.575162, 1522290, 0.03142988309264183], [1436998893.640468, 1532431, 0.03132546320557594], [1436999373.551661, 1542579, 0.03125471621751785], [1436999853.57906, 1552734, 0.03098788857460022], [1437000333.680409, 1562888, 0.0308846328407526], [1437000813.602383, 1573037, 0.03082612156867981], [1437001293.610337, 1583190, 0.030793681740760803], [1437001773.618199, 1593341, 0.03087364137172699], [1437002253.572966, 1603497, 0.030839646235108376], [1437002733.67994, 1613657, 0.030705047771334648], [1437003213.583266, 1623809, 0.03071814589202404], [1437003693.639943, 1633966, 0.0304812490940094], [1437004173.568287, 1644113, 0.03030412085354328], [1437004653.610772, 1654268, 0.03032425045967102], [1437005133.663045, 1664424, 0.030430471524596214], [1437005613.580984, 1674567, 0.03036225587129593], [1437006093.601019, 1684715, 0.03056645393371582], [1437006573.625314, 1694857, 0.03043070062994957], [1437007053.584514, 1704999, 0.030224520713090897], [1437007533.719303, 1715150, 0.03024231642484665], [1437008013.604962, 1725282, 0.03009769506752491], [1437008493.655091, 1735432, 0.030214866623282433], [1437008973.640165, 1745584, 0.030181538313627243], [1437009453.715067, 1755742, 0.03017231822013855], [1437009933.765712, 1765896, 0.030141284689307213], [1437010413.632128, 1776052, 0.030052203685045242], [1437010893.66766, 1786195, 0.030078601092100143], [1437011373.636164, 1796346, 0.029969291761517525], [1437011853.631224, 1806481, 0.02999536693096161], [1437012333.706205, 1816617, 0.030100464820861816], [1437012813.61987, 1826754, 0.03008824959397316], [1437013293.479904, 1836883, 0.029995709657669067], [1437013773.604574, 1847029, 0.02995096519589424], [1437014253.618884, 1857175, 0.02980179339647293], [1437014733.756419, 1867312, 0.029607007279992104], [1437015213.638607, 1877459, 0.02952035330235958], [1437015693.625763, 1887608, 0.02937002293765545], [1437016173.63194, 1897759, 0.029285306110978127], [1437016653.609074, 1907909, 0.029194746166467667], [1437017133.717601, 1918074, 0.029153630137443542], [1437017613.716011, 1928220, 0.029063496738672256], [1437018093.626005, 1938377, 0.028990253806114197], [1437018573.626522, 1948523, 0.0290801040828228], [1437019053.648174, 1958678, 0.029026925563812256], [1437019533.803011, 1968831, 0.029071522876620293], [1437020013.667751, 1978978, 0.02911040186882019], [1437020493.659028, 1989133, 0.02908971533179283], [1437020973.657346, 1999287, 0.028982823714613914], [1437021453.650634, 2009437, 0.028793631121516228], [1437021933.848661, 2019588, 0.02868799678981304], [1437022413.674963, 2029736, 0.028585929423570633], [1437022893.69086, 2039894, 0.028488371521234512], [1437023373.68883, 2050054, 0.028293771669268608], [1437023853.686116, 2060205, 0.028227869421243668], [1437024333.763876, 2070362, 0.0280953086912632], [1437024813.707845, 2080507, 0.02794187143445015], [1437025293.483294, 2090645, 0.0278786551207304], [1437025773.695712, 2100793, 0.02786232903599739], [1437026253.672994, 2110943, 0.02783624827861786], [1437026733.780775, 2121094, 0.027756746858358383], [1437027213.617849, 2131235, 0.027644069865345955], [1437027693.694451, 2141382, 0.02752004750072956], [1437028173.68596, 2151537, 0.0274327602237463], [1437028653.584833, 2161685, 0.027434347197413445], [1437029133.792483, 2171839, 0.02731819450855255], [1437029613.661672, 2181977, 0.027138520032167435], [1437030093.641009, 2192118, 0.027088932693004608], [1437030573.656274, 2202268, 0.02713087759912014], [1437031053.643631, 2212416, 0.027159670367836952], [1437031533.777478, 2222583, 0.027089878916740417], [1437032013.704008, 2232736, 0.026989545673131943], [1437032493.638393, 2242882, 0.02692277729511261], [1437032973.684986, 2253041, 0.026783647015690804], [1437033453.699562, 2263183, 0.026735099032521248], [1437033933.918074, 2273320, 0.02665248140692711], [1437034413.596351, 2283443, 0.02659791149199009], [1437034893.640496, 2293579, 0.026540575549006462], [1437035373.637761, 2303701, 0.02647154964506626], [1437035853.669947, 2313823, 0.02645135670900345], [1437036333.78905, 2323961, 0.026429900899529457], [1437036813.699727, 2334089, 0.026324935257434845], [1437037293.662592, 2344235, 0.026287639513611794], [1437037773.66716, 2354364, 0.02626391313970089], [1437038253.603687, 2364507, 0.026225272566080093], [1437038733.78864, 2374644, 0.026248561218380928], [1437039213.641799, 2384782, 0.026243599131703377], [1437039693.687078, 2394923, 0.026255469769239426], [1437040173.635717, 2405058, 0.026186810806393623], [1437040653.673331, 2415194, 0.02606010064482689], [1437041133.764768, 2425322, 0.026031550019979477], [1437041613.629279, 2435449, 0.02595149166882038], [1437042093.703985, 2445575, 0.025885630398988724], [1437042573.496029, 2455712, 0.025858554989099503], [1437043053.686022, 2465844, 0.0257696695625782], [1437043533.731929, 2475974, 0.02574242651462555], [1437044013.636245, 2486095, 0.025741754099726677], [1437044493.69923, 2496238, 0.02561314031481743], [1437044973.652155, 2506373, 0.02550213597714901], [1437045453.691467, 2516497, 0.025422468781471252], [1437045933.935804, 2526637, 0.025300107896327972], [1437046413.635583, 2536770, 0.02533198893070221], [1437046893.626337, 2546896, 0.025261884555220604], [1437047373.67437, 2557029, 0.025176096707582474], [1437047853.652939, 2567169, 0.025054505094885826], [1437048333.778436, 2577306, 0.024978378787636757], [1437048813.654248, 2587433, 0.024952610954642296], [1437049293.610609, 2597552, 0.02484666183590889], [1437049773.646573, 2607690, 0.024764036759734154], [1437050253.667925, 2617808, 0.024689028039574623], [1437050733.735291, 2627933, 0.024599267169833183], [1437051213.620222, 2638053, 0.024585112929344177], [1437051693.601978, 2648171, 0.024474989622831345], [1437052173.634985, 2658299, 0.024343013763427734], [1437052653.687176, 2668425, 0.024294432252645493], [1437053133.762819, 2678556, 0.024164099246263504], [1437053613.643698, 2688671, 0.024035055190324783], [1437054093.673047, 2698804, 0.024000361561775208], [1437054573.667371, 2708956, 0.023914529010653496], [1437055053.650441, 2719087, 0.023955287411808968], [1437055533.778469, 2729219, 0.023859601467847824], [1437056013.694082, 2739343, 0.023759596049785614], [1437056493.674871, 2749458, 0.02367720566689968], [1437056973.700234, 2759575, 0.023645451292395592], [1437057453.666129, 2769697, 0.023565715178847313], [1437057933.848506, 2779821, 0.023514313623309135], [1437058413.643799, 2789941, 0.023489659652113914], [1437058893.715386, 2800076, 0.023429812863469124], [1437059373.62596, 2810207, 0.023344023153185844], [1437059853.650848, 2820334, 0.023226741701364517], [1437060333.792248, 2830465, 0.023134270682930946], [1437060813.682955, 2840600, 0.02305578999221325], [1437061293.681795, 2850745, 0.02298513427376747], [1437061773.691182, 2860880, 0.022913720458745956], [1437062253.662987, 2871013, 0.022864067927002907], [1437062733.760419, 2881153, 0.02278953418135643], [1437063213.651969, 2891278, 0.02276339940726757], [1437063693.723523, 2901406, 0.022675812244415283], [1437064173.68663, 2911533, 0.022622767835855484], [1437064653.547643, 2921667, 0.02255198359489441], [1437065133.62645, 2931813, 0.022431762889027596], [1437065613.566569, 2941947, 0.022368362173438072], [1437066093.537804, 2952102, 0.022323831915855408], [1437066573.529332, 2962243, 0.02226843684911728], [1437067053.520098, 2972400, 0.022210361436009407], [1437067533.605733, 2982561, 0.022118505090475082], [1437068013.535467, 2992698, 0.022013112902641296], [1437068493.559976, 3002839, 0.02197197824716568], [1437068973.558743, 3012983, 0.02191166952252388], [1437069453.562661, 3023116, 0.021851476281881332], [1437069933.627071, 3033256, 0.021762533113360405], [1437070413.574131, 3043386, 0.021733969449996948], [1437070893.658803, 3053528, 0.021669406443834305], [1437071373.638711, 3063659, 0.02159426547586918], [1437071853.621384, 3073794, 0.02153114229440689], [1437072333.665269, 3083926, 0.021499117836356163], [1437072813.584388, 3094040, 0.021457014605402946], [1437073293.569178, 3104172, 0.021365314722061157]] diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d2.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d2.json new file mode 100644 index 0000000000..fb5a18d53a --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d2.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 0.01034154836088419], [1436926413.945391, 1476, 0.03646053001284599], [1436926893.945037, 6006, 0.031110260635614395], [1436927373.995472, 13786, 0.024214591830968857], [1436927853.989794, 23650, 0.01820789836347103], [1436928334.132361, 33755, 0.01442798599600792], [1436928813.973288, 43941, 0.012150184251368046], [1436929293.975949, 54146, 0.011141776107251644], [1436929773.992781, 64316, 0.010859030298888683], [1436930253.997415, 74465, 0.011160558089613914], [1436930734.203004, 84611, 0.011997541412711143], [1436931214.03644, 94700, 0.01278648804873228], [1436931694.094564, 104766, 0.014073861762881279], [1436932174.114955, 114817, 0.01523376815021038], [1436932654.161382, 124880, 0.016527879983186722], [1436933133.960214, 134977, 0.01782997138798237], [1436933614.044337, 145062, 0.019055265933275223], [1436934094.166206, 155169, 0.02028629370033741], [1436934574.106036, 165284, 0.02116803079843521], [1436935054.150647, 175402, 0.022192901000380516], [1436935533.819562, 185538, 0.022869590669870377], [1436936013.710422, 195712, 0.023398980498313904], [1436936493.609025, 205906, 0.02443159930408001], [1436936973.683892, 216099, 0.025154944509267807], [1436937454.138383, 226331, 0.025802481919527054], [1436937933.838475, 236532, 0.027000702917575836], [1436938413.89688, 246724, 0.02752412110567093], [1436938894.018652, 256925, 0.0278119258582592], [1436939373.69067, 267137, 0.027698883786797523], [1436939853.673692, 277369, 0.028744956478476524], [1436940333.651346, 287620, 0.029281964525580406], [1436940813.599579, 297848, 0.03002205118536949], [1436941293.596313, 308088, 0.030467400327324867], [1436941773.659172, 318362, 0.03132195770740509], [1436942253.648479, 328621, 0.031431782990694046], [1436942733.752284, 338892, 0.03147844970226288], [1436943213.621881, 349144, 0.032013144344091415], [1436943693.698743, 359399, 0.03241390734910965], [1436944173.578463, 369649, 0.03261363133788109], [1436944653.692217, 379912, 0.033306822180747986], [1436945133.677298, 390180, 0.03390969708561897], [1436945613.572411, 400445, 0.03396527096629143], [1436946093.56123, 410703, 0.03388286381959915], [1436946573.542364, 420958, 0.03399669751524925], [1436947053.616578, 431216, 0.03394070267677307], [1436947533.636973, 441483, 0.03419327735900879], [1436948013.541574, 451751, 0.0342416949570179], [1436948493.560223, 462015, 0.034808479249477386], [1436948973.512541, 472260, 0.03552314639091492], [1436949453.550055, 482483, 0.036012329161167145], [1436949933.828011, 492731, 0.035826291888952255], [1436950413.603177, 502957, 0.03600003197789192], [1436950893.563009, 513185, 0.03563224524259567], [1436951373.620887, 523410, 0.03584449738264084], [1436951853.61941, 533618, 0.03587675839662552], [1436952333.694447, 543828, 0.036698292940855026], [1436952813.621004, 554042, 0.03698749095201492], [1436953293.588156, 564251, 0.03712376952171326], [1436953773.599734, 574464, 0.03729996830224991], [1436954253.621309, 584672, 0.03730553761124611], [1436954733.738119, 594882, 0.037479378283023834], [1436955213.56617, 605091, 0.03754287213087082], [1436955693.585366, 615296, 0.0377657376229763], [1436956173.626395, 625501, 0.038117796182632446], [1436956653.601937, 635705, 0.03822959586977959], [1436957133.665878, 645915, 0.03776161000132561], [1436957613.584762, 656116, 0.03816362842917442], [1436958093.549783, 666331, 0.03853853791952133], [1436958573.646778, 676543, 0.03826189786195755], [1436959053.585655, 686750, 0.0381099209189415], [1436959533.679696, 696961, 0.03844142332673073], [1436960013.633292, 707173, 0.03868117928504944], [1436960493.578778, 717383, 0.0390009842813015], [1436960973.596715, 727598, 0.0383562371134758], [1436961453.625644, 737818, 0.0382055900990963], [1436961933.740339, 748040, 0.03806299716234207], [1436962413.573845, 758252, 0.03807120397686958], [1436962893.610678, 768470, 0.03795558586716652], [1436963373.642878, 778674, 0.038018494844436646], [1436963853.558388, 788877, 0.038447774946689606], [1436964333.658419, 799099, 0.03842216357588768], [1436964813.573319, 809289, 0.03840547427535057], [1436965293.542098, 819484, 0.038492728024721146], [1436965773.545453, 829687, 0.0387515053153038], [1436966253.586517, 839901, 0.03869732841849327], [1436966733.639348, 850120, 0.03907460719347], [1436967213.697288, 860330, 0.0395859070122242], [1436967693.617172, 870539, 0.039280518889427185], [1436968173.593885, 880748, 0.0392826572060585], [1436968653.560836, 890955, 0.03899630531668663], [1436969133.676337, 901164, 0.03888440132141113], [1436969613.506638, 911358, 0.038790252059698105], [1436970093.595964, 921560, 0.03851785138249397], [1436970573.541227, 931756, 0.03913348540663719], [1436971053.624316, 941945, 0.038978900760412216], [1436971533.655543, 952138, 0.03925086557865143], [1436972013.604738, 962349, 0.039124101400375366], [1436972493.613199, 972551, 0.0390220545232296], [1436972973.501155, 982746, 0.039025235921144485], [1436973453.64842, 992945, 0.03877083212137222], [1436973933.689516, 1003147, 0.03902769833803177], [1436974413.577769, 1013350, 0.038719139993190765], [1436974893.542281, 1023545, 0.03872331231832504], [1436975373.638453, 1033759, 0.03927341103553772], [1436975853.524388, 1043955, 0.03930830955505371], [1436976333.625792, 1054148, 0.039153918623924255], [1436976813.610661, 1064342, 0.03932590410113335], [1436977293.601581, 1074539, 0.03922765702009201], [1436977773.575627, 1084733, 0.039390794932842255], [1436978253.564972, 1094914, 0.03935663774609566], [1436978733.673144, 1105109, 0.03939087316393852], [1436979213.540585, 1115293, 0.039371199905872345], [1436979693.699591, 1125483, 0.03982992097735405], [1436980173.613012, 1135670, 0.03941287472844124], [1436980653.575769, 1145862, 0.03933672979474068], [1436981133.719264, 1156045, 0.03919614478945732], [1436981613.563551, 1166236, 0.03906407952308655], [1436982093.553233, 1176436, 0.038837045431137085], [1436982573.577846, 1186636, 0.039009105414152145], [1436983053.605749, 1196837, 0.039010051637887955], [1436983533.684994, 1207025, 0.03891472890973091], [1436984013.561492, 1217233, 0.038610219955444336], [1436984493.629873, 1227437, 0.03866511583328247], [1436984973.606714, 1237643, 0.03865685313940048], [1436985453.690084, 1247835, 0.038945719599723816], [1436985933.711388, 1257951, 0.03925580158829689], [1436986413.598807, 1268125, 0.039332933723926544], [1436986893.631797, 1278290, 0.03918297216296196], [1436987373.596962, 1288473, 0.03883613646030426], [1436987853.555549, 1298650, 0.038776978850364685], [1436988333.722032, 1308841, 0.03888171166181564], [1436988813.55697, 1319018, 0.038825325667858124], [1436989293.756905, 1329221, 0.03864298388361931], [1436989773.665141, 1339417, 0.03865634649991989], [1436990253.768302, 1349610, 0.03898858651518822], [1436990733.708919, 1359759, 0.03906260430812836], [1436991213.663033, 1369914, 0.03911694139242172], [1436991693.730925, 1380074, 0.03875250369310379], [1436992173.751791, 1390224, 0.03882621228694916], [1436992653.758682, 1400383, 0.03877855837345123], [1436993133.835604, 1410542, 0.03870398923754692], [1436993613.674655, 1420684, 0.03887751325964928], [1436994093.747454, 1430832, 0.03915301710367203], [1436994573.768973, 1440986, 0.03938450664281845], [1436995053.666661, 1451174, 0.03919720649719238], [1436995533.83439, 1461345, 0.038862887769937515], [1436996013.556996, 1471495, 0.03901274502277374], [1436996493.635477, 1481663, 0.0388539656996727], [1436996973.668684, 1491822, 0.038732752203941345], [1436997453.59326, 1501979, 0.03879735246300697], [1436997933.774019, 1512139, 0.038524042814970016], [1436998413.575162, 1522290, 0.03869651257991791], [1436998893.640468, 1532431, 0.0383637398481369], [1436999373.551661, 1542579, 0.038300249725580215], [1436999853.57906, 1552734, 0.03799160569906235], [1437000333.680409, 1562888, 0.03759683296084404], [1437000813.602383, 1573037, 0.037678662687540054], [1437001293.610337, 1583190, 0.037575822323560715], [1437001773.618199, 1593341, 0.0376887246966362], [1437002253.572966, 1603497, 0.037922415882349014], [1437002733.67994, 1613657, 0.03766244649887085], [1437003213.583266, 1623809, 0.03754705190658569], [1437003693.639943, 1633966, 0.03738937899470329], [1437004173.568287, 1644113, 0.037347543984651566], [1437004653.610772, 1654268, 0.037374842911958694], [1437005133.663045, 1664424, 0.037443988025188446], [1437005613.580984, 1674567, 0.037457264959812164], [1437006093.601019, 1684715, 0.037874478846788406], [1437006573.625314, 1694857, 0.037644676864147186], [1437007053.584514, 1704999, 0.03743988648056984], [1437007533.719303, 1715150, 0.03739031031727791], [1437008013.604962, 1725282, 0.037301771342754364], [1437008493.655091, 1735432, 0.03735104575753212], [1437008973.640165, 1745584, 0.037282250821590424], [1437009453.715067, 1755742, 0.03729768097400665], [1437009933.765712, 1765896, 0.03717759624123573], [1437010413.632128, 1776052, 0.03691410645842552], [1437010893.66766, 1786195, 0.036807890981435776], [1437011373.636164, 1796346, 0.036659423261880875], [1437011853.631224, 1806481, 0.03682238608598709], [1437012333.706205, 1816617, 0.036776404827833176], [1437012813.61987, 1826754, 0.036672260612249374], [1437013293.479904, 1836883, 0.03666841238737106], [1437013773.604574, 1847029, 0.036642514169216156], [1437014253.618884, 1857175, 0.03654393553733826], [1437014733.756419, 1867312, 0.03638240322470665], [1437015213.638607, 1877459, 0.03610989451408386], [1437015693.625763, 1887608, 0.036011870950460434], [1437016173.63194, 1897759, 0.03607400134205818], [1437016653.609074, 1907909, 0.03581620752811432], [1437017133.717601, 1918074, 0.035680998116731644], [1437017613.716011, 1928220, 0.03547567501664162], [1437018093.626005, 1938377, 0.035375215113162994], [1437018573.626522, 1948523, 0.03534447029232979], [1437019053.648174, 1958678, 0.03535373508930206], [1437019533.803011, 1968831, 0.03541970252990723], [1437020013.667751, 1978978, 0.03534942492842674], [1437020493.659028, 1989133, 0.035337116569280624], [1437020973.657346, 1999287, 0.03519223630428314], [1437021453.650634, 2009437, 0.0350094810128212], [1437021933.848661, 2019588, 0.03481736779212952], [1437022413.674963, 2029736, 0.03482922539114952], [1437022893.69086, 2039894, 0.03482965752482414], [1437023373.68883, 2050054, 0.034710027277469635], [1437023853.686116, 2060205, 0.03447446599602699], [1437024333.763876, 2070362, 0.034356746822595596], [1437024813.707845, 2080507, 0.03430519998073578], [1437025293.483294, 2090645, 0.03412580490112305], [1437025773.695712, 2100793, 0.03409077599644661], [1437026253.672994, 2110943, 0.0340830534696579], [1437026733.780775, 2121094, 0.03400549292564392], [1437027213.617849, 2131235, 0.033846043050289154], [1437027693.694451, 2141382, 0.03379584103822708], [1437028173.68596, 2151537, 0.033618565648794174], [1437028653.584833, 2161685, 0.03352222591638565], [1437029133.792483, 2171839, 0.03338197246193886], [1437029613.661672, 2181977, 0.03323192894458771], [1437030093.641009, 2192118, 0.03313163295388222], [1437030573.656274, 2202268, 0.0331595316529274], [1437031053.643631, 2212416, 0.03310840204358101], [1437031533.777478, 2222583, 0.03298124670982361], [1437032013.704008, 2232736, 0.03288085386157036], [1437032493.638393, 2242882, 0.03281677886843681], [1437032973.684986, 2253041, 0.03261971473693848], [1437033453.699562, 2263183, 0.03251069411635399], [1437033933.918074, 2273320, 0.03243493288755417], [1437034413.596351, 2283443, 0.03251812607049942], [1437034893.640496, 2293579, 0.03244208171963692], [1437035373.637761, 2303701, 0.03246922418475151], [1437035853.669947, 2313823, 0.032652080059051514], [1437036333.78905, 2323961, 0.032621122896671295], [1437036813.699727, 2334089, 0.03248974680900574], [1437037293.662592, 2344235, 0.032404426485300064], [1437037773.66716, 2354364, 0.03240393102169037], [1437038253.603687, 2364507, 0.03238365799188614], [1437038733.78864, 2374644, 0.03244389593601227], [1437039213.641799, 2384782, 0.03239350765943527], [1437039693.687078, 2394923, 0.032426562160253525], [1437040173.635717, 2405058, 0.032403264194726944], [1437040653.673331, 2415194, 0.03231978043913841], [1437041133.764768, 2425322, 0.03223187103867531], [1437041613.629279, 2435449, 0.03213196247816086], [1437042093.703985, 2445575, 0.032153598964214325], [1437042573.496029, 2455712, 0.03199320286512375], [1437043053.686022, 2465844, 0.03188605234026909], [1437043533.731929, 2475974, 0.03178738057613373], [1437044013.636245, 2486095, 0.03171614184975624], [1437044493.69923, 2496238, 0.031645938754081726], [1437044973.652155, 2506373, 0.03155189007520676], [1437045453.691467, 2516497, 0.03144536912441254], [1437045933.935804, 2526637, 0.031432293355464935], [1437046413.635583, 2536770, 0.03129834309220314], [1437046893.626337, 2546896, 0.031195342540740967], [1437047373.67437, 2557029, 0.031033318489789963], [1437047853.652939, 2567169, 0.030938012525439262], [1437048333.778436, 2577306, 0.030827201902866364], [1437048813.654248, 2587433, 0.03068169392645359], [1437049293.610609, 2597552, 0.030520914122462273], [1437049773.646573, 2607690, 0.030437452718615532], [1437050253.667925, 2617808, 0.03041636385023594], [1437050733.735291, 2627933, 0.030291059985756874], [1437051213.620222, 2638053, 0.030283397063612938], [1437051693.601978, 2648171, 0.030193043872714043], [1437052173.634985, 2658299, 0.03004123829305172], [1437052653.687176, 2668425, 0.0299222432076931], [1437053133.762819, 2678556, 0.029762346297502518], [1437053613.643698, 2688671, 0.02970775216817856], [1437054093.673047, 2698804, 0.029604140669107437], [1437054573.667371, 2708956, 0.02949359640479088], [1437055053.650441, 2719087, 0.02943229116499424], [1437055533.778469, 2729219, 0.029304414987564087], [1437056013.694082, 2739343, 0.029147598892450333], [1437056493.674871, 2749458, 0.029033908620476723], [1437056973.700234, 2759575, 0.028886595740914345], [1437057453.666129, 2769697, 0.028734514489769936], [1437057933.848506, 2779821, 0.02874554693698883], [1437058413.643799, 2789941, 0.028716085478663445], [1437058893.715386, 2800076, 0.028669510036706924], [1437059373.62596, 2810207, 0.028530430048704147], [1437059853.650848, 2820334, 0.02839958481490612], [1437060333.792248, 2830465, 0.028364405035972595], [1437060813.682955, 2840600, 0.0282796248793602], [1437061293.681795, 2850745, 0.02820495329797268], [1437061773.691182, 2860880, 0.028159918263554573], [1437062253.662987, 2871013, 0.028104742988944054], [1437062733.760419, 2881153, 0.028099438175559044], [1437063213.651969, 2891278, 0.02802356891334057], [1437063693.723523, 2901406, 0.027945902198553085], [1437064173.68663, 2911533, 0.027897505089640617], [1437064653.547643, 2921667, 0.027821676805615425], [1437065133.62645, 2931813, 0.02770490199327469], [1437065613.566569, 2941947, 0.02761264331638813], [1437066093.537804, 2952102, 0.027557073161005974], [1437066573.529332, 2962243, 0.027522796764969826], [1437067053.520098, 2972400, 0.027469975873827934], [1437067533.605733, 2982561, 0.027299631386995316], [1437068013.535467, 2992698, 0.027225365862250328], [1437068493.559976, 3002839, 0.027095869183540344], [1437068973.558743, 3012983, 0.027036350220441818], [1437069453.562661, 3023116, 0.02693818509578705], [1437069933.627071, 3033256, 0.02687198854982853], [1437070413.574131, 3043386, 0.02687297947704792], [1437070893.658803, 3053528, 0.026770537719130516], [1437071373.638711, 3063659, 0.026667704805731773], [1437071853.621384, 3073794, 0.026571234688162804], [1437072333.665269, 3083926, 0.026447603479027748], [1437072813.584388, 3094040, 0.026389220729470253], [1437073293.569178, 3104172, 0.026299258694052696]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d3.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d3.json new file mode 100644 index 0000000000..e489130ea7 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d3.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 0.03425809368491173], [1436926413.945391, 1476, 0.032557398080825806], [1436926893.945037, 6006, 0.0277252234518528], [1436927373.995472, 13786, 0.021282576024532318], [1436927853.989794, 23650, 0.015578101389110088], [1436928334.132361, 33755, 0.011687012389302254], [1436928813.973288, 43941, 0.00918175745755434], [1436929293.975949, 54146, 0.00784988235682249], [1436929773.992781, 64316, 0.007188988849520683], [1436930253.997415, 74465, 0.0072308750823140144], [1436930734.203004, 84611, 0.007685060612857342], [1436931214.03644, 94700, 0.008267422206699848], [1436931694.094564, 104766, 0.008946981281042099], [1436932174.114955, 114817, 0.009664506651461124], [1436932654.161382, 124880, 0.010994983837008476], [1436933133.960214, 134977, 0.011961394920945168], [1436933614.044337, 145062, 0.012674711644649506], [1436934094.166206, 155169, 0.013640021905303001], [1436934574.106036, 165284, 0.014305224642157555], [1436935054.150647, 175402, 0.014946703799068928], [1436935533.819562, 185538, 0.015737954527139664], [1436936013.710422, 195712, 0.016330912709236145], [1436936493.609025, 205906, 0.016695979982614517], [1436936973.683892, 216099, 0.017458846792578697], [1436937454.138383, 226331, 0.018533164635300636], [1436937933.838475, 236532, 0.01949200965464115], [1436938413.89688, 246724, 0.019517479464411736], [1436938894.018652, 256925, 0.019764307886362076], [1436939373.69067, 267137, 0.02008572220802307], [1436939853.673692, 277369, 0.021091068163514137], [1436940333.651346, 287620, 0.02140945754945278], [1436940813.599579, 297848, 0.021988170221447945], [1436941293.596313, 308088, 0.0223606675863266], [1436941773.659172, 318362, 0.022547796368598938], [1436942253.648479, 328621, 0.023031413555145264], [1436942733.752284, 338892, 0.023786410689353943], [1436943213.621881, 349144, 0.024008480831980705], [1436943693.698743, 359399, 0.024148935452103615], [1436944173.578463, 369649, 0.02457556128501892], [1436944653.692217, 379912, 0.02469060942530632], [1436945133.677298, 390180, 0.024952523410320282], [1436945613.572411, 400445, 0.02554873190820217], [1436946093.56123, 410703, 0.025771528482437134], [1436946573.542364, 420958, 0.02602078951895237], [1436947053.616578, 431216, 0.02602880820631981], [1436947533.636973, 441483, 0.026351822540163994], [1436948013.541574, 451751, 0.0262188371270895], [1436948493.560223, 462015, 0.026711203157901764], [1436948973.512541, 472260, 0.027218565344810486], [1436949453.550055, 482483, 0.02734719216823578], [1436949933.828011, 492731, 0.027217986062169075], [1436950413.603177, 502957, 0.027318857610225677], [1436950893.563009, 513185, 0.027305351570248604], [1436951373.620887, 523410, 0.027760380879044533], [1436951853.61941, 533618, 0.0280567966401577], [1436952333.694447, 543828, 0.028621215373277664], [1436952813.621004, 554042, 0.028958816081285477], [1436953293.588156, 564251, 0.029186993837356567], [1436953773.599734, 574464, 0.028960207477211952], [1436954253.621309, 584672, 0.028913332149386406], [1436954733.738119, 594882, 0.02921229600906372], [1436955213.56617, 605091, 0.029444556683301926], [1436955693.585366, 615296, 0.029747728258371353], [1436956173.626395, 625501, 0.030260732397437096], [1436956653.601937, 635705, 0.030394721776247025], [1436957133.665878, 645915, 0.03041674755513668], [1436957613.584762, 656116, 0.03058660589158535], [1436958093.549783, 666331, 0.030284838750958443], [1436958573.646778, 676543, 0.030354052782058716], [1436959053.585655, 686750, 0.030551131814718246], [1436959533.679696, 696961, 0.030686482787132263], [1436960013.633292, 707173, 0.030921922996640205], [1436960493.578778, 717383, 0.031079748645424843], [1436960973.596715, 727598, 0.03077232837677002], [1436961453.625644, 737818, 0.03084420971572399], [1436961933.740339, 748040, 0.03110562451183796], [1436962413.573845, 758252, 0.031141508370637894], [1436962893.610678, 768470, 0.031010067090392113], [1436963373.642878, 778674, 0.031100917607545853], [1436963853.558388, 788877, 0.03134296461939812], [1436964333.658419, 799099, 0.031301673501729965], [1436964813.573319, 809289, 0.031290579587221146], [1436965293.542098, 819484, 0.031435515731573105], [1436965773.545453, 829687, 0.031667787581682205], [1436966253.586517, 839901, 0.03169453889131546], [1436966733.639348, 850120, 0.03191617131233215], [1436967213.697288, 860330, 0.03205711767077446], [1436967693.617172, 870539, 0.03206227719783783], [1436968173.593885, 880748, 0.03195691108703613], [1436968653.560836, 890955, 0.03166574612259865], [1436969133.676337, 901164, 0.031929291784763336], [1436969613.506638, 911358, 0.031744007021188736], [1436970093.595964, 921560, 0.0315803587436676], [1436970573.541227, 931756, 0.031766779720783234], [1436971053.624316, 941945, 0.03181062266230583], [1436971533.655543, 952138, 0.0319465771317482], [1436972013.604738, 962349, 0.032054755836725235], [1436972493.613199, 972551, 0.03192495182156563], [1436972973.501155, 982746, 0.0319976881146431], [1436973453.64842, 992945, 0.03205036744475365], [1436973933.689516, 1003147, 0.032020118087530136], [1436974413.577769, 1013350, 0.03207429125905037], [1436974893.542281, 1023545, 0.032214779406785965], [1436975373.638453, 1033759, 0.03219134360551834], [1436975853.524388, 1043955, 0.0324082113802433], [1436976333.625792, 1054148, 0.03221917897462845], [1436976813.610661, 1064342, 0.03200480341911316], [1436977293.601581, 1074539, 0.03198748826980591], [1436977773.575627, 1084733, 0.032064300030469894], [1436978253.564972, 1094914, 0.032298240810632706], [1436978733.673144, 1105109, 0.03248215466737747], [1436979213.540585, 1115293, 0.03262820467352867], [1436979693.699591, 1125483, 0.032745134085416794], [1436980173.613012, 1135670, 0.032681502401828766], [1436980653.575769, 1145862, 0.03240214288234711], [1436981133.719264, 1156045, 0.03237201273441315], [1436981613.563551, 1166236, 0.03202598914504051], [1436982093.553233, 1176436, 0.032163310796022415], [1436982573.577846, 1186636, 0.03232435882091522], [1436983053.605749, 1196837, 0.032410554587841034], [1436983533.684994, 1207025, 0.03245232254266739], [1436984013.561492, 1217233, 0.03224659338593483], [1436984493.629873, 1227437, 0.03204221650958061], [1436984973.606714, 1237643, 0.03186390548944473], [1436985453.690084, 1247835, 0.031911786645650864], [1436985933.711388, 1257951, 0.032286882400512695], [1436986413.598807, 1268125, 0.032266560941934586], [1436986893.631797, 1278290, 0.03252791985869408], [1436987373.596962, 1288473, 0.03241678699851036], [1436987853.555549, 1298650, 0.03210347890853882], [1436988333.722032, 1308841, 0.031904902309179306], [1436988813.55697, 1319018, 0.03179018944501877], [1436989293.756905, 1329221, 0.0316874124109745], [1436989773.665141, 1339417, 0.03160090371966362], [1436990253.768302, 1349610, 0.03161816671490669], [1436990733.708919, 1359759, 0.0317724235355854], [1436991213.663033, 1369914, 0.03175821527838707], [1436991693.730925, 1380074, 0.031629402190446854], [1436992173.751791, 1390224, 0.031547073274850845], [1436992653.758682, 1400383, 0.031528495252132416], [1436993133.835604, 1410542, 0.03169562667608261], [1436993613.674655, 1420684, 0.031826674938201904], [1436994093.747454, 1430832, 0.03185039013624191], [1436994573.768973, 1440986, 0.03199826925992966], [1436995053.666661, 1451174, 0.03156091645359993], [1436995533.83439, 1461345, 0.031506411731243134], [1436996013.556996, 1471495, 0.031495608389377594], [1436996493.635477, 1481663, 0.03134337440133095], [1436996973.668684, 1491822, 0.031145554035902023], [1436997453.59326, 1501979, 0.031068041920661926], [1436997933.774019, 1512139, 0.031432390213012695], [1436998413.575162, 1522290, 0.03142932057380676], [1436998893.640468, 1532431, 0.03132513165473938], [1436999373.551661, 1542579, 0.03125539794564247], [1436999853.57906, 1552734, 0.0309873279184103], [1437000333.680409, 1562888, 0.03088490664958954], [1437000813.602383, 1573037, 0.0308260228484869], [1437001293.610337, 1583190, 0.030793415382504463], [1437001773.618199, 1593341, 0.03087344579398632], [1437002253.572966, 1603497, 0.0308389812707901], [1437002733.67994, 1613657, 0.03070608340203762], [1437003213.583266, 1623809, 0.0307186096906662], [1437003693.639943, 1633966, 0.03048117645084858], [1437004173.568287, 1644113, 0.03030446544289589], [1437004653.610772, 1654268, 0.030324051156640053], [1437005133.663045, 1664424, 0.03043009154498577], [1437005613.580984, 1674567, 0.030361991375684738], [1437006093.601019, 1684715, 0.030566193163394928], [1437006573.625314, 1694857, 0.030430208891630173], [1437007053.584514, 1704999, 0.030224468559026718], [1437007533.719303, 1715150, 0.030241932719945908], [1437008013.604962, 1725282, 0.030097855255007744], [1437008493.655091, 1735432, 0.030217904597520828], [1437008973.640165, 1745584, 0.030181601643562317], [1437009453.715067, 1755742, 0.030172593891620636], [1437009933.765712, 1765896, 0.030141659080982208], [1437010413.632128, 1776052, 0.030052196234464645], [1437010893.66766, 1786195, 0.03007938154041767], [1437011373.636164, 1796346, 0.02996920794248581], [1437011853.631224, 1806481, 0.029995175078511238], [1437012333.706205, 1816617, 0.03010040894150734], [1437012813.61987, 1826754, 0.030088385567069054], [1437013293.479904, 1836883, 0.029996229335665703], [1437013773.604574, 1847029, 0.029950618743896484], [1437014253.618884, 1857175, 0.029801754280924797], [1437014733.756419, 1867312, 0.029606210067868233], [1437015213.638607, 1877459, 0.029520301148295403], [1437015693.625763, 1887608, 0.02937021106481552], [1437016173.63194, 1897759, 0.02928493171930313], [1437016653.609074, 1907909, 0.029194936156272888], [1437017133.717601, 1918074, 0.029153617098927498], [1437017613.716011, 1928220, 0.029063349589705467], [1437018093.626005, 1938377, 0.02899051643908024], [1437018573.626522, 1948523, 0.02908063493669033], [1437019053.648174, 1958678, 0.029026903212070465], [1437019533.803011, 1968831, 0.029071694239974022], [1437020013.667751, 1978978, 0.029110101982951164], [1437020493.659028, 1989133, 0.02908976934850216], [1437020973.657346, 1999287, 0.028982611373066902], [1437021453.650634, 2009437, 0.028793690726161003], [1437021933.848661, 2019588, 0.02868787571787834], [1437022413.674963, 2029736, 0.028585631400346756], [1437022893.69086, 2039894, 0.02848806604743004], [1437023373.68883, 2050054, 0.028294002637267113], [1437023853.686116, 2060205, 0.02822807803750038], [1437024333.763876, 2070362, 0.02809525839984417], [1437024813.707845, 2080507, 0.027941878885030746], [1437025293.483294, 2090645, 0.02787884697318077], [1437025773.695712, 2100793, 0.027862509712576866], [1437026253.672994, 2110943, 0.027835993096232414], [1437026733.780775, 2121094, 0.027756690979003906], [1437027213.617849, 2131235, 0.027644263580441475], [1437027693.694451, 2141382, 0.02752007730305195], [1437028173.68596, 2151537, 0.027432529255747795], [1437028653.584833, 2161685, 0.027434471994638443], [1437029133.792483, 2171839, 0.027317894622683525], [1437029613.661672, 2181977, 0.027138294652104378], [1437030093.641009, 2192118, 0.027088705450296402], [1437030573.656274, 2202268, 0.027131302282214165], [1437031053.643631, 2212416, 0.02715957537293434], [1437031533.777478, 2222583, 0.027089620009064674], [1437032013.704008, 2232736, 0.026989320293068886], [1437032493.638393, 2242882, 0.026922713965177536], [1437032973.684986, 2253041, 0.02678370475769043], [1437033453.699562, 2263183, 0.0267350971698761], [1437033933.918074, 2273320, 0.026652036234736443], [1437034413.596351, 2283443, 0.0265977680683136], [1437034893.640496, 2293579, 0.02654072269797325], [1437035373.637761, 2303701, 0.026471523568034172], [1437035853.669947, 2313823, 0.026451298967003822], [1437036333.78905, 2323961, 0.026429779827594757], [1437036813.699727, 2334089, 0.026324886828660965], [1437037293.662592, 2344235, 0.026287589222192764], [1437037773.66716, 2354364, 0.026264755055308342], [1437038253.603687, 2364507, 0.026225194334983826], [1437038733.78864, 2374644, 0.02624845691025257], [1437039213.641799, 2384782, 0.02624380588531494], [1437039693.687078, 2394923, 0.026255516335368156], [1437040173.635717, 2405058, 0.026186630129814148], [1437040653.673331, 2415194, 0.026059549301862717], [1437041133.764768, 2425322, 0.02603207901120186], [1437041613.629279, 2435449, 0.025951188057661057], [1437042093.703985, 2445575, 0.025885486975312233], [1437042573.496029, 2455712, 0.0258584376424551], [1437043053.686022, 2465844, 0.02576967515051365], [1437043533.731929, 2475974, 0.02574247308075428], [1437044013.636245, 2486095, 0.025741368532180786], [1437044493.69923, 2496238, 0.025613142177462578], [1437044973.652155, 2506373, 0.025502001866698265], [1437045453.691467, 2516497, 0.025422129780054092], [1437045933.935804, 2526637, 0.02530006691813469], [1437046413.635583, 2536770, 0.02533203549683094], [1437046893.626337, 2546896, 0.025261884555220604], [1437047373.67437, 2557029, 0.02517615258693695], [1437047853.652939, 2567169, 0.025054262951016426], [1437048333.778436, 2577306, 0.024978358298540115], [1437048813.654248, 2587433, 0.024952327832579613], [1437049293.610609, 2597552, 0.024846646934747696], [1437049773.646573, 2607690, 0.024763893336057663], [1437050253.667925, 2617808, 0.024688972160220146], [1437050733.735291, 2627933, 0.024599123746156693], [1437051213.620222, 2638053, 0.024585271254181862], [1437051693.601978, 2648171, 0.024474715813994408], [1437052173.634985, 2658299, 0.0243435837328434], [1437052653.687176, 2668425, 0.024294523522257805], [1437053133.762819, 2678556, 0.024163981899619102], [1437053613.643698, 2688671, 0.024034887552261353], [1437054093.673047, 2698804, 0.024000374600291252], [1437054573.667371, 2708956, 0.023914175108075142], [1437055053.650441, 2719087, 0.02395522966980934], [1437055533.778469, 2729219, 0.023859599605202675], [1437056013.694082, 2739343, 0.02375946193933487], [1437056493.674871, 2749458, 0.023677179589867592], [1437056973.700234, 2759575, 0.023645443841814995], [1437057453.666129, 2769697, 0.02356558106839657], [1437057933.848506, 2779821, 0.023514214903116226], [1437058413.643799, 2789941, 0.023489613085985184], [1437058893.715386, 2800076, 0.023429814726114273], [1437059373.62596, 2810207, 0.023343827575445175], [1437059853.650848, 2820334, 0.02322673238813877], [1437060333.792248, 2830465, 0.023134106770157814], [1437060813.682955, 2840600, 0.023055672645568848], [1437061293.681795, 2850745, 0.022985080257058144], [1437061773.691182, 2860880, 0.02291373908519745], [1437062253.662987, 2871013, 0.022864071652293205], [1437062733.760419, 2881153, 0.0227896086871624], [1437063213.651969, 2891278, 0.02276325598359108], [1437063693.723523, 2901406, 0.022676151245832443], [1437064173.68663, 2911533, 0.022622840479016304], [1437064653.547643, 2921667, 0.022551873698830605], [1437065133.62645, 2931813, 0.022431621327996254], [1437065613.566569, 2941947, 0.022368427366018295], [1437066093.537804, 2952102, 0.022323856130242348], [1437066573.529332, 2962243, 0.022268367931246758], [1437067053.520098, 2972400, 0.022210223600268364], [1437067533.605733, 2982561, 0.022118542343378067], [1437068013.535467, 2992698, 0.022013003006577492], [1437068493.559976, 3002839, 0.021971898153424263], [1437068973.558743, 3012983, 0.021911533549427986], [1437069453.562661, 3023116, 0.021851375699043274], [1437069933.627071, 3033256, 0.021762363612651825], [1437070413.574131, 3043386, 0.021733952686190605], [1437070893.658803, 3053528, 0.021669508889317513], [1437071373.638711, 3063659, 0.021594204008579254], [1437071853.621384, 3073794, 0.021531015634536743], [1437072333.665269, 3083926, 0.021499203518033028], [1437072813.584388, 3094040, 0.021456807851791382], [1437073293.569178, 3104172, 0.02136526256799698]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d4.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d4.json new file mode 100644 index 0000000000..434b78cd0f --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/beta/d4.json @@ -0,0 +1 @@ +[[1436925978.257845, 7, 0.5028539896011353], [1436926413.945391, 1476, 0.4976981580257416], [1436926893.945037, 6006, 0.5092837810516357], [1436927373.995472, 13786, 0.5118998885154724], [1436927853.989794, 23650, 0.5314905643463135], [1436928334.132361, 33755, 0.550969123840332], [1436928813.973288, 43941, 0.5487659573554993], [1436929293.975949, 54146, 0.5263530015945435], [1436929773.992781, 64316, 0.5077286958694458], [1436930253.997415, 74465, 0.5120566487312317], [1436930734.203004, 84611, 0.5140185952186584], [1436931214.03644, 94700, 0.5133042335510254], [1436931694.094564, 104766, 0.5233010053634644], [1436932174.114955, 114817, 0.5230671763420105], [1436932654.161382, 124880, 0.5250263810157776], [1436933133.960214, 134977, 0.5088120698928833], [1436933614.044337, 145062, 0.5097426176071167], [1436934094.166206, 155169, 0.5103482007980347], [1436934574.106036, 165284, 0.5021579265594482], [1436935054.150647, 175402, 0.49785494804382324], [1436935533.819562, 185538, 0.4970649182796478], [1436936013.710422, 195712, 0.5023221373558044], [1436936493.609025, 205906, 0.5063169002532959], [1436936973.683892, 216099, 0.50455641746521], [1436937454.138383, 226331, 0.5104150772094727], [1436937933.838475, 236532, 0.5066487193107605], [1436938413.89688, 246724, 0.5183079838752747], [1436938894.018652, 256925, 0.5163102746009827], [1436939373.69067, 267137, 0.5216323733329773], [1436939853.673692, 277369, 0.5153006315231323], [1436940333.651346, 287620, 0.5240126252174377], [1436940813.599579, 297848, 0.5263218879699707], [1436941293.596313, 308088, 0.5236956477165222], [1436941773.659172, 318362, 0.534295916557312], [1436942253.648479, 328621, 0.540306031703949], [1436942733.752284, 338892, 0.5359382033348083], [1436943213.621881, 349144, 0.540198564529419], [1436943693.698743, 359399, 0.5404431819915771], [1436944173.578463, 369649, 0.5429667234420776], [1436944653.692217, 379912, 0.5415231585502625], [1436945133.677298, 390180, 0.54068922996521], [1436945613.572411, 400445, 0.5396349430084229], [1436946093.56123, 410703, 0.5486253499984741], [1436946573.542364, 420958, 0.5451043248176575], [1436947053.616578, 431216, 0.5478819608688354], [1436947533.636973, 441483, 0.5503379106521606], [1436948013.541574, 451751, 0.5534676313400269], [1436948493.560223, 462015, 0.5574610829353333], [1436948973.512541, 472260, 0.5558810234069824], [1436949453.550055, 482483, 0.5529404878616333], [1436949933.828011, 492731, 0.5618430972099304], [1436950413.603177, 502957, 0.5641138553619385], [1436950893.563009, 513185, 0.5707159638404846], [1436951373.620887, 523410, 0.5676558613777161], [1436951853.61941, 533618, 0.5637813806533813], [1436952333.694447, 543828, 0.5682924389839172], [1436952813.621004, 554042, 0.5690237283706665], [1436953293.588156, 564251, 0.5655006766319275], [1436953773.599734, 574464, 0.553955614566803], [1436954253.621309, 584672, 0.5558924674987793], [1436954733.738119, 594882, 0.5603042840957642], [1436955213.56617, 605091, 0.5625290870666504], [1436955693.585366, 615296, 0.5668522715568542], [1436956173.626395, 625501, 0.5736584663391113], [1436956653.601937, 635705, 0.5693879723548889], [1436957133.665878, 645915, 0.576599657535553], [1436957613.584762, 656116, 0.5648065805435181], [1436958093.549783, 666331, 0.5632508397102356], [1436958573.646778, 676543, 0.5660487413406372], [1436959053.585655, 686750, 0.568809449672699], [1436959533.679696, 696961, 0.5667826533317566], [1436960013.633292, 707173, 0.5637232661247253], [1436960493.578778, 717383, 0.5675314664840698], [1436960973.596715, 727598, 0.5714674592018127], [1436961453.625644, 737818, 0.564845085144043], [1436961933.740339, 748040, 0.5700833797454834], [1436962413.573845, 758252, 0.5702976584434509], [1436962893.610678, 768470, 0.5745863914489746], [1436963373.642878, 778674, 0.5763651728630066], [1436963853.558388, 788877, 0.5721960067749023], [1436964333.658419, 799099, 0.5714120864868164], [1436964813.573319, 809289, 0.5687000155448914], [1436965293.542098, 819484, 0.5728974938392639], [1436965773.545453, 829687, 0.5738612413406372], [1436966253.586517, 839901, 0.5702064037322998], [1436966733.639348, 850120, 0.5715107321739197], [1436967213.697288, 860330, 0.5695001482963562], [1436967693.617172, 870539, 0.5783872008323669], [1436968173.593885, 880748, 0.5758792161941528], [1436968653.560836, 890955, 0.572809636592865], [1436969133.676337, 901164, 0.5752230286598206], [1436969613.506638, 911358, 0.5861247181892395], [1436970093.595964, 921560, 0.5834078788757324], [1436970573.541227, 931756, 0.5814791321754456], [1436971053.624316, 941945, 0.5803619623184204], [1436971533.655543, 952138, 0.5765199065208435], [1436972013.604738, 962349, 0.5693190693855286], [1436972493.613199, 972551, 0.5720453262329102], [1436972973.501155, 982746, 0.5741620063781738], [1436973453.64842, 992945, 0.5705713629722595], [1436973933.689516, 1003147, 0.5657351613044739], [1436974413.577769, 1013350, 0.5685256123542786], [1436974893.542281, 1023545, 0.5698860287666321], [1436975373.638453, 1033759, 0.5801734328269958], [1436975853.524388, 1043955, 0.577880322933197], [1436976333.625792, 1054148, 0.5780594348907471], [1436976813.610661, 1064342, 0.5804633498191833], [1436977293.601581, 1074539, 0.5842364430427551], [1436977773.575627, 1084733, 0.5745837092399597], [1436978253.564972, 1094914, 0.5848771333694458], [1436978733.673144, 1105109, 0.5795935392379761], [1436979213.540585, 1115293, 0.583346426486969], [1436979693.699591, 1125483, 0.5840965509414673], [1436980173.613012, 1135670, 0.5807850360870361], [1436980653.575769, 1145862, 0.5843925476074219], [1436981133.719264, 1156045, 0.5828814506530762], [1436981613.563551, 1166236, 0.5873864889144897], [1436982093.553233, 1176436, 0.5896572470664978], [1436982573.577846, 1186636, 0.5887367725372314], [1436983053.605749, 1196837, 0.5841871500015259], [1436983533.684994, 1207025, 0.5867579579353333], [1436984013.561492, 1217233, 0.5940297842025757], [1436984493.629873, 1227437, 0.5925037860870361], [1436984973.606714, 1237643, 0.5981529951095581], [1436985453.690084, 1247835, 0.5954598188400269], [1436985933.711388, 1257951, 0.5903756022453308], [1436986413.598807, 1268125, 0.5837404131889343], [1436986893.631797, 1278290, 0.583182156085968], [1436987373.596962, 1288473, 0.5860618352890015], [1436987853.555549, 1298650, 0.5829544067382812], [1436988333.722032, 1308841, 0.5798720121383667], [1436988813.55697, 1319018, 0.589148998260498], [1436989293.756905, 1329221, 0.5905702710151672], [1436989773.665141, 1339417, 0.5900465250015259], [1436990253.768302, 1349610, 0.5893078446388245], [1436990733.708919, 1359759, 0.589722752571106], [1436991213.663033, 1369914, 0.5907371640205383], [1436991693.730925, 1380074, 0.5939858555793762], [1436992173.751791, 1390224, 0.5906378626823425], [1436992653.758682, 1400383, 0.5876493453979492], [1436993133.835604, 1410542, 0.5912420153617859], [1436993613.674655, 1420684, 0.5887293219566345], [1436994093.747454, 1430832, 0.589107096195221], [1436994573.768973, 1440986, 0.5928497910499573], [1436995053.666661, 1451174, 0.5916265845298767], [1436995533.83439, 1461345, 0.5911784768104553], [1436996013.556996, 1471495, 0.5890726447105408], [1436996493.635477, 1481663, 0.5914839506149292], [1436996973.668684, 1491822, 0.5915400385856628], [1436997453.59326, 1501979, 0.591564416885376], [1436997933.774019, 1512139, 0.5926578640937805], [1436998413.575162, 1522290, 0.5942149758338928], [1436998893.640468, 1532431, 0.5931802988052368], [1436999373.551661, 1542579, 0.587592601776123], [1436999853.57906, 1552734, 0.5877953171730042], [1437000333.680409, 1562888, 0.590681791305542], [1437000813.602383, 1573037, 0.5924896001815796], [1437001293.610337, 1583190, 0.5913501381874084], [1437001773.618199, 1593341, 0.5952408909797668], [1437002253.572966, 1603497, 0.5953922271728516], [1437002733.67994, 1613657, 0.6002237200737], [1437003213.583266, 1623809, 0.6042569875717163], [1437003693.639943, 1633966, 0.6017740368843079], [1437004173.568287, 1644113, 0.6037994623184204], [1437004653.610772, 1654268, 0.6037947535514832], [1437005133.663045, 1664424, 0.6028310060501099], [1437005613.580984, 1674567, 0.603211522102356], [1437006093.601019, 1684715, 0.6052727699279785], [1437006573.625314, 1694857, 0.6032628417015076], [1437007053.584514, 1704999, 0.5978461503982544], [1437007533.719303, 1715150, 0.602828323841095], [1437008013.604962, 1725282, 0.6063790917396545], [1437008493.655091, 1735432, 0.6047347784042358], [1437008973.640165, 1745584, 0.6031648516654968], [1437009453.715067, 1755742, 0.6067507863044739], [1437009933.765712, 1765896, 0.6062817573547363], [1437010413.632128, 1776052, 0.609245240688324], [1437010893.66766, 1786195, 0.6066284775733948], [1437011373.636164, 1796346, 0.6102170944213867], [1437011853.631224, 1806481, 0.609173595905304], [1437012333.706205, 1816617, 0.6035751104354858], [1437012813.61987, 1826754, 0.604059636592865], [1437013293.479904, 1836883, 0.6039224863052368], [1437013773.604574, 1847029, 0.5974730849266052], [1437014253.618884, 1857175, 0.6040806174278259], [1437014733.756419, 1867312, 0.6017186045646667], [1437015213.638607, 1877459, 0.5987159609794617], [1437015693.625763, 1887608, 0.6047909259796143], [1437016173.63194, 1897759, 0.6033824682235718], [1437016653.609074, 1907909, 0.6038352847099304], [1437017133.717601, 1918074, 0.6083348989486694], [1437017613.716011, 1928220, 0.6044996380805969], [1437018093.626005, 1938377, 0.6009799242019653], [1437018573.626522, 1948523, 0.60047847032547], [1437019053.648174, 1958678, 0.6019382476806641], [1437019533.803011, 1968831, 0.6007305383682251], [1437020013.667751, 1978978, 0.6025127172470093], [1437020493.659028, 1989133, 0.6051828861236572], [1437020973.657346, 1999287, 0.6085876822471619], [1437021453.650634, 2009437, 0.6065122485160828], [1437021933.848661, 2019588, 0.6084572076797485], [1437022413.674963, 2029736, 0.6065473556518555], [1437022893.69086, 2039894, 0.6075063347816467], [1437023373.68883, 2050054, 0.6095973253250122], [1437023853.686116, 2060205, 0.6047213077545166], [1437024333.763876, 2070362, 0.6034210324287415], [1437024813.707845, 2080507, 0.6008927822113037], [1437025293.483294, 2090645, 0.604469895362854], [1437025773.695712, 2100793, 0.6068717837333679], [1437026253.672994, 2110943, 0.6099737882614136], [1437026733.780775, 2121094, 0.6105009317398071], [1437027213.617849, 2131235, 0.611957311630249], [1437027693.694451, 2141382, 0.6141949892044067], [1437028173.68596, 2151537, 0.6135279536247253], [1437028653.584833, 2161685, 0.6111017465591431], [1437029133.792483, 2171839, 0.6135671138763428], [1437029613.661672, 2181977, 0.6112024188041687], [1437030093.641009, 2192118, 0.6097264289855957], [1437030573.656274, 2202268, 0.6097284555435181], [1437031053.643631, 2212416, 0.6121350526809692], [1437031533.777478, 2222583, 0.6147991418838501], [1437032013.704008, 2232736, 0.6118316054344177], [1437032493.638393, 2242882, 0.6191433072090149], [1437032973.684986, 2253041, 0.6188027262687683], [1437033453.699562, 2263183, 0.6163974404335022], [1437033933.918074, 2273320, 0.6144159436225891], [1437034413.596351, 2283443, 0.6123769879341125], [1437034893.640496, 2293579, 0.6139131188392639], [1437035373.637761, 2303701, 0.6150627136230469], [1437035853.669947, 2313823, 0.6149951219558716], [1437036333.78905, 2323961, 0.6155945658683777], [1437036813.699727, 2334089, 0.613308310508728], [1437037293.662592, 2344235, 0.6153736114501953], [1437037773.66716, 2354364, 0.6160987615585327], [1437038253.603687, 2364507, 0.611574113368988], [1437038733.78864, 2374644, 0.6145234107971191], [1437039213.641799, 2384782, 0.6117951273918152], [1437039693.687078, 2394923, 0.6129845380783081], [1437040173.635717, 2405058, 0.6095831394195557], [1437040653.673331, 2415194, 0.6110679507255554], [1437041133.764768, 2425322, 0.6099690198898315], [1437041613.629279, 2435449, 0.6105908155441284], [1437042093.703985, 2445575, 0.6124749779701233], [1437042573.496029, 2455712, 0.6118302345275879], [1437043053.686022, 2465844, 0.6094756722450256], [1437043533.731929, 2475974, 0.6094986796379089], [1437044013.636245, 2486095, 0.6114639639854431], [1437044493.69923, 2496238, 0.6101082563400269], [1437044973.652155, 2506373, 0.6105718612670898], [1437045453.691467, 2516497, 0.6115666627883911], [1437045933.935804, 2526637, 0.6128115653991699], [1437046413.635583, 2536770, 0.6122986078262329], [1437046893.626337, 2546896, 0.6142017245292664], [1437047373.67437, 2557029, 0.6111341714859009], [1437047853.652939, 2567169, 0.611350417137146], [1437048333.778436, 2577306, 0.6126709580421448], [1437048813.654248, 2587433, 0.6111524105072021], [1437049293.610609, 2597552, 0.6135894060134888], [1437049773.646573, 2607690, 0.6136029362678528], [1437050253.667925, 2617808, 0.6141685843467712], [1437050733.735291, 2627933, 0.6170881390571594], [1437051213.620222, 2638053, 0.6189730167388916], [1437051693.601978, 2648171, 0.6157540678977966], [1437052173.634985, 2658299, 0.6178646683692932], [1437052653.687176, 2668425, 0.6164441108703613], [1437053133.762819, 2678556, 0.6175132393836975], [1437053613.643698, 2688671, 0.6158696413040161], [1437054093.673047, 2698804, 0.6162974238395691], [1437054573.667371, 2708956, 0.6160892844200134], [1437055053.650441, 2719087, 0.6176281571388245], [1437055533.778469, 2729219, 0.6165231466293335], [1437056013.694082, 2739343, 0.6171510219573975], [1437056493.674871, 2749458, 0.6124134659767151], [1437056973.700234, 2759575, 0.6120688319206238], [1437057453.666129, 2769697, 0.6126770377159119], [1437057933.848506, 2779821, 0.6126595139503479], [1437058413.643799, 2789941, 0.616513729095459], [1437058893.715386, 2800076, 0.6130264401435852], [1437059373.62596, 2810207, 0.6114044785499573], [1437059853.650848, 2820334, 0.6077002882957458], [1437060333.792248, 2830465, 0.6086235046386719], [1437060813.682955, 2840600, 0.6084680557250977], [1437061293.681795, 2850745, 0.6094310879707336], [1437061773.691182, 2860880, 0.6066345572471619], [1437062253.662987, 2871013, 0.6094250082969666], [1437062733.760419, 2881153, 0.609106719493866], [1437063213.651969, 2891278, 0.6080747246742249], [1437063693.723523, 2901406, 0.6081057786941528], [1437064173.68663, 2911533, 0.6066460609436035], [1437064653.547643, 2921667, 0.6057829856872559], [1437065133.62645, 2931813, 0.6092885136604309], [1437065613.566569, 2941947, 0.6089289784431458], [1437066093.537804, 2952102, 0.6070758700370789], [1437066573.529332, 2962243, 0.6096142530441284], [1437067053.520098, 2972400, 0.609714925289154], [1437067533.605733, 2982561, 0.6116167306900024], [1437068013.535467, 2992698, 0.6119107007980347], [1437068493.559976, 3002839, 0.6119140386581421], [1437068973.558743, 3012983, 0.6115538477897644], [1437069453.562661, 3023116, 0.6126777529716492], [1437069933.627071, 3033256, 0.6146017909049988], [1437070413.574131, 3043386, 0.6119789481163025], [1437070893.658803, 3053528, 0.6139205694198608], [1437071373.638711, 3063659, 0.612362802028656], [1437071853.621384, 3073794, 0.6109192371368408], [1437072333.665269, 3083926, 0.6141091585159302], [1437072813.584388, 3094040, 0.6132751703262329], [1437073293.569178, 3104172, 0.6132386922836304]]
\ No newline at end of file diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/runs.json b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/runs.json new file mode 100644 index 0000000000..90fb0cbb09 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/data/runs.json @@ -0,0 +1,22 @@ +{ + "alpha": { + "scalars": [ + "d1", + "d2", + "d3", + "d4" + ], + "histograms": [], + "images": [] + }, + "beta": { + "scalars": [ + "d1", + "d2", + "d3", + "d4" + ], + "histograms": [], + "images": [] + } +} diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/demo/index.html b/tensorflow/tensorboard/components/tf-event-dashboard/demo/index.html new file mode 100644 index 0000000000..34ad6a7263 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/demo/index.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../tf-event-dashboard.html"> + <link rel="stylesheet" type="text/css" href="../../../lib/css/global.css"> + <title>Event Dashboard Demo Demo</title> + </head> + <body> + <script> + TF.Urls.runsUrl = function() {return "data/runs.json"}; + TF.Urls.scalarsUrl = function(tag, run) {return "data/" + run + "/" + tag + ".json";}; + </script> + + <tf-event-dashboard id="demo"></tf-event-dashboard> + </body> +</html> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts b/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts new file mode 100644 index 0000000000..bf9f7b70e2 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/dragZoomInteraction.ts @@ -0,0 +1,150 @@ +module Plottable { +export class DragZoomLayer extends Components.SelectionBoxLayer { + private _dragInteraction: Interactions.Drag; + private _doubleClickInteraction: Interactions.DoubleClick; + private xDomainToRestore: any[]; + private yDomainToRestore: any[]; + private isZoomed = false; + private easeFn: (t: number) => number = d3.ease("cubic-in-out"); + private _animationTime = 750; + + /* 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 + */ + constructor(xScale: QuantitativeScale<number | { valueOf(): number }>, + yScale: QuantitativeScale<number | { valueOf(): number }>) { + super(); + this.xScale(xScale); + this.yScale(yScale); + this._dragInteraction = new Interactions.Drag(); + this._dragInteraction.attachTo(this); + this._doubleClickInteraction = new Interactions.DoubleClick(); + this._doubleClickInteraction.attachTo(this); + this.setupCallbacks(); + } + + private setupCallbacks() { + let dragging = false; + this._dragInteraction.onDragStart((startPoint: Point) => { + this.bounds({ + topLeft: startPoint, + bottomRight: startPoint, + }); + }); + this._dragInteraction.onDrag((startPoint, endPoint) => { + this.bounds({topLeft: startPoint, bottomRight: endPoint}); + this.boxVisible(true); + dragging = true; + }); + this._dragInteraction.onDragEnd((startPoint, endPoint) => { + this.boxVisible(false); + this.bounds({topLeft: startPoint, bottomRight: endPoint}); + if (dragging) { + this.zoom(); + } + dragging = false; + }); + + this._doubleClickInteraction.onDoubleClick(this.unzoom.bind(this)); + } + + /* Set the time (in ms) over which the zoom will interpolate. + * 0 implies no interpolation. (ie zoom is instant) + */ + public animationTime(): number; + public animationTime(animationTime: number): DragZoomLayer; + public animationTime(animationTime?: number): any { + 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. */ + public ease(fn: (t: number) => number): DragZoomLayer { + if (typeof(fn) !== "function") { + throw new Error("ease function must be a function"); + } + if (fn(0) !== 0 || fn(1) !== 1) { + 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 + private zoom() { + let x0: number = this.xExtent()[0].valueOf(); + let x1: number = this.xExtent()[1].valueOf(); + let y0: number = this.yExtent()[1].valueOf(); + let y1: number = 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 + private unzoom() { + 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 + private isZooming(isZooming: boolean) { + this._dragInteraction.enabled(!isZooming); + this._doubleClickInteraction.enabled(!isZooming); + } + + private interpolateZoom(x0f: number, x1f: number, y0f: number, y1f: number) { + let x0s: number = this.xScale().domain()[0].valueOf(); + let x1s: number = this.xScale().domain()[1].valueOf(); + let y0s: number = this.yScale().domain()[0].valueOf(); + let y1s: number = this.yScale().domain()[1].valueOf(); + + // Copy a ref to the ease fn, so that changing ease wont affect zooms in progress + let ease = this.easeFn; + let interpolator = (a: number, b: number, p: number) => d3.interpolateNumber(a, b)(ease(p)); + + this.isZooming(true); + let start = Date.now(); + let draw = () => { + let now = Date.now(); + let passed = now - start; + let p = this._animationTime === 0 ? 1 : Math.min(1, passed / this._animationTime); + let x0 = interpolator(x0s, x0f, p); + let x1 = interpolator(x1s, x1f, p); + let y0 = interpolator(y0s, y0f, p); + let y1 = interpolator(y1s, y1f, p); + this.xScale().domain([x0, x1]); + this.yScale().domain([y0, y1]); + if (p < 1) { + Utils.DOM.requestAnimationFramePolyfill(draw); + } else { + this.isZooming(false); + } + }; + draw(); + } +} +} diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.html new file mode 100644 index 0000000000..39ca9704c3 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.html @@ -0,0 +1,101 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../imports/plottable.html"> +<link rel="import" href="../imports/lodash.html"> + +<!-- +tf-chart (TFChart) creates an element that draws a line chart for dispalying event values. +It has the following settable properties: + tag: (required, string) - the name of the tag to load for this chart + selectedRuns: (required, string[]) - the runs the chart should display + xType: (required, string) - the way to display x values - allows "step" or "wall_time" + dataCoordinator: (required, TF.DataCoordinator) - the data coordinator for talking to backend + colorScale: (required, Plottable.Scales.Color) - maps from runs to colors + tooltipUpdater: (required, function) - allows the chart to modify tooltips + +It exposes the following methods: + redraw() - cause the chart to re-render (useful if e.g. container size changed) + +The data is expected to be an array of [wall_time, step, value] arrays. +The wall_time is serialized as seconds since epoch. +--> +<dom-module id="tf-chart"> + <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 src="dragZoomInteraction.js"></script> + <script src="tf-chart.js"></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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.ts b/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.ts new file mode 100644 index 0000000000..0b0103d697 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-chart.ts @@ -0,0 +1,327 @@ +/// <reference path="../../typings/tsd.d.ts" /> +/// <reference path="../../bower_components/plottable/plottable.d.ts" /> + +module TF { + type TFDatum = [number, number, number]; + type tooltipMap = {[run: string]: string}; + type TooltipUpdater = (tooltipMap, xValue, closestRun) => void; + + let Y_TOOLTIP_FORMATTER_PRECISION = 4; + let STEP_AXIS_FORMATTER_PRECISION = 4; + let Y_AXIS_FORMATTER_PRECISION = 3; + + export class BaseChart { + protected dataCoordinator: TF.DataCoordinator; + protected tag: string; + protected tooltipUpdater: TooltipUpdater; + + protected xAccessor: Plottable.Accessor<number | Date>; + protected xScale: Plottable.QuantitativeScale<number | Date>; + protected yScale: Plottable.QuantitativeScale<number>; + protected gridlines: Plottable.Components.Gridlines; + protected center: Plottable.Components.Group; + protected xAxis: Plottable.Axes.Numeric | Plottable.Axes.Time; + protected yAxis: Plottable.Axes.Numeric; + protected xLabel: Plottable.Components.AxisLabel; + protected yLabel: Plottable.Components.AxisLabel; + protected outer: Plottable.Components.Table; + protected colorScale: Plottable.Scales.Color; + protected xTooltipFormatter: (d: number) => string; + constructor( + tag: string, + dataCoordinator: TF.DataCoordinator, + tooltipUpdater: TooltipUpdater, + xType: string, + colorScale: Plottable.Scales.Color + ) { + this.dataCoordinator = dataCoordinator; + this.tag = tag; + this.colorScale = colorScale; + this.tooltipUpdater = tooltipUpdater; + this.buildChart(xType); + } + + public changeRuns(runs: string[]) { + throw new Error("Abstract method not implemented"); + } + + protected addCrosshairs(plot: Plottable.XYPlot<number | Date, number>, yAccessor): Plottable.Components.Group { + var pi = new Plottable.Interactions.Pointer(); + pi.attachTo(plot); + let xGuideLine = new Plottable.Components.GuideLineLayer<void>("vertical"); + let yGuideLine = new Plottable.Components.GuideLineLayer<void>("horizontal"); + xGuideLine.addClass("crosshairs"); + yGuideLine.addClass("crosshairs"); + var group = new Plottable.Components.Group([plot, xGuideLine, yGuideLine]); + let yfmt = multiscaleFormatter(Y_TOOLTIP_FORMATTER_PRECISION); + + pi.onPointerMove((p: Plottable.Point) => { + let run2val: {[run: string]: string} = {}; + let x: number = this.xScale.invert(p.x).valueOf(); + let yMin: number = this.yScale.domain()[0]; + let yMax: number = this.yScale.domain()[1]; + let closestRun: string = null; + let minYDistToRun: number = Infinity; + let yValueForCrosshairs: number = p.y; + plot.datasets().forEach((dataset) => { + let run: string = dataset.metadata().run; + let data: TFDatum[] = dataset.data(); + let xs: number[] = data.map((d, i) => this.xAccessor(d, i, dataset).valueOf()); + let idx: number = _.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; + } + let previous = data[idx - 1]; + let next = data[idx]; + let x0: number = this.xAccessor(previous, idx - 1, dataset).valueOf(); + let x1: number = this.xAccessor(next, idx, dataset).valueOf(); + let y0: number = yAccessor(previous, idx - 1, dataset).valueOf(); + let y1: number = yAccessor(next, idx, dataset).valueOf(); + let slope: number = (y1 - y0) / (x1 - x0); + let y: number = 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; + } + let 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(() => { + this.tooltipUpdater(null, null, null); + xGuideLine.pixelPosition(-1); + yGuideLine.pixelPosition(-1); + }); + + return group; + + } + + protected buildChart(xType: string) { + 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"); + let 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] + ]); + } + + protected buildPlot(xAccessor, xScale, yScale): Plottable.Component { + throw new Error("Abstract method not implemented."); + } + + public renderTo(target: d3.Selection<any>) { + this.outer.renderTo(target); + } + + public redraw() { + this.outer.redraw(); + } + + protected destroy() { + this.outer.destroy(); + } + } + + export class LineChart extends BaseChart { + private plot: Plottable.Plots.Line<number | Date>; + protected buildPlot(xAccessor, xScale, yScale): Plottable.Component { + var yAccessor = accessorize("2"); + var plot = new Plottable.Plots.Line<number | Date>(); + plot.x(xAccessor, xScale); + plot.y(yAccessor, yScale); + plot.attr("stroke", (d: any, i: number, m: any) => m.run, this.colorScale); + this.plot = plot; + var group = this.addCrosshairs(plot, yAccessor); + return group; + } + + public changeRuns(runs: string[]) { + var datasets = this.dataCoordinator.getDatasets(this.tag, runs); + this.plot.datasets(datasets); + } + + } + + export class HistogramChart extends BaseChart { + private plots: Plottable.XYPlot<number | Date, number>[]; + + public changeRuns(runs: string[]) { + var datasets = this.dataCoordinator.getDatasets(this.tag, runs); + this.plots.forEach((p) => p.datasets(datasets)); + } + + protected buildPlot(xAccessor, xScale, yScale): Plottable.Component { + var percents = [0, 228, 1587, 3085, 5000, 6915, 8413, 9772, 10000]; + var opacities = _.range(percents.length - 1).map((i) => (percents[i + 1] - percents[i]) / 2500); + var accessors = percents.map((p, i) => (datum) => datum[2][i][1]); + var median = 4; + var medianAccessor = accessors[median]; + + var plots = _.range(accessors.length - 1).map((i) => { + var p = new Plottable.Plots.Area<number | Date>(); + 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", (d: any, i: number, m: any) => m.run, this.colorScale); + p.attr("stroke", (d: any, i: number, m: any) => m.run, this.colorScale); + p.attr("stroke-weight", (d: any, i: number, m: any) => "0.5px"); + p.attr("stroke-opacity", () => opacities[i]); + p.attr("fill-opacity", () => opacities[i]); + return p; + }); + + var medianPlot = new Plottable.Plots.Line<number | Date>(); + medianPlot.x(xAccessor, xScale); + medianPlot.y(medianAccessor, yScale); + medianPlot.attr("stroke", (d: any, i: number, m: any) => m.run, this.colorScale); + + this.plots = plots; + var group = this.addCrosshairs(medianPlot, medianAccessor); + return new Plottable.Components.Group([new Plottable.Components.Group(plots), group]); + } + } + + /* 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: number): ((v: number) => string) { + return (v: number) => { + var absv = Math.abs(v); + if (absv < 1E-15) { + // Sometimes zero-like values get an annoying representation + absv = 0; + } + var f: (x: number) => string; + 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: string): Plottable.Accessor<number> { + return (d: any, index: number, dataset: Plottable.Dataset) => d[key]; + } + + interface XComponents { + /* tslint:disable */ + scale: Plottable.Scales.Linear | Plottable.Scales.Time, + axis: Plottable.Axes.Numeric | Plottable.Axes.Time, + accessor: Plottable.Accessor<number | Date>, + tooltipFormatter: (d: number) => string; + /* tslint:enable */ + } + + function stepX(): XComponents { + 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(): XComponents { + 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: (d: any, index: number, dataset: Plottable.Dataset) => { + return d[0] * 1000; // convert seconds to ms + }, + tooltipFormatter: (d: number) => formatter(new Date(d)), + }; + } + + function relativeX(): XComponents { + var scale = new Plottable.Scales.Linear(); + var formatter = (n: number) => { + 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: (d: any, index: number, dataset: Plottable.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: string): XComponents { + switch (xType) { + case "step": + return stepX(); + case "wall_time": + return wallX(); + case "relative": + return relativeX(); + default: + throw new Error("invalid xType: " + xType); + } + } +} diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-color-scale.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-color-scale.html new file mode 100644 index 0000000000..b559cab9cd --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-color-scale.html @@ -0,0 +1,69 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../imports/lodash.html"> +<link rel="import" href="../imports/plottable.html"> + +<!-- +tf-color-scale is a plumbing component that takes in an array of runs, and produces +an upward-bindable outColorScale, which is a color scale mapping from those runs to +a set of colors. + +Right now, the colors are hard-coded and must be manually synchronized with the colors expected in +tf-run-selector. TODO(danmane): we should enshrine the mapping elsewhere. +--> +<dom-module id="tf-color-scale"> + <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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-data-coordinator.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-data-coordinator.html new file mode 100644 index 0000000000..454dff4a9e --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-data-coordinator.html @@ -0,0 +1,29 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../imports/plottable.html"> +<link rel="import" href="../imports/lodash.html"> + +<!-- +tf-data-coordinator is a simple plumbing component that takes in a value url generator +(a function that takes a tag and a run and returns a url), and produces an upward-bindable +TF.DataCoordinator for consumption elsewhere. +--> +<dom-module id="tf-data-coordinator"> + <script src="dataCoordinator.js"></script> + <script src="dataset.js"></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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-event-dashboard.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-event-dashboard.html new file mode 100644 index 0000000000..534f62072f --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-event-dashboard.html @@ -0,0 +1,208 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="tf-data-coordinator.html"> +<link rel="import" href="tf-tooltip-coordinator.html"> +<link rel="import" href="tf-run-selector.html"> +<link rel="import" href="tf-x-type-selector.html"> +<link rel="import" href="../tf-dashboard-common/tf-run-generator.html"> +<link rel="import" href="tf-color-scale.html"> +<link rel="import" href="../tf-dashboard-common/tf-url-generator.html"> +<link rel="import" href="../tf-dashboard-common/tf-dashboard-layout.html"> +<link rel="import" href="../tf-dashboard-common/tensorboard-color.html"> +<link rel="import" href="../tf-dashboard-common/dashboard-style.html"> +<link rel="import" href="../tf-dashboard-common/tf-downloader.html"> +<link rel="import" href="../tf-categorizer/tf-categorizer.html"> +<link rel="import" href="tf-chart.html"> +<link rel="import" href="../tf-collapsable-pane/tf-collapsable-pane.html"> +<link rel="import" href="../../bower_components/iron-collapse/iron-collapse.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="../imports/lodash.html"> +<link rel="import" href="../tf-dashboard-common/warning-style.html"> + +<!-- +tf-event-dashboard is a complete frontend that loads runs from a backend, +and creates chart panes that display data for those runs. + +It provides a categorizer, run selector, and x type selector, by which the user +can customize how data is organized and displayed. + +Each chart has a button that can toggle whether it is "selected"; selectedRuns +charts are larger. + +Organizationally, the #plumbing div contains components that have no concrete +manifestation and just effect data bindings or data loading. The #sidebar contains +shared controls like the tf-categorizer, tf-run-selector, and tf-x-type-selector. +The #center div contains tf-charts embedded inside tf-collapsable-panes. +--> +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-run-selector.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-run-selector.html new file mode 100644 index 0000000000..92cecb2e5a --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-run-selector.html @@ -0,0 +1,104 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-checkbox/paper-checkbox.html"> +<link rel="import" href="../imports/lodash.html"> +<link rel="import" href="../tf-dashboard-common/scrollbar-style.html"> +<link rel="import" href="../tf-multi-checkbox/tf-multi-checkbox.html"> + +<!-- +tf-run-selector creates a set of checkboxes to display which runs are selected. +It also displays tooltips. + +Properties in: +- runs: Array of strings representing the runs that may be selected +- tooltips: An object that maps from a run to the associated tooltip string. +When tooltips are available, runs that have no associated tooltip will be +hidden. When tooltips are available, the runs will be sorted by their tooltip. +- closestRun: The name of the run that is closest to the cursor (present when +tooltips are active). It will be highlighted +- classScale: An object (generated by tf-dashboard-common/tf-color-scale) that +maps from a run name to a class name, which will be used to color the run. +- xValue: The string that represents the x-value associated with the tooltips. +- xType: The string that describes what kind of data is displayed on the x axis. + +Properties out: +- outSelected: The array of run names that are currently checked by the user. + +--> +<dom-module id="tf-run-selector"> + <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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-tooltip-coordinator.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-tooltip-coordinator.html new file mode 100644 index 0000000000..23b0cba87a --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-tooltip-coordinator.html @@ -0,0 +1,48 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<!-- tf-tooltip-coordinator is a plumbing component that provides a TooltipUpdater, +which is a function that allows modification of the values within the tooltip-coordinator +from javascript logic elsewhere. It then propagates the values to other Polymer components. + +Thus, the tooltip-coordinator allows many JS pieces of the application to modify a single +piece of shared state. + --> +<dom-module id="tf-tooltip-coordinator"> + <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> diff --git a/tensorflow/tensorboard/components/tf-event-dashboard/tf-x-type-selector.html b/tensorflow/tensorboard/components/tf-event-dashboard/tf-x-type-selector.html new file mode 100644 index 0000000000..078e456a0d --- /dev/null +++ b/tensorflow/tensorboard/components/tf-event-dashboard/tf-x-type-selector.html @@ -0,0 +1,75 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-button/paper-button.html"> +<link rel="import" href="../tf-dashboard-common/tensorboard-color.html"> + +<!-- +tf-x-type-selector is a simple component that creates buttons labeled "step" and "wall", + and provides (as upward bindable) an outXType property that is either "step" or "wall_time". +--> +<dom-module id="tf-x-type-selector"> + <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> diff --git a/tensorflow/tensorboard/components/tf-graph-board/tf-graph-board.html b/tensorflow/tensorboard/components/tf-graph-board/tf-graph-board.html new file mode 100644 index 0000000000..c053d6f7a7 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-board/tf-graph-board.html @@ -0,0 +1,152 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../tf-graph/tf-graph.html"> +<link rel="import" href="../tf-graph-info/tf-graph-info.html"> +<link rel="import" href="../../bower_components/paper-progress/paper-progress.html"> +<!-- Element for putting tf-graph and tf-graph-info side by side. + +Example + +<tf-graph-board graph=[[graph]]></tf-graph-board> + +--> + +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/colors.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/colors.ts new file mode 100644 index 0000000000..8912483c09 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/colors.ts @@ -0,0 +1,133 @@ +module tf { + +/** + * Mapping from color palette name to color pallette, which contains + * exact colors for multiple states of a single color pallette. + */ +export let 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", // 500 + "active": "#424242", // 800 + "disabled": "F5F5F5" // 100 + } +].reduce((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"; + */ +export let 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((m, c) => { + c.groups.forEach(function(group) { + m[group] = c.color; + }); + return m; +}, {}); + +} diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts new file mode 100644 index 0000000000..ed148bf719 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/common.ts @@ -0,0 +1,236 @@ +/// <reference path="../../../typings/tsd.d.ts" /> + +declare module graphlib { + + interface GraphOptions { + name: string; + /** + * Direction for rank nodes. Can be TB, BT, LR, or RL, where T = top, + * B = bottom, L = left, and R = right. + */ + rankdir: string; + type: string|number; + /** Number of pixels between each rank in the layout. */ + ranksep?: number; + /** Number of pixels that separate nodes horizontally in the layout. */ + nodesep?: number; + } + + export interface EdgeObject { + v: string; + w: string; + name?: string; + } + + export class Graph<N, E> { + constructor(opt?: Object); + setNode(name: string, value?: N): void; + hasNode(name: string): boolean; + setEdge(fromName: string, toName: string, value?: E): void; + hasEdge(fromName: string, toName: string): boolean; + edge(fromName: string, toName: string): E; + edge(edgeObject: EdgeObject): E; + removeEdge(v: string, w: string): void; + nodes(): string[]; + node(name: string): N; + removeNode(name: string): void; + setGraph(graphOptions: GraphOptions): void; + graph(): GraphOptions; + nodeCount(): number; + neighbors(name: string): string[]; + successors(name: string): string[]; + predecessors(name: string): string[]; + edges(): EdgeObject[]; + outEdges(name: string): E[]; + inEdges(name: string): E[]; + /** Returns those nodes in the graph that have no in-edges. Takes O(|V|) time. */ + sources(): string[]; + /** + * Remove the node with the id v in the graph or do nothing if + * the node is not in the graph. If the node was removed this + * function also removes any incident edges. Returns the graph, + * allowing this to be chained with other functions. Takes O(|E|) time. + */ + removeNode(name: string): Graph<N, E>; + setParent(name: string, parentName: string): void; + } +} + +module tf { +/** + * Recommended delay (ms) when running an expensive task asynchronously + * that gives enough time for the progress bar to update its UI. + */ +const ASYNC_TASK_DELAY = 20; + +export function time<T>(msg: string, task: () => T) { + let start = Date.now(); + let result = task(); + /* tslint:disable */ + console.log(msg, ":", Date.now() - start, "ms"); + /* tslint:enable */ + return result; +} + +/** + * Tracks task progress. Each task being passed a progress tracker needs + * to call the below-defined methods to notify the caller about the gradual + * progress of the task. + */ +export interface ProgressTracker { + updateProgress(incrementValue: number): void; + setMessage(msg: string): void; + reportError(msg: string): void; +} + +/** + * 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. + */ +export function getSubtaskTracker(parentTracker: ProgressTracker, + impactOnTotalProgress: number, subtaskMsg: string): ProgressTracker { + 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); + } + }; +} + +/** + * Runs an expensive task asynchronously and returns a promise of the result. + */ +export function runAsyncTask<T>(msg: string, incProgressValue: number, + task: () => T, tracker: ProgressTracker): Promise<T> { + return new Promise((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); + }); +} + +/** + * Returns a query selector with escaped special characters that are not + * allowed in a query selector. + */ +export function escapeQuerySelector(querySelector: string): string { + return querySelector.replace( /([:.\[\],/\\\(\)])/g, "\\$1" ); +} + +/** + * TensorFlow node definition as definied in the graph proto file. + */ +export interface TFNode { + /** Name of the node */ + name: string; + /** List of nodes that are inputs for this node. */ + input: string[]; + /** The name of the device where the computation will run. */ + device: string; + /** The name of the operation associated with this node. */ + op: string; + /** List of attributes that describe/modify the operation. */ + attr: {key: string, value: Object}[]; +} + +/** + * TensorFlow stats file definition as defined in the stats proto file. + */ +export interface TFStats { + devStats: {device: string, nodeStats: TFNodeStats[]}[]; +} + +/** + * TensorFlow stats for a node as defined in the stats proto file. + */ +export interface TFNodeStats { + nodeName: string; + // The next 4 properties are currently stored as string in json + // and must be parsed. + allStartMicros: number; + opStartRelMicros: number; + opEndRelMicros: number; + allEndRelMicros: number; + memory: { + allocatorName: string; + totalBytes: number; // Stored as string in json and should be parsed. + peakBytes: number; // Stored as string in json and should be parsed. + }[]; + /** Output sizes recorded for a single execution of a graph node */ + output: TFNodeOutput[]; + timelineLabel: string; + scheduledMicros: string; + threadId: string; +} + +/** + * Description for the output tensor(s) of an operation in the graph. + */ +export interface TFNodeOutput { + slot: number; // Stored as string in json and should be parsed. + /** Was the tensor allocated by this Op or a previous computation */ + allocationType: string; + tensorDescription: { + /** Data type of tensor elements */ + dtype: string; + /** Shape of the tensor */ + shape: { + /** + * Dimensions of the tensor, such as [{name: "input", size: 30}, + * {name: "output", size: 40}] for a 30 x 40 2D tensor. The names + * are optional. The order of entries in "dim" matters: It indicates + * the layout of the values in the tensor in-memory representation. + */ + dim: { + /** Size of the tensor in that dimension */ + size: number, // Stored as string in json and should be parsed. + /** Optional name of the tensor dimension */ + name?: string + }[]; + }; + /** Information about the size and allocator used for the data */ + allocationDescription: { + // The next 2 properties are stored as string in json and + // should be parsed. + /** Total number of bytes requested */ + requestedBytes: number; + /** Total number of bytes allocated, if known */ + allocatedBytes?: number; + /** Name of the allocator used */ + allocatorName: string; + }; + }; +} +} // close module tf + +/** + * Declaring dagre var used for dagre layout. + */ +declare var dagre: { layout(graph: graphlib.Graph<any, any>): void; }; diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts new file mode 100644 index 0000000000..64e537154b --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/graph.ts @@ -0,0 +1,889 @@ +/// <reference path="../../../typings/tsd.d.ts" /> +/// <reference path="common.ts" /> +module tf.graph { + +/** Delimiter used in node names to denote namespaces. */ +export const NAMESPACE_DELIM = "/"; +const FULL_GRAPH_NAME = "fullGraph"; +export const ROOT_NAME = "__root__"; + +// Separator between the source and the destination name of the edge. +export const EDGE_KEY_DELIM = "--"; + +export enum GraphType {FULL, EMBEDDED, META, SERIES, CORE, SHADOW, BRIDGE, + EDGE}; +export enum NodeType {META, OP, SERIES, BRIDGE, ELLIPSIS}; + +/** + * A BaseEdge is the label object (in the graphlib sense) for an edge in the + * original, full graph produced after parsing. Subsequent graphs, like those + * which belong to Metanodes, should not use BaseEdge objects, but instead + * contain Metaedges (which in turn may contain any number of BaseEdges). + */ +export interface BaseEdge extends graphlib.EdgeObject { + isControlDependency: boolean; + isReferenceEdge: boolean; +} + +/** + * A SlimGraph is inspired by graphlib.Graph, but having only the functionality + * that we need. + */ +export class SlimGraph { + nodes: { [nodeName: string]: OpNode }; + edges: BaseEdge[]; + + constructor() { + this.nodes = {}; + this.edges = []; + } +} + +interface NormalizedInput { + name: string; + hasNumberPart: boolean; + isControlDependency: boolean; +} + +interface BuildParams { + enableEmbedding: boolean; + inEmbeddingTypes: string[]; + outEmbeddingTypes: string[]; + refEdges: { [inputEdge: string]: boolean }; +} + +/** + * The most basic information about a node in the hierarchical graph. + */ +export interface Node { + /** The name of the node, used frequently to look up nodes by name. */ + name: string; + /** Which type of node this is. */ + type: NodeType; + /** + * Whether this node is a type that may contain other nodes. Those types + * should extend from GroupNode. + * + * For an OpNode, isGroupNode will be false, even though it may have + * embeddings. These embedding Nodes will have their parentNode set to the + * OpNode. However, embeddings are later rendered as annotations, not as + * children to be made visible on expansion (like a Metanode or SeriesNode). + */ + isGroupNode: boolean; + /** + * The number of nodes this node represents. For OpNodes, this will be 1, and + * for GroupNodes it will be a count of the total number of descendents it + * contains. + */ + cardinality: number; + /** + * The Node which is this Node's parent. This is of type Node and not + * GroupNode because of embeddings, which will have a parent OpNode. + */ + parentNode: Node; + /** Runtime execution stats for this node, if available */ + stats: NodeStats; +} + +export interface OpNode extends Node { + op: string; + device: string; + attr: {key: string, value: Object}[]; + inputs: NormalizedInput[]; + inEmbeddings: OpNode[]; + outEmbeddings: OpNode[]; +} + +export interface BridgeNode extends Node { + /** + * Whether this bridge node represents edges coming into its parent node. + */ + inbound: boolean; +} + +/** + * A node that is used when there are more than the maximum number of allowed + * annotations hanging off of a node. This node represents an ellipsis + * annotation, indicating a number of additional annotations. + */ +export interface EllipsisNode extends Node { + /** + * The number of nodes this ellipsis represents. + */ + numMoreNodes: number; + + /** + * Sets the number of nodes this ellipsis represents and changes the node + * name accordingly. + */ + setNumMoreNodes(numNodes: number); +} + +export interface GroupNode extends Node { + /** + * The metagraph contains nodes and metaedges between the immediate children + * of this group. The node label objects may be other GroupNodes (like + * SeriesNodes and Metanodes) or individual OpNodes. All edge label objects + * are Metaedges, each of which contains references to the original + * BaseEdge(s) from which it was created. + */ + metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + + /** + * The bridgegraph contains only edges which link immediate children of this + * group with nodes outside of the metagraph. As in the metagraph, all edge + * label objects are Metaedges which contain references to the original + * BaseEdge(s) that contribute to it. + * + * For a Metaedge in the bridgegraph, its external endpoint will be the same + * as the metagraph edge from which it came. This is most easily explained + * by example. + * + * Consider an original graph that contains a BaseEdge A/B/C->Z/Y/X. + * + * +-------+ (BaseEdge) +-------+ + * | A/B/C |>----------------->| Z/Y/X | + * +-------+ +-------+ + * + * When we construct the Root's metagraph, it will contain nodes for A and Z, + * and a Metaedge A->Z. The A->Z Metaedge will contain the original BaseEdge + * A/B/C->Z/Y/X in its baseEdgeGraph. The Root's bridgegraph will always be + * empty. + * + * +---+ (Root.metagraph edge) +---+ + * | A |>--------------------------->| Z | + * +---+ +---+ + * + * Now consider the Metanode A. Its metagraph will contain a Metanode for A/B + * and no edges. A's bridgegraph will have one Metaedge from A/B->Z, which + * was derived from the Root's Metaedge A->Z. That Metaedge will contain the + * original BaseEdge in its baseEdgeGraph. + * + * +---------+ + * | A | + * | +---+ | (A.bridgegraph edge) +---+ + * | | B |>---------------------------->| Z | + * | +---+ | +---+ + * +---------+ + * + * Finally, consider the Metanode A/B. Its metagraph will contain a Metanode + * for A/B/C and again no edges. A/B's bridgegraph will have one Metaedge + * from A/B/C->Z, which was derived from A's bridgegraph Metaedge A/B->Z. + * As before, the A/B/C->Z Metaedge will contain the original BaseEdge in its + * baseEdgeGraph. + * + * +---------------+ + * | A | + * | +---------+ | + * | | B | | + * | | +---+ | | (A/B.bridgegraph edge) +---+ + * | | | C |>----------------------------------->| Z | + * | | +---+ | | +---+ + * | +---------+ | + * +---------------+ + * + * Likewise, under the Metanode Z and Z/Y, to compute the bridgegraph, we'll + * end up with Metaedges A->Z/Y and A->Z/Y/X respectively. So the original + * BaseEdge A/B/C->Z/Y/X becomes four different Metaedges in four different + * bridgegraphs: + * + * + A/B->Z in GroupNode A's bridgegraph, + * + A/B/C->Z in GroupNode A/B's bridgegraph, + * + A->Z/Y in GroupNode Z's bridgegraph, and + * + A->Z/Y/X in GroupNode Z/Y's bridgegraph. + * + * Considering any BaseEdge then, if N is the number of path segments in the + * source and M is the number of path semgents in the destination, then the + * total number of bridgegraph edges you could create would be (N-1)(M-1). + * + * For this reason, it is computationally expensive to generate all the + * bridgegraphs for all the Metanodes, and instead they should be computed + * on demand as needed. + */ + bridgegraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + + /** + * Stores how many times each device name appears in its children + * op nodes. Used to color group nodes by devices. + */ + deviceHistogram: {[device: string]: number}; + + /** + * Flag indicating whether this GroupNode's metagraph contains any edges that + * are not control edges. Used to quickly determine how to draw a collapsed + * series (vertically or horizontally). + */ + hasNonControlEdges: boolean; +} + +export interface Metanode extends GroupNode { + depth: number; + templateId: string; + opHistogram: {[op: string]: number}; + getFirstChild(): GroupNode|OpNode; + getRootOp(): OpNode; + /** Return name of all leaves inside a metanode. */ + leaves(): string[]; +} + +export interface SeriesNode extends GroupNode { + hasLoop: boolean; + prefix: string; + suffix: string; + clusterId: number; + ids: number[]; + parent: string; +} + +export class EllipsisNodeImpl implements EllipsisNode { + name: string; + numMoreNodes: number; + stats: NodeStats; + type: NodeType; + isGroupNode: boolean; + cardinality: number; + parentNode: Node; + + /** + * Constructs a new ellipsis annotation node. + * + * @param numNodes The number of additional annotations this node represents. + */ + constructor(numNodes: number) { + this.type = NodeType.ELLIPSIS; + this.isGroupNode = false; + this.cardinality = 1; + this.parentNode = null; + this.stats = null; + this.setNumMoreNodes(numNodes); + } + + setNumMoreNodes(numNodes: number) { + this.numMoreNodes = numNodes; + this.name = "... " + numNodes + " more"; + } +}; + +/** + * A label object for nodes in the full graph and leaf nodes in the render + * graph. + */ +class OpNodeImpl implements OpNode { + name: string; + op: string; + device: string; + stats: NodeStats; + attr: {key: string, value: Object}[]; + inputs: NormalizedInput[]; + type: NodeType; + isGroupNode: boolean; + cardinality: number; + inEmbeddings: OpNode[]; + outEmbeddings: OpNode[]; + parentNode: Node; + + /** + * 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. + */ + constructor(rawNode: tf.TFNode, normalizedInputs: NormalizedInput[]) { + 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; + } +}; + +export function createMetanode(name: string, opt = {}): Metanode { + return new MetanodeImpl(name, opt); +} + +/** + * Joins the information from the stats file (memory, compute time) with the graph + * information. + */ +export function joinStatsInfoWithGraph(graph: SlimGraph, + statsJson: TFStats): void { + _.each(statsJson.devStats, stats => { + _.each(stats.nodeStats, 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. + let nodeName = nodeStats.nodeName in graph.nodes ? + nodeStats.nodeName : + nodeStats.nodeName + NAMESPACE_DELIM + "(" + nodeStats.nodeName + ")"; + if (nodeName in graph.nodes) { + // Compute the total bytes used. + let totalBytes = 0; + if (nodeStats.memory) { + _.each(nodeStats.memory, alloc => { + if (alloc.totalBytes) { + totalBytes += Number(alloc.totalBytes); + } + }); + } + let outputSize: number[][] = null; + if (nodeStats.output) { + outputSize = _.map(nodeStats.output, output => { + return _.map(output.tensorDescription.shape.dim, + dim => Number(dim.size)); + }); + } + graph.nodes[nodeName].stats = new NodeStats(totalBytes, + Number(nodeStats.allEndRelMicros), outputSize); + } + }); + }); +} + +/** + * Execution stats for the node. + */ +class NodeStats { + constructor(totalBytes: number, totalMicros: number, outputSize: number[][]) { + this.totalBytes = totalBytes; + this.totalMicros = totalMicros; + this.outputSize = outputSize; + } + + /** + * Total number of bytes used for the node. Sum of all childen + * if it is a Group node. + */ + totalBytes: number; + /** + * Total number of compute time in microseconds used for the node. + * Sum of all children if it is a Group node. + */ + totalMicros: number; + /** + * The shape of each output tensors, if there are any. + * Empty if it is a Group node. + */ + outputSize: number[][]; + + /** + * Combines the specified stats with the current stats. + * Modifies the current object. This methos is used to + * compute aggregate stats for group nodes. + */ + combine(stats: NodeStats): void { + if (stats.totalBytes != null) { + this.totalBytes += stats.totalBytes; + } + if (stats.totalMicros != null) { + this.totalMicros += stats.totalMicros; + } + } +} + +class MetanodeImpl implements Metanode { + name: string; + stats: NodeStats; + type: NodeType; + depth: number; + isGroupNode: boolean; + cardinality: number; + metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + bridgegraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + templateId: string; + opHistogram: {[op: string]: number}; + deviceHistogram: {[op: string]: number}; + parentNode: Node; + hasNonControlEdges: boolean; + + /** A label object for meta-nodes in the graph hierarchy */ + constructor(name: string, 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<GroupNode|OpNode, Metaedge>(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; + } + + getFirstChild(): GroupNode|OpNode { + 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). + */ + getRootOp(): OpNode { + let nameSplit = this.name.split("/"); + let rootOpName = this.name + "/(" + nameSplit[nameSplit.length - 1] + ")"; + return <OpNode>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. + */ + leaves(): string[] { + let leaves = []; + let queue = [<Node> this]; + let metagraph; // Defined here due to a limitation of ES6->5 compilation. + while (queue.length) { + let node = queue.shift(); + if (node.isGroupNode) { + metagraph = (<GroupNode> node).metagraph; + _.each(metagraph.nodes(), name => queue.push(metagraph.node(name))); + } else { + leaves.push(node.name); + } + } + return leaves; + } +}; + +export interface Metaedge extends graphlib.EdgeObject { + + /** + * Stores the original BaseEdges represented by this Metaedge. + */ + baseEdgeList: BaseEdge[]; + + /** + * Whether this edge represents a relationship that is inbound (or outbound) + * to the object which contains this information. For example, in a Metanode's + * bridgegraph, each edge connects an immediate child to something outside + * the Metanode. If the destination of the edge is inside the Metanode, then + * its inbound property should be true. If the destination is outside the + * Metanode, then its inbound property should be false. + * + * The property is optional because not all edges can be described as + * inbound/outbound. For example, in a Metanode's metagraph, all of the edges + * connect immediate children of the Metanode. None should have an inbound + * property, or they should be null/undefined. + */ + inbound?: boolean; + + /** + * Number of regular edges (not control dependency edges). + */ + numRegularEdges: number; + + /** + * Number of control dependency edges. + */ + numControlEdges: number; + + /** + * Number of reference edges, which is an edge to an operation + * that takes a reference to its input and changes its value. + */ + numRefEdges: number; + + addBaseEdge(edge: BaseEdge): void; +} + +export function createMetaedge(v: string, w: string): Metaedge { + return new MetaedgeImpl(v, w); +} + +/** + * A label object for edges between metanodes of subgraphs in the render graph. + */ +class MetaedgeImpl implements Metaedge { + v: string; + w: string; + baseEdgeList: BaseEdge[]; + inbound: boolean; + numRegularEdges: number; + numControlEdges: number; + numRefEdges: number; + + constructor(v: string, w: string) { + this.v = v; + this.w = w; + this.baseEdgeList = []; + this.inbound = null; + this.numRegularEdges = 0; + this.numControlEdges = 0; + this.numRefEdges = 0; + } + + addBaseEdge(edge: BaseEdge): void { + this.baseEdgeList.push(edge); + if (edge.isControlDependency) { + this.numControlEdges += 1; + } else { + this.numRegularEdges += 1; + } + if (edge.isReferenceEdge) { + this.numRefEdges += 1; + } + } +} + +export function createSeriesNode(prefix: string, suffix: string, + parent: string, clusterId: number, name: string): SeriesNode { + return new SeriesNodeImpl(prefix, suffix, parent, clusterId, name); +} + +export function getSeriesNodeName(prefix: string, suffix: string, + parent: string, startId?: number, endId?: number): string { + let numRepresentation = + (typeof startId !== "undefined" && typeof endId !== "undefined") ? + "[" + startId + "-" + endId + "]" : "#"; + let pattern = prefix + numRepresentation + suffix; + return (parent ? parent + "/" : "") + pattern; +} + +class SeriesNodeImpl implements SeriesNode { + name: string; + type: NodeType; + stats: NodeStats; + hasLoop: boolean; + prefix: string; + suffix: string; + clusterId: number; + ids: number[]; + parent: string; + isGroupNode: boolean; + cardinality: number; + metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + bridgegraph: graphlib.Graph<GroupNode|OpNode, Metaedge>; + parentNode: Node; + deviceHistogram: {[op: string]: number}; + hasNonControlEdges: boolean; + + constructor(prefix: string, suffix: string, parent: string, + clusterId: number, name: string) { + 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<Metanode, Metaedge>(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); + } +} + +/** + * 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: string[]): NormalizedInput[] { + return _.reduce(inputs, function(normalizedInputs, inputName) { + let start = inputName[0] === "^"; + let colon = inputName.lastIndexOf(":"); + let end = colon !== -1 && + inputName.length - colon > 1 && + !(/\D/).test(inputName.substring(colon + 1)) ? + colon : inputName.length; + let 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; + }, []); +} + +export function build(rawNodes: tf.TFNode[], params: BuildParams, + tracker: ProgressTracker): Promise<SlimGraph|void> { + /** + * A dictionary that maps each in-embedding node name to its host node label + * object. + */ + let inEmbedding: {[nodeName: string]: OpNode} = {}; + /** + * A dictionary that maps each node name to an array of the node's + * out-embedding node label objects. + */ + let outEmbeddings: {[inputName: string]: OpNode[]} = {}; + let isInEmbeddedPred = getEmbedPredicate(params.inEmbeddingTypes); + let isOutEmbeddedPred = getEmbedPredicate(params.outEmbeddingTypes); + let embeddingNodeNames: string[] = []; + /** + * 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. + */ + let nodeNames = new Array<string>(rawNodes.length); + + return runAsyncTask("Normalizing names", 30, () => { + let opNodes = new Array<OpNode>(rawNodes.length); + let index = 0; + _.each(rawNodes, rawNode => { + let normalizedInputs = normalizeInputs(rawNode.input); + let 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, input => { + let 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((opNodes) => { + // Create the graph data structure from the graphlib library. + return runAsyncTask("Building the data structure", 70, () => { + let normalizedNameDict = mapStrictHierarchy(nodeNames, + embeddingNodeNames); + let graph = new SlimGraph; + + // Add the nodes to the graph. + _.each(opNodes, opNode => { + let 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, 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, opNode => { + _.each(opNode.inputs, (input, i) => { + let 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, (node, name) => { + node.name = normalizedNameDict[node.name] || node.name; + }); + + return graph; + }, tracker); + }) + .catch(function(reason) { + throw new Error("Failure creating graph"); + }); +}; + +/** + * Create a new graphlib.Graph() instance with default parameters + */ +export function createGraph<N, E>(name: string, type, opt = {}): + graphlib.Graph<N, E> { + let graph = new graphlib.Graph<N, E>(opt); + graph.setGraph({ + name: name, + rankdir: "BT", // BT,TB,LR,RL + type: type + }); + return graph; +}; + +/** + * Create a predicate for checking whether a node should be embedded based on + * the specified types. + */ +function getEmbedPredicate(types: string[]) { + return function(node) { + // check types + for (let i = 0; i < types.length; i++) { + let 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: string): string { + let parts = name.split(NAMESPACE_DELIM); + return name + 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: string[], + embeddingNodeNames: string[]): {[oldName: string]: string} { + /** Dictionary that maps the old new to the new name */ + let newNameDictionary: {[oldName: string]: string} = {}; + /** Set used to store all namespaces. */ + let namespaceSet: {[namespace: string]: boolean} = {}; + // sort the nodes to make prefix check faster + nodeNames.sort(); + // look for nodes with a prefix a,a/b -> a/(a),a/b + for (let i = 0; i < nodeNames.length - 1; ++i) { + let 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), ns => { + namespaceSet[ns] = true; + }); + let b = nodeNames[i + 1]; + if (_.startsWith(b, a + NAMESPACE_DELIM)) { + newNameDictionary[a] = getStrictName(a); + } + } + // Go through all the embedding node names and rename them in case they + // collide with namespaces. + _.each(embeddingNodeNames, 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: graphlib.Graph<any, any>): number[] { + let 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. + */ +export function hasSimilarDegreeSequence(graph1: graphlib.Graph<any, any>, + graph2: graphlib.Graph<any, any>): boolean { + let dg1 = degreeSequence(graph1); + let dg2 = degreeSequence(graph2); + + for (let i = 0; i < dg1.length; i++) { + if (dg1[i] !== dg2[i]) { + return false; + } + } + return true; +}; + +/** + * 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']. + */ +export function getHierarchicalPath(name: string, + seriesNames?: { [name: string]: string }): string[] { + let path: string[] = []; + let i = name.indexOf(NAMESPACE_DELIM); + // Push all parent portions of the path. + while (i >= 0) { + path.push(name.substring(0, i)); + i = name.indexOf(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) { + let seriesName = seriesNames[name]; + if (seriesName) { + path.push(seriesName); + } + } + // Push the leaf of the path. + path.push(name); + return path; +}; + +} // close module tf.graph diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts new file mode 100644 index 0000000000..6c9333de4c --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/hierarchy.ts @@ -0,0 +1,715 @@ +/// <reference path="graph.ts" /> +/// <reference path="template.ts" /> + +/** + * Package for the Graph Hierarchy for TensorFlow graph. + */ +module tf.graph.hierarchy { + +const LOG_PREFIX_MSG = "Graph hierarchy: "; + +/** + * Class used as output for getPredecessors and getSuccessors methods + */ +interface Edges { + control: string[]; + regular: string[]; +} + +export interface Hierarchy { + root: Metanode; + templates: {[templateId: string]: string[]}; + /** List of all device names */ + devices: string[]; + getNodeMap(): {[nodeName: string]: GroupNode|OpNode}; + node(name: string): GroupNode|OpNode; + setNode(name: string, node: GroupNode|OpNode): void; + getBridgegraph(nodeName: string): graphlib.Graph<GroupNode|OpNode, Metaedge>; + getPredecessors(nodeName: string): Edges; + getSuccessors(nodeName: string): Edges; + getTopologicalOrdering(nodeName: string): { [childName: string]: number }; +} + +/** + * Class for the Graph Hierarchy for TensorFlow graph. + */ +class HierarchyImpl implements Hierarchy { + root: Metanode; + templates: {[templateId: string]: string[]}; + private index: {[nodeName: string]: GroupNode|OpNode}; + devices: string[]; + orderings: { [nodeName: string]: { [childName: string]: number } }; + + constructor() { + this.root = createMetanode(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[ROOT_NAME] = this.root; + this.orderings = {}; + } + + getNodeMap(): {[nodeName: string]: GroupNode|OpNode} { + return this.index; + } + + node(name: string): GroupNode|OpNode { + return this.index[name]; + } + + setNode(name: string, node: GroupNode|OpNode): void { + 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. + */ + getBridgegraph(nodeName: string): graphlib.Graph<GroupNode|OpNode, Metaedge> { + let node = this.index[nodeName]; + if (!node) { + throw Error("Could not find node in hierarchy: " + nodeName); + } + if (!("metagraph" in node)) { + return null; + } + let groupNode = <GroupNode> node; + if (groupNode.bridgegraph) { + return groupNode.bridgegraph; + } + let bridgegraph = groupNode.bridgegraph = + createGraph<GroupNode|OpNode, Metaedge>( + "BRIDGEGRAPH", GraphType.BRIDGE); + if (!node.parentNode || !("metagraph" in node.parentNode)) { + return bridgegraph; + } + + let parentNode = <GroupNode>node.parentNode; + let parentMetagraph = parentNode.metagraph; + let 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], parentGraph => { + _(parentGraph.edges()) + .filter(e => e.v === nodeName || e.w === nodeName) + .each(parentEdgeObj => { + + let inbound = parentEdgeObj.w === nodeName; + let 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, baseEdge => { + + // Based on the direction, figure out which is the descendant node + // and which is the "other" node (sibling of parent or ancestor). + let [descendantName, otherName] = + inbound ? + [baseEdge.w, parentEdgeObj.v] : + [baseEdge.v, parentEdgeObj.w]; + + // Determine the immediate child containing this descendant node. + let 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. + let bridgeEdgeObj = <graphlib.EdgeObject> { + v: inbound ? otherName : childName, + w: inbound ? childName : otherName, + }; + let bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj); + if (!bridgeMetaedge) { + bridgeMetaedge = 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. + */ + getChildName(nodeName: string, descendantName: string): string { + // Walk up the hierarchy from the descendant to find the child. + let currentNode: Node = 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. + */ + getPredecessors(nodeName: string): Edges { + let node = this.index[nodeName]; + if (!node) { + throw Error("Could not find node with name: " + nodeName); + } + + let predecessors = this.getOneWayEdges(node, true); + + // Add embedded predecessors, such as constants. + if (!node.isGroupNode) { + _.each((<OpNode>node).inEmbeddings, 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. + */ + getSuccessors(nodeName: string): Edges { + let node = this.index[nodeName]; + if (!node) { + throw Error("Could not find node with name: " + nodeName); + } + + let successors = this.getOneWayEdges(node, false); + + // Add embedded successors, such as summaries. + if (!node.isGroupNode) { + _.each((<OpNode>node).outEmbeddings, embeddedNode => { + successors.regular.push(embeddedNode.name); + }); + } + return successors; + } + + /** Helper method for getPredeccessors and getSuccessors */ + getOneWayEdges(node: GroupNode|OpNode, inEdges: boolean) { + let edges = { control: [], regular: [] }; + // A node with no parent cannot have any edges. + if (!node.parentNode) { + return edges; + } + if (node.parentNode.isGroupNode) { + let parentNode = <GroupNode>node.parentNode; + let metagraph = parentNode.metagraph; + let 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. + */ + getTopologicalOrdering(nodeName: string): { [childName: string]: number } { + let 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. + let successors: { [childName: string]: string[] } = {}; + + // Set of node names which have appeared as a destination. + let destinations: { [childName: string]: boolean } = {}; + + let metagraph = (<GroupNode> node).metagraph; + _.each(metagraph.edges(), (e: graphlib.EdgeObject) => { + 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). + let queue: string[] = + _.difference(_.keys(successors), _.keys(destinations)); + + // Produce an ordering by traversing the graph breadth first. + let ordering = this.orderings[nodeName] = {}; + let index = 0; + while (queue.length) { + let childName = queue.shift(); + ordering[childName] = index++; + _.each(successors[childName], succName => queue.push(succName)); + delete successors[childName]; // Prevent cycles from infinite looping. + } + return ordering; + } + +} + +/** + * 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: graphlib.Graph<GroupNode|OpNode, Metaedge>, + node: Node, inbound: boolean, targets: Edges): void { + _.each(<Metaedge[]> graph.edges(), e => { + let [selfName, otherName] = inbound ? [e.w, e.v] : [e.v, e.w]; + if (selfName === node.name) { + if (node.isGroupNode) { + let targetList = graph.edge(e).numRegularEdges + ? targets.regular : targets.control; + targetList.push(otherName); + } else { + _.each(graph.edge(e).baseEdgeList, baseEdge => { + let targetList = baseEdge.isControlDependency + ? targets.control : targets.regular; + targetList.push(inbound ? baseEdge.v : baseEdge.w); + }); + } + } + }); +} + +interface HierarchyParams { + verifyTemplate: boolean; + groupSeries: boolean; +} + +/** + * @param graph The raw graph. + * @param params Parameters used when building a hierarchy. + */ +export function build(graph: tf.graph.SlimGraph, params: HierarchyParams, + tracker: ProgressTracker): Promise<Hierarchy|void> { + let h = new HierarchyImpl(); + let seriesNames: { [name: string]: string } = {}; + return runAsyncTask("Adding nodes", 20, () => { + // Get all the possible device names. + let deviceNames = {}; + _.each(graph.nodes, (node, nodeName) => { + if (node.device != null) { + deviceNames[node.device] = true; + } + }); + h.devices = _.keys(deviceNames); + addNodes(h, graph); + }, tracker) + .then(() => { + return runAsyncTask("Detect series", 20, () => { + if (params.groupSeries) { + groupSeries(h.root, h, seriesNames); + } + }, tracker); + }) + .then(() => { + return runAsyncTask("Adding edges", 30, () => { + addEdges(h, graph, seriesNames); + }, tracker); + }) + .then(() => { + return runAsyncTask("Finding similar subgraphs", 30, () => { + h.templates = template.detect(h, params.verifyTemplate); + }, tracker); + }) + .then(() => { + return h; + }).catch(function(reason) { + throw new Error("Failure creating graph hierarchy"); + }); +}; + +/** + * Creates the metanodes in the hierarchical graph and assigns parent-child + * relationship between them. + */ +function addNodes(h: Hierarchy, graph: SlimGraph) { + _.each(graph.nodes, (node, nodeName) => { + let path = getHierarchicalPath(node.name); + let parent: Metanode = 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 (let 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; } + let name = path[i]; + let child = <Metanode>h.node(name); + if (!child) { + child = createMetanode(name); + child.parentNode = parent; + h.setNode(name, child); + parent.metagraph.setNode(name, 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: Hierarchy, graph: SlimGraph, + seriesNames: { [name: string]: string }) { + + let 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. + let sourcePath: string[] = []; + let destPath: string[] = []; + + // 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). + let getPath = (node: Node, path: string[]): number => { + let i = 0; + while (node) { + path[i++] = node.name; + node = node.parentNode; + } + return i - 1; + }; + + _.each(graph.edges, baseEdge => { + + // Get the hierarchical paths for the source and destination of the edge. + let sourceAncestorIndex = getPath(graph.nodes[baseEdge.v], sourcePath); + let 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."); + } + } + + let sharedAncestorNode = + <GroupNode>nodeIndex[sourcePath[sourceAncestorIndex + 1]]; + let sourceAncestorName = sourcePath[sourceAncestorIndex]; + let destAncestorName = destPath[destAncestorIndex]; + + // Find or create the Metaedge which should contain this BaseEdge inside + // the shared ancestor. + let metaedge = + sharedAncestorNode.metagraph.edge(sourceAncestorName, destAncestorName); + if (!metaedge) { + metaedge = 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: Metanode, hierarchy: Hierarchy, + seriesNames: { [name: string]: string }) { + let metagraph = metanode.metagraph; + _.each(metagraph.nodes(), n => { + let child = metagraph.node(n); + if (child.type === tf.graph.NodeType.META) { + groupSeries(<Metanode>child, hierarchy, seriesNames); + } + }); + + let clusters = clusterNodes(metagraph); + let seriesDict = detectSeries(clusters, metagraph); + + // Add each series node to the graph and add its grouped children to its own + // metagraph. + _.each(seriesDict, function(seriesNode: SeriesNode, seriesName: string) { + let nodeMemberNames = seriesNode.metagraph.nodes(); + let firstMember = seriesNode.metagraph.node(nodeMemberNames[0]); + let seriesType = firstMember.type; + + hierarchy.setNode(seriesName, seriesNode); // add to the index + metagraph.setNode(seriesName, seriesNode); + _.each(nodeMemberNames, n => { + let child = <OpNode> 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: graphlib.Graph<GroupNode|OpNode, Metaedge>): + {[clusterId: string]: string[]} { + let result: {[clusterId: string]: string[]} = {}; + return _.reduce(metagraph.nodes(), function(clusters: {[clusterId: string]: string[]}, n: string) { + let child = metagraph.node(n); + if (child.type === NodeType.META) { + // skip metanodes + return clusters; + } + let template = (<OpNode>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: {[clusterId: string]: string[]}, + metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>): + {[seriesName: string]: SeriesNode} { + let seriesDict: {[seriesName: string]: SeriesNode} = {}; + _.each(clusters, function(members, clusterId: string) { + 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. + */ + let 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: string) { + let isGroup = name.charAt(name.length - 1) === "*"; + let namepath = name.split("/"); + let leaf = namepath[namepath.length - 1]; + let parent = namepath.slice(0, namepath.length - 1).join("/"); + let matches = leaf.match(/^(\D*)_(\d+)$/); + + let prefix; + let id; + let suffix = ""; + if (matches) { // if found "<number>" in the name, assign id. + prefix = matches[1]; // the front non-numeric characters + id = matches[2]; // the digits + } else { // for node without "_<number>", make them zero-th items. + prefix = isGroup ? leaf.substr(0, leaf.length - 1) : leaf; + if (prefix.charAt(prefix.length - 1) !== "_") { + prefix += "_"; + } + id = 0; + suffix = isGroup ? "*" : ""; + } + let seriesName = getSeriesNodeName(prefix, suffix, parent); + candidatesDict[seriesName] = candidatesDict[seriesName] || []; + let seriesNode = 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: SeriesNode[], 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. + let seriesNodes = [seriesInfoArray[0]]; + for (let index = 1; index < seriesInfoArray.length; index++) { + let 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: SeriesNode[], + seriesDict: {[seriesName: string] : SeriesNode}, + clusterId: number, + metagraph: graphlib.Graph<GroupNode|OpNode, Metaedge>) { + if (seriesNodes.length > 1) { + let curSeriesName = getSeriesNodeName( + seriesNodes[0].prefix, seriesNodes[0].suffix, + seriesNodes[0].parent, seriesNodes[0].clusterId, + seriesNodes[seriesNodes.length - 1].clusterId); + let curSeriesNode = 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; + } +} + +} // close module tf.graph.hierarchy diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts new file mode 100644 index 0000000000..4eb3cab011 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/layout.ts @@ -0,0 +1,628 @@ +/// <reference path="graph.ts" /> +/// <reference path="render.ts" /> + +module tf.graph.layout { + +/** Set of parameters that define the look and feel of the graph. */ +export const PARAMS = { + animation: { + /** Default duration for graph animations in ms. */ + duration: 250 + }, + graph: { + /** Graph parameter for metanode. */ + meta: { + /** + * Dagre's nodesep param - number of pixels that + * separate nodes horizontally in the layout. + * + * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout + */ + nodeSep: 110, + /** + * Dagre's ranksep param - number of pixels + * between each rank in the layout. + * + * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout + */ + rankSep: 25 + }, + /** Graph parameter for metanode. */ + series: { + /** + * Dagre's nodesep param - number of pixels that + * separate nodes horizontally in the layout. + * + * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout + */ + nodeSep: 90, + /** + * Dagre's ranksep param - number of pixels + * between each rank in the layout. + * + * See https://github.com/cpettitt/dagre/wiki#configuring-the-layout + */ + rankSep: 25, + }, + /** + * Padding is used to correctly position the graph SVG inside of its parent + * element. The padding amounts are applied using an SVG transform of X and + * Y coordinates. + */ + padding: { + paddingTop: 40, + paddingLeft: 20 + } + }, + subscene: { + meta: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 10, + /** + * Used to leave room for the label on top of the highest node in + * the core graph. + */ + labelHeight: 20, + /** X-space between each extracted node and the core graph. */ + extractXOffset: 50, + /** Y-space between each extracted node. */ + extractYOffset: 20 + }, + series: { + paddingTop: 10, + paddingBottom: 10, + paddingLeft: 10, + paddingRight: 10, + labelHeight: 10 + } + }, + nodeSize: { + /** Size of meta nodes. */ + meta: { + radius: 5, + width: 60, + /** A scale for the node's height based on number of nodes inside */ + height: d3.scale.linear().domain([1, 200]).range([15, 60]).clamp(true), + /** The radius of the circle denoting the expand button. */ + expandButtonRadius: 3 + }, + /** Size of op nodes. */ + op: { + width: 15, + height: 6, + radius: 3, // for making annotation touching ellipse + labelOffset: -8 + }, + /** Size of series nodes. */ + series: { + expanded: { + // For expanded series nodes, width and height will be + // computed to account for the subscene. + radius: 10, + labelOffset: 0, + }, + vertical: { + // When unexpanded, series whose underlying metagraphs contain + // one or more non-control edges will show as a vertical stack + // of ellipses. + width: 16, + height: 13, + labelOffset: -13, + }, + horizontal: { + // When unexpanded, series whose underlying metagraphs contain + // no non-control edges will show as a horizontal stack of + // ellipses. + width: 24, + height: 8, + radius: 10, // Forces annotations to center line. + labelOffset: -10, + }, + }, + /** Size of bridge nodes. */ + bridge: { + // NOTE: bridge nodes will normally be invisible, but they must + // take up some space so that the layout step leaves room for + // their edges. + width: 20, + height: 20, + radius: 2, + labelOffset: 0 + } + }, + shortcutSize: { + /** Size of shortcuts for op nodes */ + op: { + width: 10, + height: 4 + }, + /** Size of shortcuts for meta nodes */ + meta: { + width: 12, + height: 4, + radius: 1 + }, + /** Size of shortcuts for series nodes */ + series: { + width: 14, + height: 4, + } + }, + annotations: { + /** X-space between the shape and each annotation-node. */ + xOffset: 10, + /** Y-space between each annotation-node. */ + yOffset: 3, + /** X-space between each annotation-node and its label. */ + labelOffset: 2, + /** Estimate max width for annotation label */ + labelWidth: 35 + }, + constant: { + size: { + width: 4, + height: 4 + } + }, + series: { + /** Maximum number of repeated item for unexpanded series node. */ + maxStackCount: 3, + /** + * Positioning offset ratio for collapsed stack + * of parallel series (series without edges between its members). + */ + parallelStackOffsetRatio: 0.2, + /** + * Positioning offset ratio for collapsed stack + * of tower series (series with edges between its members). + */ + towerStackOffsetRatio: 0.5 + }, + minimap : { + /** The maximum width/height the minimap can have. */ + size: 150 + } +}; + +/** Calculate layout for a scene of a group node. */ +export function scene(renderNodeInfo: render.RenderGroupNodeInformation) + : void { + // Update layout, size, and annotations of its children nodes and edges. + if (renderNodeInfo.node.isGroupNode) { + layoutChildren(renderNodeInfo); + } + + // Update position of its children nodes and edges + if (renderNodeInfo.node.type === NodeType.META) { + layoutMetanode(renderNodeInfo); + } else if (renderNodeInfo.node.type === NodeType.SERIES) { + layoutSeriesNode(renderNodeInfo); + } +}; + +/** + * Update layout, size, and annotations of its children nodes and edges. + */ +function layoutChildren(renderNodeInfo: render.RenderGroupNodeInformation) + : void { + let children = renderNodeInfo.coreGraph.nodes().map(n => { + return renderNodeInfo.coreGraph.node(n); + }).concat(renderNodeInfo.isolatedInExtract, + renderNodeInfo.isolatedOutExtract); + + _.each(children, childNodeInfo => { + // Set size of each child + switch (childNodeInfo.node.type) { + case NodeType.OP: + _.extend(childNodeInfo, PARAMS.nodeSize.op); + break; + case NodeType.BRIDGE: + _.extend(childNodeInfo, PARAMS.nodeSize.bridge); + break; + case NodeType.META: + if (!childNodeInfo.expanded) { + // set fixed width and scalable height based on cardinality + _.extend(childNodeInfo, PARAMS.nodeSize.meta); + childNodeInfo.height = + PARAMS.nodeSize.meta.height(childNodeInfo.node.cardinality); + } else { + let childGroupNodeInfo = + <render.RenderGroupNodeInformation>childNodeInfo; + scene(childGroupNodeInfo); // Recursively layout its subscene. + } + break; + case NodeType.SERIES: + if (childNodeInfo.expanded) { + _.extend(childNodeInfo, PARAMS.nodeSize.series.expanded); + let childGroupNodeInfo = + <render.RenderGroupNodeInformation>childNodeInfo; + scene(childGroupNodeInfo); // Recursively layout its subscene. + } else { + let childGroupNodeInfo = + <render.RenderGroupNodeInformation>childNodeInfo; + let seriesParams = + childGroupNodeInfo.node.hasNonControlEdges ? + PARAMS.nodeSize.series.vertical : + PARAMS.nodeSize.series.horizontal; + _.extend(childNodeInfo, seriesParams); + } + break; + default: + throw Error("Unrecognized node type: " + childNodeInfo.node.type); + } + + // Layout each child's annotations + layoutAnnotation(childNodeInfo); + }); +} + +/** + * Calculate layout for a graph using dagre + * @param graph the graph to be laid out + * @param params layout parameters + * @return width and height of the core graph + */ +function dagreLayout(graph: graphlib.Graph<any, any>, params) + : {height: number, width: number} { + _.extend(graph.graph(), { + nodeSep: params.nodeSep, + rankSep: params.rankSep + }); + + let bridgeNodeNames = []; + let nonBridgeNodeNames = []; + + // Split out nodes into bridge and non-bridge nodes, and calculate the total + // width we should use for bridge nodes. + _.each(graph.nodes(), nodeName => { + let nodeInfo = graph.node(nodeName); + if (nodeInfo.node.type === NodeType.BRIDGE) { + bridgeNodeNames.push(nodeName); + } else { + nonBridgeNodeNames.push(nodeName); + } + }); + + // If there are no non-bridge nodes, then the graph has zero size. + if (!nonBridgeNodeNames.length) { + return { + width: 0, + height: 0, + }; + } + + dagre.layout(graph); + + let graphLabel = graph.graph(); + + // Calculate the true bounding box of the graph by iterating over nodes and + // edges rather than accepting dagre's word for it. In particular, we should + // ignore the extra-wide bridge nodes and bridge edges, and allow for + // annotation boxes and labels. + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + _.each(nonBridgeNodeNames, nodeName => { + let nodeInfo = graph.node(nodeName); + let w = 0.5 * nodeInfo.width; + let x1 = nodeInfo.x - w - nodeInfo.inboxWidth; + let x2 = nodeInfo.x + w + nodeInfo.outboxWidth; + minX = x1 < minX ? x1 : minX; + maxX = x2 > maxX ? x2 : maxX; + let labelLength = + nodeName.length - nodeName.lastIndexOf(NAMESPACE_DELIM); + // TODO(jimbo): Account for font width rather than using a magic number. + let charWidth = 3; // 3 pixels per character. + let lw = 0.5 * labelLength * charWidth; + let lx1 = nodeInfo.x - lw; + let lx2 = nodeInfo.x + lw; + minX = lx1 < minX ? lx1 : minX; + maxX = lx2 > maxX ? lx2 : maxX; + // TODO(jimbo): Account for the height of labels above op nodes here. + let h = 0.5 * nodeInfo.outerHeight; + let y1 = nodeInfo.y - h; + let y2 = nodeInfo.y + h; + minY = y1 < minY ? y1 : minY; + maxY = y2 > maxY ? y2 : maxY; + }); + _.each(graph.edges(), edgeObj => { + let renderMetaedgeInfo = graph.edge(edgeObj); + if (renderMetaedgeInfo.structural) { + return; // Skip structural edges from min/max calculations. + } + _.each(renderMetaedgeInfo.points, + (point: { x: number, y: number }) => { + minX = point.x < minX ? point.x : minX; + maxX = point.x > maxX ? point.x : maxX; + minY = point.y < minY ? point.y : minY; + maxY = point.y > maxY ? point.y : maxY; + }); + }); + + // Shift all nodes and edge points to account for the left-padding amount, + // and the invisble bridge nodes. + _.each(graph.nodes(), nodeName => { + let nodeInfo = graph.node(nodeName); + nodeInfo.x -= minX; + nodeInfo.y -= minY; + }); + _.each(graph.edges(), edgeObj => { + _.each(graph.edge(edgeObj).points, + (point: { x: number, y: number }) => { + point.x -= minX; + point.y -= minY; + }); + }); + + return { + width: maxX - minX, + height: maxY - minY, + }; +} + +/** Layout a metanode. */ +function layoutMetanode(renderNodeInfo): void { + // First, copy params specific to meta nodes onto this render info object. + let params = PARAMS.subscene.meta; + renderNodeInfo = _.extend(renderNodeInfo, params); + + // Invoke dagre.layout() on the core graph and record the bounding box + // dimensions. + _.extend(renderNodeInfo.coreBox, + dagreLayout(renderNodeInfo.coreGraph, PARAMS.graph.meta)); + + // Calculate the position of nodes in isolatedInExtract relative to the + // top-left corner of inExtractBox (the bounding box for all inExtract nodes) + // and calculate the size of the inExtractBox. + let hasInExtract = renderNodeInfo.isolatedInExtract.length > 0; + + renderNodeInfo.inExtractBox.width = hasInExtract ? + _(renderNodeInfo.isolatedInExtract).pluck("outerWidth").max() : 0; + + renderNodeInfo.inExtractBox.height = + _.reduce(renderNodeInfo.isolatedInExtract, (height, child: any, i) => { + let yOffset = i > 0 ? params.extractYOffset : 0; + // use outerWidth/Height here to avoid overlaps between extracts + child.x = renderNodeInfo.inExtractBox.width / 2; + child.y = height + yOffset + child.outerHeight / 2; + return height + yOffset + child.outerHeight; + }, 0); + + // Calculate the position of nodes in isolatedOutExtract relative to the + // top-left corner of outExtractBox (the bounding box for all outExtract + // nodes) and calculate the size of the outExtractBox. + let hasOutExtract = renderNodeInfo.isolatedOutExtract.length > 0; + renderNodeInfo.outExtractBox.width = hasOutExtract ? + _(renderNodeInfo.isolatedOutExtract).pluck("outerWidth").max() : 0; + + renderNodeInfo.outExtractBox.height = + _.reduce(renderNodeInfo.isolatedOutExtract, (height, child: any, i) => { + let yOffset = i > 0 ? params.extractYOffset : 0; + // use outerWidth/Height here to avoid overlaps between extracts + child.x = renderNodeInfo.outExtractBox.width / 2; + child.y = height + yOffset + child.outerHeight / 2; + return height + yOffset + child.outerHeight; + }, 0); + + // Determine the whole metanode's width (from left to right). + renderNodeInfo.width = + params.paddingLeft + renderNodeInfo.coreBox.width + params.paddingRight + + (hasInExtract ? + renderNodeInfo.inExtractBox.width + params.extractXOffset : 0) + + (hasOutExtract ? + params.extractXOffset + renderNodeInfo.outExtractBox.width : 0); + + // TODO(jimbo): Remove labelHeight and instead incorporate into box sizes. + // Determine the whole metanode's height (from top to bottom). + renderNodeInfo.height = + renderNodeInfo.labelHeight + + params.paddingTop + + Math.max( + renderNodeInfo.inExtractBox.height, + renderNodeInfo.coreBox.height, + renderNodeInfo.outExtractBox.height + ) + + params.paddingBottom; +} + +/** + * Calculate layout for series node's core graph. Only called for an expanded + * series. + */ +function layoutSeriesNode(node: render.RenderGroupNodeInformation): void { + let graph = node.coreGraph; + + let params = PARAMS.subscene.series; + _.extend(node, params); + + // Layout the core. + _.extend(node.coreBox, + dagreLayout(node.coreGraph, PARAMS.graph.series)); + + _.each(graph.nodes(), nodeName => { + graph.node(nodeName).excluded = false; + }); + + // Series do not have in/outExtractBox so no need to include them here. + node.width = node.coreBox.width + params.paddingLeft + params.paddingRight; + node.height = node.coreBox.height + params.paddingTop + params.paddingBottom; +} + +/** + * Calculate layout for annotations of a given node. + * This will modify positions of the the given node and its annotations. + * + * @see tf.graph.render.Node and tf.graph.render.Annotation + * for description of each property of each render node. + * + */ + function layoutAnnotation(renderNodeInfo: render.RenderNodeInformation): void { + // If the render node is an expanded metanode, then its annotations will not + // be visible and we should skip the annotation calculations. + if (renderNodeInfo.expanded) { + _.extend(renderNodeInfo, { + inboxWidth: 0, + inboxHeight: 0, + outboxWidth: 0, + outboxHeight: 0, + outerWidth: renderNodeInfo.width, + outerHeight: renderNodeInfo.height + }); + return; + } + + let inAnnotations = renderNodeInfo.inAnnotations.list; + let outAnnotations = renderNodeInfo.outAnnotations.list; + + // Calculate size for in-annotations + _.each(inAnnotations, a => sizeAnnotation(a)); + + // Calculate size for out-annotations + _.each(outAnnotations, a => sizeAnnotation(a)); + + let params = PARAMS.annotations; + renderNodeInfo.inboxWidth = + inAnnotations.length > 0 ? + (<any>_(inAnnotations).pluck("width").max()) + + params.xOffset + params.labelWidth + params.labelOffset : + 0; + + renderNodeInfo.outboxWidth = + outAnnotations.length > 0 ? + (<any>_(outAnnotations).pluck("width").max()) + + params.xOffset + params.labelWidth + params.labelOffset : + 0; + + // Calculate annotation node position (a.dx, a.dy) + // and total height for in-annotations + // After this chunk of code: + // inboxHeight = sum of annotation heights+ (annotation.length - 1 * yOffset) + let inboxHeight = _.reduce(inAnnotations, + (height, a: any, i) => { + let yOffset = i > 0 ? params.yOffset : 0; + a.dx = -(renderNodeInfo.width + a.width) / 2 - params.xOffset; + a.dy = height + yOffset + a.height / 2; + return height + yOffset + a.height; + }, 0); + + _.each(inAnnotations, (a: any) => { + a.dy -= inboxHeight / 2; + + a.labelOffset = params.labelOffset; + }); + + // Calculate annotation node position position (a.dx, a.dy) + // and total height for out-annotations + // After this chunk of code: + // outboxHeight = sum of annotation heights + + // (annotation.length - 1 * yOffset) + let outboxHeight = _.reduce(outAnnotations, + (height, a: any, i) => { + let yOffset = i > 0 ? params.yOffset : 0; + a.dx = (renderNodeInfo.width + a.width) / 2 + params.xOffset; + a.dy = height + yOffset + a.height / 2; + return height + yOffset + a.height; + }, 0); + + _.each(outAnnotations, (a: any) => { + // adjust by (half of ) the total height + // so dy is relative to the host node's center. + a.dy -= outboxHeight / 2; + + a.labelOffset = params.labelOffset; + }); + + // Creating scales for touch point between the in-annotation edges + // and their hosts. + + let inTouchHeight = + Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, + inboxHeight / 2); + inTouchHeight = inTouchHeight < 0 ? 0 : inTouchHeight; + + let inY = d3.scale.linear() + .domain([0, inAnnotations.length - 1]) + .range([-inTouchHeight, inTouchHeight]); + + // Calculate annotation edge position + _.each(inAnnotations, (a: any, i) => { + a.points = [ + // The annotation node end + { + dx: a.dx + a.width / 2, + dy: a.dy + }, + + // The host node end + { + dx: - renderNodeInfo.width / 2, + // only use scale if there are more than one, + // otherwise center it vertically + dy: inAnnotations.length > 1 ? inY(i) : 0 + } + ]; + }); + + // Creating scales for touch point between the out-annotation edges + // and their hosts. + let outTouchHeight = + Math.min(renderNodeInfo.height / 2 - renderNodeInfo.radius, + outboxHeight / 2); + outTouchHeight = outTouchHeight < 0 ? 0 : outTouchHeight; + let outY = d3.scale.linear() + .domain([0, outAnnotations.length - 1]) + .range([-outTouchHeight, outTouchHeight]); + + _.each(outAnnotations, (a: any, i) => { + // Add point from the border of the annotation node + a.points = [ + // The host node end + { + dx: renderNodeInfo.width / 2, + // only use scale if there are more than one, + // otherwise center it vertically + dy: outAnnotations.length > 1 ? outY(i) : 0 + }, + // The annotation node end + { + dx: a.dx - a.width / 2, + dy: a.dy + } + ]; + }); + + renderNodeInfo.outerWidth = renderNodeInfo.width + renderNodeInfo.inboxWidth + + renderNodeInfo.outboxWidth; + renderNodeInfo.outerHeight = + Math.max(renderNodeInfo.height, inboxHeight, outboxHeight); +} + +/** + * Set size of an annotation node. + */ +function sizeAnnotation(a: render.Annotation): void { + switch (a.annotationType) { + case render.AnnotationType.CONSTANT: + _.extend(a, PARAMS.constant.size); + break; + case render.AnnotationType.SHORTCUT: + if (a.node.type === NodeType.OP) { + _.extend(a, PARAMS.shortcutSize.op); + } else if (a.node.type === NodeType.META) { + _.extend(a, PARAMS.shortcutSize.meta); + } else if (a.node.type === NodeType.SERIES) { + _.extend(a, PARAMS.shortcutSize.series); + } else { + throw Error("Invalid node type: " + a.node.type); + } + break; + case render.AnnotationType.SUMMARY: + _.extend(a, PARAMS.constant.size); + break; + } +} + +} // close module diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts new file mode 100644 index 0000000000..b4864738a9 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/parser.ts @@ -0,0 +1,189 @@ +/// <reference path="../../../typings/tsd.d.ts" /> +/// <reference path="common.ts" /> +module tf.graph.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: string): string|number|boolean { + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + let firstChar = value[0]; + if (firstChar === "\"") { + return value.substring(1, value.length - 1); + } + let num = parseFloat(value); + return isNaN(num) ? value : num; +} + +/** + * Fetches a text file and returns a promise of the result. + */ +export function readPbTxt(filepath: string): Promise<string> { + return new Promise<string>(function(resolve, reject) { + d3.text(filepath, function(error, text) { + if (error) { + reject(error); + return; + } + resolve(text); + }); + }); +} + +/** + * Fetches and parses a json file and returns a promise of the result. + */ +export function readJson(filepath: string): Promise<Object> { + return new Promise<Object>(function(resolve, reject) { + d3.json(filepath, function(error, text) { + if (error) { + reject(error); + return; + } + resolve(text); + }); + }); +} + +/** + * Reads the graph and stats file (if available), parses them and returns a + * promise of the result. + */ +export function readAndParseData(dataset: {path: string, statsPath: string}, + pbTxtContent: string, tracker: ProgressTracker): + Promise<{ nodes: TFNode[], statsJson: Object }|void> { + let graphPbTxt; + let statsJson; + return runAsyncTask("Reading graph.pbtxt", 20, () => { + return pbTxtContent || readPbTxt(dataset.path); + }, tracker) + .then(function(text) { + graphPbTxt = text; + return runAsyncTask("Reading stats.pbtxt", 20, () => { + return (dataset != null && dataset.statsPath != null) ? + readJson(dataset.statsPath) : null; + }, tracker); + }) + .then(function(json) { + statsJson = json; + return runAsyncTask("Parsing graph.pbtxt", 60, () => { + return parsePbtxt(graphPbTxt); + }, tracker); + }) + .then(function(nodes) { + return { + nodes: nodes, + statsJson: statsJson + }; + }) + .catch(function(reason) { + throw new Error("Failure parsing graph definition"); + }); +} + +/** + * Parses a proto txt file into a javascript object. + * + * @param input The string contents of the proto txt file. + * @return The parsed object. + */ +export function parsePbtxt(input: string): TFNode[] { + let output: { [name: string]: any; } = { node: [] }; + let stack = []; + let path: string[] = []; + let current: { [name: string]: any; } = output; + + function splitNameAndValueInAttribute(line: string) { + let colonIndex = line.indexOf(":"); + let name = line.substring(0, colonIndex).trim(); + let 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. + */ + let ARRAY_ATTRIBUTES: {[attrPath: string] : boolean} = { + "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: Object, name: string, + value: Object|string|number|boolean, path: string[]): void { + // We treat "node" specially since it is done so often. + let 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. + let startPos = 0; + while (startPos < input.length) { + let endPos = input.indexOf("\n", startPos); + if (endPos === -1) { + endPos = input.length; + } + let line = input.substring(startPos, endPos); + startPos = endPos + 1; + if (!line) { + continue; + } + switch (line[line.length - 1]) { + case "{": // create new object + let name = line.substring(0, line.length - 2).trim(); + let newValue: { [name: string]: any; } = {}; + stack.push(current); + path.push(name); + addAttribute(current, name, newValue, path); + current = newValue; + break; + case "}": + current = stack.pop(); + path.pop(); + break; + default: + let x = splitNameAndValueInAttribute(line); + addAttribute(current, x.name, x.value, path.concat(x.name)); + break; + } + } + + return output["node"]; +} + +} // Close module tf.graph.parser. diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts new file mode 100644 index 0000000000..363f006fd5 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/render.ts @@ -0,0 +1,1360 @@ +/// <reference path="graph.ts" /> +/// <reference path="hierarchy.ts" /> + +/** + * Package for the Render Hierarchy for TensorFlow graph. + */ + +module tf.graph.render { + +/** + * Color parameters for node encoding. + * @type {Object} + */ +export let 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: number, lightened? : boolean) { + // The code below is a flexible way to computationally create a set + // of colors that go well together. + let hues = MetanodeColors.HUES; + let n = hues.length; + let hue = hues[id % n]; + let m = Math.sin(hue * Math.PI / 360); + let sat = lightened ? 30 : 90 - 60 * m; + let light = lightened ? 95 : 80; + return d3.hsl(hue, .01 * sat, .01 * light).toString(); + }, + DEVICE_PALETTE: function (index: number): string { + return MetanodeColors.STRUCTURE_PALETTE(index); + }, + UNKNOWN: "#eee", + GRADIENT_OUTLINE: "#888" +}; + +/** + * Parameters that affect how the graph is rendered on the screen. + */ +interface RenderGraphParams { + /** + * Whether to extract high degree nodes from the core part of the graph. + */ + enableExtraction: boolean; + /** + * Maximum in-degree that a node can have without being considered as + * high in-degree node. + */ + maxInDegree: number; + /** + * Maximum out-degree that a node can have without being considered as + * high out-degree node. + */ + maxOutDegree: number; + /** + * Maximum number of control edges a node can have before they aren't + * displayed. + */ + maxControlDegree: number; + /** + * Types patterns for predefined out-extract nodes, which are + * sink-like nodes that will be extracted from the main graph. + */ + outExtractTypes: string[]; + + /** + * Types patterns for predefined in-extract nodes, which are + * source-like nodes that will be extracted from the main graph. + */ + inExtractTypes: string[]; + + /** + * 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: boolean; + + /** + * 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: boolean; + + /** + * Whether to add bridge nodes and edges to the core when building the + * subhierarchy of an expanded metanode. See buildSubhierarchy(). + */ + enableBridgegraph: boolean; + + /** + * 2 colors, for the minimum and maximum value respectively, whenever we + * have a gradient scale. + */ + minMaxColors: string[]; + + /** + * Maximum number of annotations to be displayed on a node. + */ + maxAnnotations: number; +} + +/** + * Stores the rendering information, such as x and y coordinates, + * for each node in the graph. + */ +export class RenderGraphInformation { + private hierarchy: hierarchy.Hierarchy; + private index: {[nodeName: string]: RenderNodeInformation}; + private params: RenderGraphParams; + private deviceColorMap: d3.scale.Ordinal<string, string>; + private memoryUsageScale: d3.scale.Linear<string, string>; + private computeTimeScale: d3.scale.Linear<string, string>; + // Since the rendering information for each node is constructed lazily, + // upon node's expansion by the user, we keep a map between the node's name and + // whether the rendering information was already constructed for that node. + private hasSubhierarchy: {[nodeName: string]: boolean}; + root: RenderGroupNodeInformation; + + constructor(hierarchy: hierarchy.Hierarchy, params: RenderGraphParams) { + this.hierarchy = hierarchy; + this.index = {}; + this.deviceColorMap = d3.scale.ordinal<string>() + .domain(hierarchy.devices) + .range(_.map(d3.range(hierarchy.devices.length), + MetanodeColors.DEVICE_PALETTE)); + + let topLevelGraph = hierarchy.root.metagraph; + // Find the maximum and minimum memory usage. + let memoryExtent = d3.extent(topLevelGraph.nodes(), + (nodeName, index) => { + let 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<string, string>() + .domain(memoryExtent) + .range(params.minMaxColors); + + // Find also the minimum and maximum compute time. + let computeTimeExtent = d3.extent(topLevelGraph.nodes(), (nodeName, index) => { + let 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<string, string>() + .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; + } + + getRenderNodeByName(nodeName: string): RenderNodeInformation { + 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. + */ + getNearestVisibleAncestor(name: string): string { + let path = getHierarchicalPath(name); + for (let i = 0; i < path.length; i++) { + let 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). + setDepth(depth: number): void { + setGroupNodeDepth(this.root, +depth); + } + + buildSubhierarchy(nodeName: string): void { + // Terminate if the rendering hierarchy was already constructed + // for this node. + if (nodeName in this.hasSubhierarchy) { + return; + } + + let renderNodeInfo = this.index[nodeName]; + + // If it is not a meta node or a series node, don't do anything. + if (renderNodeInfo.node.type !== NodeType.META && + renderNodeInfo.node.type !== NodeType.SERIES) { + return; + } + + // At this point we know the rendering information is about a group node. + let renderGroupNodeInfo = <RenderGroupNodeInformation> renderNodeInfo; + let metagraph = renderGroupNodeInfo.node.metagraph; + let 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(), childName => { + + let childNode = metagraph.node(childName); + let childRenderInfo = childNode.isGroupNode ? + new RenderGroupNodeInformation(<GroupNode>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((<OpNode>childNode).inEmbeddings, embedding => { + let renderMetaedgeInfo = new RenderMetaedgeInformation(null); + addInAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, + AnnotationType.CONSTANT, this.params); + this.index[embedding.name] = new RenderNodeInformation(embedding); + }); + _.each((<OpNode>childNode).outEmbeddings, embedding => { + let renderMetaedgeInfo = new RenderMetaedgeInformation(null); + addOutAnnotation(childRenderInfo, embedding, null, renderMetaedgeInfo, + AnnotationType.SUMMARY, this.params); + this.index[embedding.name] = new RenderNodeInformation(embedding); + }); + let device = (<OpNode>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. + let pairs = _.pairs((<GroupNode> childNode).deviceHistogram); + if (pairs.length > 0) { + // Compute the total # of devices. + let numDevices = _.sum(pairs, _.last); + childRenderInfo.deviceColors = _.map(pairs, 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(), edgeObj => { + let metaedge = metagraph.edge(edgeObj); + let renderMetaedgeInfo = new RenderMetaedgeInformation(metaedge); + coreGraph.setEdge(edgeObj.v, edgeObj.w, renderMetaedgeInfo); + }); + + if (this.params.enableExtraction && + renderGroupNodeInfo.node.type === 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. + let parentNode = renderGroupNodeInfo.node.parentNode; + if (!parentNode) { + return; + } + let parentNodeInfo = + <RenderGroupNodeInformation> this.index[parentNode.name]; + + // Utility function for computing the name of a bridge node. + let getBridgeNodeName = (inbound, ...rest) => + rest.concat([inbound ? "IN" : "OUT"]).join("~~"); + + // Build out the bridgegraph. + let bridgegraph = this.hierarchy.getBridgegraph(nodeName); + + // Look for popular nodes so we can make annotations instead of paths. + let otherCounts = { + // Counts of edges coming INTO other nodes by name (outgoing from self). + in: <{[nodeName: string]: number}> {}, + // Counts of edges going OUT from other nodes by name (coming into self). + out: <{[nodeName: string]: number}> {}, + // Counts of all control edges involving other nodes by name. + control: <{[nodeName: string]: number}> {}, + }; + _.each(bridgegraph.edges(), e => { + // An edge is inbound if its destination node is in the metagraph. + let inbound = !!metagraph.node(e.w); + let otherName = inbound ? e.v : e.w; + let 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. + let hierarchyNodeMap = this.hierarchy.getNodeMap(); + _.each(bridgegraph.edges(), bridgeEdgeObj => { + let bridgeMetaedge = bridgegraph.edge(bridgeEdgeObj); + + // Determine whether this bridge edge is incoming by checking the + // metagraph for a node that matches the destination end. + let 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). + let [childName, otherName] = + inbound ? + [bridgeEdgeObj.w, bridgeEdgeObj.v] : + [bridgeEdgeObj.v, bridgeEdgeObj.w]; + + let childRenderInfo = this.index[childName]; + let otherRenderInfo = this.index[otherName]; + let 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. + let isHighDegreeControlEdge = !bridgeMetaedge.numRegularEdges && + otherCounts.control[otherName] > this.params.maxControlDegree; + + let [annotations, childAnnotations] = + inbound ? + [renderNodeInfo.inAnnotations, childRenderInfo.inAnnotations] : + [renderNodeInfo.outAnnotations, childRenderInfo.outAnnotations]; + + let 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. + let 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. + let canDrawBridgePath = false; + if (this.params.enableBridgegraph && + !isOtherHighDegree && + !isHighDegreeControlEdge && + childRenderInfo.isInCore()) { + + // Utility function for finding an adjoining metaedge. + let findAdjoiningMetaedge = targetName => { + let adjoiningEdgeObj: graphlib.EdgeObject = + inbound ? + { v: targetName, w: nodeName } : + { v: nodeName, w: targetName }; + return <RenderMetaedgeInformation> + 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. + let backwards = false; + if (adjoiningMetaedge && !bridgeMetaedge.numRegularEdges) { + // Find the top-most adjoining render metaedge information, and the + // GroupNode whose metagraph must contain the associated metaedge. + let topAdjoiningMetaedge = adjoiningMetaedge; + let topGroupNode = parentNodeInfo.node; + while (topAdjoiningMetaedge.adjoiningMetaedge) { + topAdjoiningMetaedge = topAdjoiningMetaedge.adjoiningMetaedge; + topGroupNode = <GroupNode>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. + let ordering = this.hierarchy.getTopologicalOrdering(topGroupNode.name); + let 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. + let bridgeContainerName = getBridgeNodeName(inbound, nodeName); + let bridgeNodeName = getBridgeNodeName(inbound, otherName, nodeName); + let bridgeNodeRenderInfo = coreGraph.node(bridgeNodeName); + if (!bridgeNodeRenderInfo) { + + // Find or create the directional container for the bridge node. + let bridgeContainerInfo = coreGraph.node(bridgeContainerName); + if (!bridgeContainerInfo) { + let bridgeContainerNode: BridgeNode = { + // Important node properties. + name: bridgeContainerName, + type: 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); + } + + let bridgeNode: BridgeNode = { + // Important node properties. + name: bridgeNodeName, + type: 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. + let 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], inbound => { + let bridgeContainerName = getBridgeNodeName(inbound, nodeName); + let bridgeContainerInfo = coreGraph.node(bridgeContainerName); + if (!bridgeContainerInfo) { + return; + } + _.each(coreGraph.nodes(), childName => { + // Short-circuit if this child is a bridge node or it's not a terminal + // node in the direction we're interested in. + let childNodeInfo = coreGraph.node(childName); + if (childNodeInfo.node.type === NodeType.BRIDGE) { + return; + } + let 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). + let structuralNodeName = + getBridgeNodeName(inbound, nodeName, "STRUCTURAL_TARGET"); + let structuralRenderInfo = coreGraph.node(structuralNodeName); + if (!structuralRenderInfo) { + let bridgeNode: BridgeNode = { + // Important Node properties. + name: structuralNodeName, + type: 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. + let 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); + }); + }); + } +} + +/** + * 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. + */ +export class Annotation { + node: Node; + renderNodeInfo: RenderNodeInformation; + renderMetaedgeInfo: RenderMetaedgeInformation; + annotationType: AnnotationType; + /** + * Center position of annotation relative to the host + * node's center x. + */ + dx: number; + /** + * Center position of annotation relative to the host + * node's center y. + */ + dy: number; + width: number; + height: number; + /** + * A flag whether it is an in-annotation (if true) or + * out-annotation (if false). + */ + isIn: boolean; + /** Label horizontal offset from the end of the node shape */ + labelOffset: number; + /** + * Array of points for edges from the annotation to its host + * node. Each point contains the point location, relative to + * the host node's center. + */ + points: {dx: number, dy: number}[]; + + /** + * 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. + */ + constructor(node: Node, renderNodeInfo: RenderNodeInformation, + renderMetaedgeInfo: RenderMetaedgeInformation, type: AnnotationType, + isIn: boolean) { + 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 = []; + } +}; + +export enum AnnotationType {SHORTCUT, CONSTANT, SUMMARY, ELLIPSIS}; + +/** + * Manages a list of annotations. Two will be used for each + * RenderNodeInformation, one for in annotations and one for out annotations. + */ +export class AnnotationList { + /** + * List of visually drawable annotations, may include an ellipses annotation + * if the number added exceeds the number specified by maxAnnotations. + */ + list: Annotation[]; + + /** + * Set of nodes which have been added as annotations to this list, so we can + * prevent duplicates. + */ + nodeNames: { [nodeName: string]: boolean }; + + constructor() { + this.list = []; + this.nodeNames = {}; + } + + /** + * Append an annotation to the list, or a stand-in ellipsis annotation instead + * if this would make it too many. + */ + push(annotation: Annotation, params: RenderGraphParams): void { + 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; + } + + let lastAnnotation = this.list[this.list.length - 1]; + if (lastAnnotation.annotationType === AnnotationType.ELLIPSIS) { + let ellipsisNode = <EllipsisNode>lastAnnotation.node; + ellipsisNode.setNumMoreNodes(++ellipsisNode.numMoreNodes); + return; + } + + let ellipsisNode = new tf.graph.EllipsisNodeImpl(1); + this.list.push(new Annotation(ellipsisNode, + new RenderNodeInformation(ellipsisNode), null, + AnnotationType.ELLIPSIS, annotation.isIn)); + } +} + +/** + * Contains rendering information about a node in the hierarchical graph. + */ +export class RenderNodeInformation { + /** Reference to the original underlying Node from the hierarchical graph. */ + node: Node; + /** Whether the node is expanded or not. */ + expanded: boolean; + /** + * List of rendering information about in-annotations like constants and + * shortcuts to high-degree nodes. + */ + inAnnotations: AnnotationList; + /** List of rendering information about out-annotations (e.g. summary nodes) */ + outAnnotations: AnnotationList; + + // --- Params specified by layout --- // + + /** Center x position */ + x: number; + /** Center y position */ + y: number; + /** Width of the node's shape */ + width: number; + /** Height of the node's shape. */ + height: number; + /** Width of the bounding box for all in-annotations. */ + inboxWidth: number; + /** Width of the bounding box for all out-annotations. */ + outboxWidth: number; + /** + * Whether the node should be excluded from the scene. + * This is only used when there are too many items in a series so we only + * want to include top N ones. + */ + // TODO(jimbo): Now that series rendering is non-recursive, remove this and + // all its uses from the code base. + excluded: boolean; + + // --- Params used in drawing the bridge paths --- // + + /** + * All bridge nodes are meant to be invisible, but whereas most represent a + * relationship from the underlying graph hierarchy, some exist solely for + * layout reasons. Specifically, those bridge nodes which have only structural + * rendering metaedges. + */ + structural: boolean; + + // --- Params for the size of the node box --- // + + /** Label vertical offset from the center of node shape */ + labelOffset: number; + /** X-space between each extracted node and the core graph. */ + extractXOffset: number; + /** Rectangle radius (for making rounded rectangle) */ + radius: number; + + // --- Params for expanded node --- // + + /** Label height for expanded node. */ + labelHeight: number; + // Paddings between inner subscene and the border of the expanded node. + paddingTop: number; + paddingLeft: number; + paddingRight: number; + paddingBottom: number; + + /** Width of the whole node including its shape and its annotations */ + outerWidth: number; + /** Height of the whole node including its shape and its annotations */ + outerHeight: number; + /** + * Whether a node is extracted as source-like (having high out-degree or matching + * predefined in-extract pattern.) + */ + isInExtract: boolean; + /** + * Whether a node is extracted as sink-like (having high in-degree or matching + * predefined out-extract pattern.) + */ + isOutExtract: boolean; + + /** + * List of (color, proportion) tuples based on the proportion of devices of + * its children. If this node is an op node, this list will have only one + * color with proportion 1.0. + */ + deviceColors: {color: string, proportion: number}[]; + + /** + * Color according to the memory usage of this node. + */ + memoryColor: string; + + /** + * Color according to the compute time of this node. + */ + computeTimeColor: string; + + constructor(node: 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; + } + + isInCore(): boolean { + return !this.isInExtract && !this.isOutExtract; + } +} + +/** + * Contains rendering information about a Metaedge from the underlying + * hierarchical graph. It may be from either a metagraph or a bridgegraph. + */ +export class RenderMetaedgeInformation { + /** + * Reference to the original underlying Metaedge from the hierarchical graph, + * if any. This will be null for the edges which connect OpNodes to their + * embeddings, for example. + */ + metaedge: Metaedge; + + /** + * Reference to the adjoining RenderMeteaedgeInformation from the parent's + * coreGraph. This is used during layout to determine the point at which this + * edge should touch the node's bounding box. This property will be null for + * edges which terminate at a node on both ends (all non-bridge edges). + */ + adjoiningMetaedge: RenderMetaedgeInformation; + + /** + * Most of the time, a RenderMetaedgeInformation object represents a real + * edge between nodes in the underlying graph structure. But sometimes, an + * edge only exsts for layout purposes. These structural edges are added + * during buildSubhierarchy() to force dagre.layout() to put bridge nodes + * at the ends of the flow. + * @see buildSubhierarchy() + */ + structural: boolean; + + /** + * Weight of the edge, used by dagre when deciding how important an edge is. + * Edges with higher weight are made shorter and straighter. The default + * dagre uses is 1. + */ + weight: number; + + /** + * X and Y coordinate pairs of the points in the path of the edge. + * @see tf.graph.node.subsceneAdjustPaths + */ + points: any[]; + + /** + * D3 selection of the group containing the path that displays this edge. + */ + edgeGroup: d3.Selection<RenderMetaedgeInformation>; + + constructor(metaedge: Metaedge) { + this.metaedge = metaedge; + this.adjoiningMetaedge = null; + this.structural = false; + this.weight = 1; + } +} + +function addInAnnotation(node: RenderNodeInformation, predecessor: Node, + predecessorRenderInfo: RenderNodeInformation, edge: any, + type: AnnotationType, params: RenderGraphParams): void { + let annotation = new Annotation(predecessor, predecessorRenderInfo, edge, + type, true); + node.inAnnotations.push(annotation, params); +} + +function addOutAnnotation(node: RenderNodeInformation, successor: Node, + successorRenderInfo: RenderNodeInformation, edge: any, + type: AnnotationType, params: RenderGraphParams): void { + let annotation = new Annotation(successor, successorRenderInfo, edge, + type, false); + node.outAnnotations.push(annotation, params); +} + +function setGraphDepth(graph: graphlib.Graph<RenderNodeInformation, any>, + depth: number) { + _.each(graph.nodes(), nodeName => { + let child = graph.node(nodeName); + child.expanded = depth > 1; // set all child of depth 1 to collapsed + if (depth > 0) { + switch (child.node.type) { + case NodeType.META: + case NodeType.SERIES: + setGroupNodeDepth(<RenderGroupNodeInformation>child, depth - 1); + break; + // Do nothing for leaf + } + } + }); +}; + +export class RenderGroupNodeInformation extends RenderNodeInformation { + node: GroupNode; + /** + * The core graph is derived from the underlying node's metagraph, minus + * the extracted source-like and sink-like nodes. + */ + coreGraph: graphlib.Graph<RenderNodeInformation, RenderMetaedgeInformation>; + /** Size of the bounding box for a metanode's core graph. */ + coreBox: { + width: number, + height: number, + }; + /** Size of the bounding box for a metanode's isolated in-extract children. */ + inExtractBox: {width: number, height: number}; + /** Size of the bounding box for a metanode's isolated out-extract children. */ + outExtractBox: {width: number, height: number}; + /** Array of isolated in-extract nodes. */ + isolatedInExtract: RenderNodeInformation[]; + /** Array of isolated out-extract nodes. */ + isolatedOutExtract: RenderNodeInformation[]; + + constructor(groupNode: GroupNode) { + super(groupNode); + let metagraph = groupNode.metagraph; + let gl = metagraph.graph(); + this.coreGraph = + createGraph<RenderNodeInformation, RenderMetaedgeInformation>( + gl.name, 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 = []; + } +} + +function setGroupNodeDepth(renderInfo: RenderGroupNodeInformation, + depth: number): void { + 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: graphlib.Graph<RenderNodeInformation, {}>, + v: string, w: string, params: RenderGraphParams) { + let src = graph.node(v); + let sink = graph.node(w); + let 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: RenderGroupNodeInformation, n: string, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + graph.node(n).isOutExtract = true; + + _.each(graph.predecessors(n), (p, index) => { + createShortcut(graph, p, n, params); + }); + + if (params.detachAllEdgesForHighDegree) { + _.each(graph.successors(n), (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: RenderGroupNodeInformation, n: string, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + graph.node(n).isInExtract = true; + + _.each(graph.successors(n), (s, index) => { + createShortcut(graph, n, s, params); + }); + + if (params.detachAllEdgesForHighDegree) { + _.each(graph.predecessors(n), (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: Node, types: string[]): boolean { + if (node.type === NodeType.OP) { + for (let i = 0; i < types.length; i++) { + if ((<OpNode>node).op === types[i]) { return true; } + } + } else if (node.type === NodeType.META) { + let rootOpNode = (<Metanode>node).getRootOp(); + if (rootOpNode) { + for (let 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: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + _.each(graph.nodes(), n => { + let 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: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + _.each(graph.nodes(), n => { + let renderInfo = graph.node(n); + if (hasTypeIn(renderInfo.node, params.inExtractTypes)) { + makeInExtract(renderNode, n, params); + } + }); +} + +/** Extract from nodes with in-degree > maxInDegree */ +function extractHighInDegree(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + let maxInDegree = params.maxInDegree; + + // detect first so degrees don't get affected by other removal + let highInDegreeNames = _.filter(graph.nodes(), 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. + let numEdgesToCount = _.reduce(graph.predecessors(n), (numEdgesToCount, pred) => { + let 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, n => { + makeOutExtract(renderNode, n, params); + }); +} + +/** Extract nodes with out-degree > maxOutDegree */ +function extractHighOutDegree(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + let maxOutDegree = params.maxOutDegree; + + // detect first so degrees don't get affected by other removal + let highOutDegreeNames = _.filter(graph.nodes(), 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. + let numEdgesToCount = _.reduce(graph.successors(n), (numEdgesToCount, succ) => { + let 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, n => { + makeInExtract(renderNode, n, params); + }); +} + +/** Remove control edges from nodes that have too many control edges */ +function removeControlEdges(renderNode: RenderGroupNodeInformation, + params: RenderGraphParams) { + let graph = renderNode.coreGraph; + + // Collect control edges into a map by node name. + let map = <{[nodeName: string]: graphlib.EdgeObject[]}>{}; + _.each(graph.edges(), 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, (edges, nodeName) => { + if (edges.length > params.maxControlDegree) { + _.each(edges, e => 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 + */ +export function mapIndexToHue(id: number): number { + let GOLDEN_RATIO = 1.61803398875; + // Hue of 0 is reserved for the gray nodes. + let MIN_HUE = 1; + let MAX_HUE = 359; + let COLOR_RANGE = MAX_HUE - MIN_HUE; + return MIN_HUE + ((COLOR_RANGE * GOLDEN_RATIO * id) % COLOR_RANGE); +}; + +/** + * 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: RenderGroupNodeInformation, + params: RenderGraphParams) { + 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. + + let graph = renderNode.coreGraph; + _.each(graph.nodes(), n => { + let child = graph.node(n); + let degree = graph.neighbors(n).length; + + if (degree === 0) { + let hasOutAnnotations = child.outAnnotations.list.length > 0; + let hasInAnnotations = child.inAnnotations.list.length > 0; + + if (child.isInExtract) { // Is source-like. + // 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) { // Is sink-like. + // 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 { + // if a low degree node has both in- & out- annotations, do nothing + // because it is unclear which side it should go to. + } + } + } + }); +} +} // close module tf.graph.render diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts new file mode 100644 index 0000000000..82609e8652 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/annotation.ts @@ -0,0 +1,223 @@ +/// <reference path="../graph.ts" /> +/// <reference path="../render.ts" /> +/// <reference path="scene.ts" /> +/// <reference path="edge.ts" /> + +module tf.graph.scene.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 + */ +export function buildGroup(container, annotationData: render.AnnotationList, + d: render.RenderNodeInformation, sceneBehavior) { + // Select all children and join with data. + let 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, d => { return d.node.name; }); + + annotationGroups.enter() + .append("g") + .attr("data-name", a => { return a.node.name; }) + .each(function(a) { + let aGroup = d3.select(this); + + // Add annotation to the index in the scene + sceneBehavior.addAnnotationGroup(a, d, aGroup); + // Append annotation edge + let edgeType = Class.Annotation.EDGE; + let metaedge = a.renderMetaedgeInfo && a.renderMetaedgeInfo.metaedge; + if (metaedge && !metaedge.numRegularEdges) { + edgeType += " " + Class.Annotation.CONTROL_EDGE; + } + // If any edges are reference edges, add the reference edge class. + if (metaedge && metaedge.numRefEdges) { + edgeType += " " + Class.Edge.REF_LINE; + } + 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, Class.Annotation.ELLIPSIS); + } + }); + + annotationGroups + .attr("class", a => { + return Class.Annotation.GROUP + " " + + annotationToClassName(a.annotationType) + + " " + node.nodeClass(a); + }) + .each(function(a) { + let 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) { + let aGroup = d3.select(this); + + // Remove annotation from the index in the scene + sceneBehavior.removeAnnotationGroup(a, d, aGroup); + }) + .remove(); + return annotationGroups; +}; + +/** + * Maps an annotation enum to a class name used in css rules. + */ +function annotationToClassName(annotationType: render.AnnotationType) { + return (tf.graph.render.AnnotationType[annotationType] || "") + .toLowerCase() || null; +} + +function buildShape(aGroup, a: render.Annotation, sceneBehavior) { + if (a.annotationType === tf.graph.render.AnnotationType.SUMMARY) { + let image = scene.selectOrCreateChild(aGroup, "image"); + image.attr({ + "xlink:href": sceneBehavior.resolveUrl("../../lib/svg/summary-icon.svg"), + "height": "12px", + "width": "12px", + "cursor": "pointer" + }); + } else { + let shape = node.buildShape(aGroup, a, Class.Annotation.NODE); + // add title tag to get native tooltips + scene.selectOrCreateChild(shape, "title").text(a.node.name); + } +} + +function addAnnotationLabelFromNode(aGroup, a: render.Annotation) { + let namePath = a.node.name.split("/"); + let text = namePath[namePath.length - 1]; + let shortenedText = text.length > 8 ? text.substring(0, 8) + "..." : text; + return addAnnotationLabel(aGroup, shortenedText, a, null, text); +} + +function addAnnotationLabel(aGroup, label, a, additionalClassNames, + fullLabel?) { + let classNames = Class.Annotation.LABEL; + if (additionalClassNames) { + classNames += " " + additionalClassNames; + } + let 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: render.RenderNodeInformation, + sceneBehavior) { + selection + .on("mouseover", a => { + sceneBehavior.fire("annotation-highlight", { + name: a.node.name, + hostName: d.node.name + }); + }) + .on("mouseout", a => { + sceneBehavior.fire("annotation-unhighlight", { + name: a.node.name, + hostName: d.node.name + }); + }) + .on("click", a => { + // Stop this event"s propagation so that it isn't also considered a + // graph-select. + (<Event>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: render.RenderNodeInformation, a: render.Annotation, + 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) { + node.stylize(aGroup, a.renderNodeInfo, sceneBehavior, + 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." + 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("." + Class.Annotation.NODE + " ellipse"), + d.x + a.dx, d.y + a.dy, a.width, a.height); + scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " rect"), + d.x + a.dx, d.y + a.dy, a.width, a.height); + scene.positionRect(aGroup.select("." + Class.Annotation.NODE + " use"), + d.x + a.dx, d.y + a.dy, a.width, a.height); + + // Edge position + aGroup.select("path." + Class.Annotation.EDGE).transition().attr("d", a => { + // map relative position to absolute position + let points = a.points.map(p => { + return {x: p.dx + d.x, y: p.dy + d.y}; + }); + return edge.interpolate(points); + }); +}; + +} // close module diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts new file mode 100644 index 0000000000..e11ec97f80 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/edge.ts @@ -0,0 +1,177 @@ +/// <reference path="../graph.ts" /> +/// <reference path="../render.ts" /> +/// <reference path="scene.ts" /> + +module tf.graph.scene.edge { + +let Scene = tf.graph.scene; // Aliased + +export function getEdgeKey(edgeObj) { + return edgeObj.v + tf.graph.EDGE_KEY_DELIM + edgeObj.w; +} + +/** + * 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 + */ +export function buildGroup(sceneGroup, + graph: graphlib.Graph<tf.graph.render.RenderNodeInformation, + tf.graph.render.RenderMetaedgeInformation>, sceneBehavior) { + let edgeData = _.reduce(graph.edges(), (edges, edgeObj) => { + let edgeLabel = graph.edge(edgeObj); + edges.push({ + v: edgeObj.v, + w: edgeObj.w, + label: edgeLabel + }); + return edges; + }, []); + + let container = scene.selectOrCreateChild(sceneGroup, "g", + Class.Edge.CONTAINER); + let containerNode = container.node(); + + // Select all children and join with data. + // (Note that all children of g.edges are g.edge) + let 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", Class.Edge.GROUP) + .attr("data-edge", getEdgeKey) + .each(function(d) { + let 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. + let extraEdgeClass = d.label.metaedge && d.label.metaedge.numRefEdges + ? Class.Edge.REF_LINE + " " + 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(d => { + delete sceneBehavior._edgeGroupIndex[getEdgeKey(d)]; + }) + .remove(); + return edgeGroups; +}; + +/** + * 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. + */ +export function appendEdge(edgeGroup, d, sceneBehavior, edgeClass?) { + edgeClass = edgeClass || Class.Edge.LINE; // set default type + + if (d.label && d.label.structural) { + edgeClass += " " + Class.Edge.STRUCTURAL; + } + + edgeGroup.append("path") + .attr("class", edgeClass); +}; + +/** + * Returns a tween interpolator for the endpoint of an edge path. + */ +function getEdgePathInterpolator(d, i, a) { + let renderMetaedgeInfo = d.label; + let adjoiningMetaedge = renderMetaedgeInfo.adjoiningMetaedge; + if (!adjoiningMetaedge) { + return d3.interpolate(a, interpolate(renderMetaedgeInfo.points)); + } + + let renderPath = this; + + // Get the adjoining path that matches the adjoining metaedge. + let adjoiningPath = + <SVGPathElement>((<HTMLElement>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). + let inbound = renderMetaedgeInfo.metaedge.inbound; + + return function(t) { + let 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. + let points = renderMetaedgeInfo.points; + let index = inbound ? 0 : points.length - 1; + points[index].x = adjoiningPoint.x; + points[index].y = adjoiningPoint.y; + let dPath = interpolate(points); + return dPath; + }; +} + +export let interpolate = d3.svg.line() + .interpolate("basis") + .x((d: any) => { return d.x; }) + .y((d: any) => { return d.y; }); + +function position(d) { + d3.select(this).select("path." + Class.Edge.LINE) + .each(function(d) { + let 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) { + let a; + let metaedge = d.label.metaedge; + edgeGroup + .select("path." + Class.Edge.LINE) + .classed("control-dep", metaedge && !metaedge.numRegularEdges); +}; + +} // close module diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts new file mode 100644 index 0000000000..1a34132765 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/minimap.ts @@ -0,0 +1,269 @@ +/// <reference path="../../../../typings/tsd.d.ts" /> +/// <reference path="../common.ts" /> + +module tf.scene { + +/** Show minimap when the viewpoint area is less than X% of the whole area. */ +const FRAC_VIEWPOINT_AREA: number = 0.8; + +export class Minimap { + /** The minimap container. */ + private minimap: HTMLElement; + /** The canvas used for drawing the mini version of the svg. */ + private canvas: HTMLCanvasElement; + /** A buffer canvas used for temporary drawing to avoid flickering. */ + private canvasBuffer: HTMLCanvasElement; + + /** The minimap svg used for holding the viewpoint rectangle. */ + private minimapSvg: SVGSVGElement; + /** The rectangle showing the current viewpoint. */ + private viewpoint: SVGRectElement; + /** + * The scale factor for the minimap. The factor is determined automatically + * so that the minimap doesn't violate the maximum width/height specified + * in the constructor. The minimap maintains the same aspect ratio as the + * original svg. + */ + private scaleMinimap: number; + /** The main svg element. */ + private svg: SVGSVGElement; + /** The svg group used for panning and zooming the main svg. */ + private zoomG: SVGGElement; + /** The zoom behavior of the main svg. */ + private mainZoom: d3.behavior.Zoom<any>; + /** The maximum width and height for the minimap. */ + private maxWandH: number; + /** The last translation vector used in the main svg. */ + private translate: [number, number]; + /** The last scaling factor used in the main svg. */ + private scaleMain: number; + /** The coordinates of the viewpoint rectangle. */ + private viewpointCoord: {x: number, y: number}; + /** The current size of the minimap */ + private minimapSize: {width: number, height: number}; + /** Padding (px) due to the main labels of the graph. */ + private labelPadding: number; + /** + * 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. + */ + constructor(svg: SVGSVGElement, zoomG: SVGGElement, + mainZoom: d3.behavior.Zoom<any>, minimap: HTMLElement, + maxWandH: number, labelPadding: number) { + this.svg = svg; + this.labelPadding = labelPadding; + this.zoomG = zoomG; + this.mainZoom = mainZoom; + this.maxWandH = maxWandH; + let $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. + let $minimapSvg = $minimap.select("svg"); + + // Make the viewpoint rectangle draggable. + let $viewpoint = $minimapSvg.select("rect"); + let dragmove = (d) => { + this.viewpointCoord.x = (<DragEvent>d3.event).x; + this.viewpointCoord.y = (<DragEvent>d3.event).y; + this.updateViewpoint(); + }; + this.viewpointCoord = {x: 0, y: 0}; + let drag = d3.behavior.drag().origin(Object).on("drag", dragmove); + $viewpoint.datum(this.viewpointCoord).call(drag); + + // Make the minimap clickable. + $minimapSvg.on("click", () => { + if ((<Event>d3.event).defaultPrevented) { + // This click was part of a drag event, so suppress it. + return; + } + // Update the coordinates of the viewpoint. + let width = Number($viewpoint.attr("width")); + let height = Number($viewpoint.attr("height")); + let clickCoords = d3.mouse($minimapSvg.node()); + this.viewpointCoord.x = clickCoords[0] - width / 2; + this.viewpointCoord.y = clickCoords[1] - height / 2; + this.updateViewpoint(); + }); + this.viewpoint = <SVGRectElement> $viewpoint.node(); + this.minimapSvg = <SVGSVGElement> $minimapSvg.node(); + this.minimap = minimap; + this.canvas = <HTMLCanvasElement> $minimap.select("canvas.first").node(); + this.canvasBuffer = + <HTMLCanvasElement> $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. + */ + private updateViewpoint(): void { + // 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. + let mainX = - this.viewpointCoord.x * this.scaleMain / this.scaleMinimap; + let mainY = - this.viewpointCoord.y * this.scaleMain / this.scaleMinimap; + let 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). + */ + update(): void { + let $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. + let stylesText = ""; + for (let k = 0; k < document.styleSheets.length; k++) { + try { + let cssRules = (<any>document.styleSheets[k]).cssRules || + (<any>document.styleSheets[k]).rules; + if (cssRules == null) { + continue; + } + for (let 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. + let 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. + let $zoomG = d3.select(this.zoomG); + let zoomTransform = $zoomG.attr("transform"); + $zoomG.attr("transform", null); + + // Get the size of the entire scene. + let 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(<any>this.minimapSize); + d3.select(this.canvasBuffer).attr(<any>this.minimapSize); + if (this.translate != null && this.zoom != null) { + // Update the viewpoint rectangle shape since the aspect ratio of the + // map has changed. + requestAnimationFrame(() => this.zoom()); + } + + // Serialize the main svg to a string which will be used as the rendering + // content for the canvas. + let 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); + let image = new Image(); + image.onload = () => { + // Draw the svg content onto the buffer canvas. + let 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(() => { + // 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. + [this.canvas, this.canvasBuffer] = [this.canvasBuffer, this.canvas]; + }); + }; + 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. + */ + zoom(translate?: [number, number], scale?: number): void { + // 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. + let svgRect = this.svg.getBoundingClientRect(); + let $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; + let viewpointWidth = svgRect.width * this.scaleMinimap / this.scaleMain; + let 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. + let mapWidth = this.minimapSize.width; + let mapHeight = this.minimapSize.height; + let x = this.viewpointCoord.x; + let y = this.viewpointCoord.y; + let w = Math.min(Math.max(0, x + viewpointWidth), mapWidth) - + Math.min(Math.max(0, x), mapWidth); + let h = Math.min(Math.max(0, y + viewpointHeight), mapHeight) - + Math.min(Math.max(0, y), mapHeight); + let fracIntersect = (w * h) / (mapWidth * mapHeight); + if (fracIntersect < FRAC_VIEWPOINT_AREA) { + this.minimap.classList.remove("hidden"); + } else { + this.minimap.classList.add("hidden"); + } + } +} + +} // close module tf.scene diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts new file mode 100644 index 0000000000..8c74b37e07 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/node.ts @@ -0,0 +1,525 @@ +/// <reference path="../graph.ts" /> +/// <reference path="scene.ts" /> +/// <reference path="annotation.ts" /> + +module tf.graph.scene.node { + +/** + * 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 + */ +export function buildGroup(sceneGroup, + nodeData: render.RenderNodeInformation[], sceneBehavior) { + let container = scene.selectOrCreateChild(sceneGroup, "g", + Class.Node.CONTAINER); + // Select all children and join with data. + // (Note that all children of g.nodes are g.node) + let 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, (d: any) => { + // 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", d => { return d.node.name; }) + .each(function(d) { + let nodeGroup = d3.select(this); + // index node group for quick stylizing + sceneBehavior.addNodeGroup(d.node.name, nodeGroup); + }); + + // UPDATE + nodeGroups + .attr("class", d => { + return Class.Node.GROUP + " " + nodeClass(d); + }) + .each(function(d) { + let nodeGroup = d3.select(this); + // add g.in-annotations (always add -- to keep layer order consistent.) + let inAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", + Class.Annotation.INBOX); + annotation.buildGroup(inAnnotationBox, d.inAnnotations, d, + sceneBehavior); + + // add g.out-annotations (always add -- to keep layer order consistent.) + let outAnnotationBox = scene.selectOrCreateChild(nodeGroup, "g", + Class.Annotation.OUTBOX); + annotation.buildGroup(outAnnotationBox, d.outAnnotations, d, + sceneBehavior); + + // label + let 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 === NodeType.META); + + // build .shape below label + let shape = buildShape(nodeGroup, d, 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); + + let nodeGroup = d3.select(this); + if (d.inAnnotations.list.length > 0) { + nodeGroup.select("." + Class.Annotation.INBOX) + .selectAll("." + Class.Annotation.GROUP) + .each(a => { + sceneBehavior.removeAnnotationGroup(a, d); + }); + } + if (d.outAnnotations.list.length > 0) { + nodeGroup.select("." + Class.Annotation.OUTBOX) + .selectAll("." + Class.Annotation.GROUP) + .each(a => { + sceneBehavior.removeAnnotationGroup(a, d); + }); + } + }) + .remove(); + return nodeGroups; +}; + +/** + * 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: render.RenderGroupNodeInformation, sceneBehavior) { + if (renderNodeInfo.node.isGroupNode) { + if (renderNodeInfo.expanded) { + // Recursively build the subscene. + return scene.buildGroup(nodeGroup, renderNodeInfo, sceneBehavior, + Class.Subscene.GROUP); + } + // Clean out existing subscene if the node is not expanded. + scene.selectChild(nodeGroup, "g", Class.Subscene.GROUP).remove(); + } + return null; +}; + +/** + * Translate the subscene of the given node group + */ +function subscenePosition(nodeGroup, d: render.RenderNodeInformation) { + let x0 = d.x - d.width / 2.0 + d.paddingLeft; + let y0 = d.y - d.height / 2.0 + d.paddingTop; + + let subscene = scene.selectChild(nodeGroup, "g", 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: render.RenderNodeInformation, sceneBehavior) { + let group = scene.selectOrCreateChild( + selection, "g", Class.Node.BUTTON_CONTAINER); + scene.selectOrCreateChild(group, "circle", Class.Node.BUTTON_CIRCLE); + scene.selectOrCreateChild(group, "path", Class.Node.EXPAND_BUTTON).attr( + "d", "M0,-2.2 V2.2 M-2.2,0 H2.2"); + scene.selectOrCreateChild(group, "path", Class.Node.COLLAPSE_BUTTON).attr( + "d", "M-2.2,0 H2.2"); + group.on("click", d => { + // Stop this event's propagation so that it isn't also considered a + // node-select. + (<Event>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: render.RenderNodeInformation, + sceneBehavior, disableInteraction?: boolean) { + if (disableInteraction) { + selection.attr("pointer-events", "none"); + return; + } + selection.on("dblclick", d => { + sceneBehavior.fire("node-toggle-expand", { name: d.node.name }); + }) + .on("mouseover", 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", 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", d => { + // Stop this event's propagation so that it isn't also considered + // a graph-select. + (<Event>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: render.RenderNodeInformation, + sceneBehavior) { + let namePath = renderNodeInfo.node.name.split("/"); + let text = namePath[namePath.length - 1]; + + // Truncate long labels for unexpanded Metanodes. + let useFontScale = renderNodeInfo.node.type === NodeType.META && + !renderNodeInfo.expanded; + + let label = scene.selectOrCreateChild(nodeGroup, "text", 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) + "..."; + } + let 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. + */ +let 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: render.RenderNodeInformation, + yOffset: number) { + scene.selectChild(nodeGroup, "text", 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. + */ +export function buildShape(nodeGroup, d, nodeClass: string, before?) { + // Create a group to house the underlying visual elements. + let shapeGroup = scene.selectOrCreateChild(nodeGroup, "g", nodeClass, + before); + // TODO(jimbo): DOM structure should be templated in HTML somewhere, not JS. + switch (d.node.type) { + case NodeType.OP: + scene.selectOrCreateChild(shapeGroup, "ellipse", + Class.Node.COLOR_TARGET); + break; + case NodeType.SERIES: + // Choose the correct stamp to use to represent this series. + let stampType = "annotation"; + let groupNodeInfo = <render.RenderGroupNodeInformation>d; + if (groupNodeInfo.coreGraph) { + stampType = groupNodeInfo.node.hasNonControlEdges + ? "vertical" : "horizontal"; + } + scene.selectOrCreateChild(shapeGroup, "use", Class.Node.COLOR_TARGET) + .attr("xlink:href", "#op-series-" + stampType + "-stamp"); + scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET) + .attr({ rx: d.radius, ry: d.radius }); + break; + case NodeType.BRIDGE: + scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET) + .attr({ rx: d.radius, ry: d.radius }); + break; + case NodeType.META: + scene.selectOrCreateChild(shapeGroup, "rect", Class.Node.COLOR_TARGET) + .attr({ rx: d.radius, ry: d.radius }); + break; + default: + throw Error("Unrecognized node type: " + d.node.type); + } + return shapeGroup; +}; + +export function nodeClass(d: render.RenderNodeInformation) { + switch (d.node.type) { + case NodeType.OP: + return Class.OPNODE; + case NodeType.META: + return Class.METANODE; + case NodeType.SERIES: + return Class.SERIESNODE; + case NodeType.BRIDGE: + return Class.BRIDGENODE; + case NodeType.ELLIPSIS: + return Class.ELLIPSISNODE; + }; + throw Error("Unrecognized node type: " + d.node.type); +}; + +/** Modify node and its subscene and its label's positional attributes */ +function position(nodeGroup, d: render.RenderNodeInformation, sceneBehavior) { + let shapeGroup = scene.selectChild(nodeGroup, "g", Class.Node.SHAPE); + switch (d.node.type) { + case NodeType.OP: { + // position shape + let shape = scene.selectChild(shapeGroup, "ellipse"); + scene.positionEllipse(shape, d.x, d.y, d.width, d.height); + labelPosition(nodeGroup, d, d.labelOffset); + break; + } + case NodeType.META: { + // position shape + let 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 NodeType.SERIES: { + let 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 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. + let 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 */ +let 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: render.RenderNodeInformation, isExpanded: boolean): string { + let colorParams = tf.graph.render.MetanodeColors; + switch (colorBy) { + case ColorBy.STRUCTURE: + if (renderInfo.node.type === tf.graph.NodeType.META) { + let tid = (<Metanode>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 === NodeType.BRIDGE) { + return renderInfo.structural ? "#f0e" : + (<BridgeNode>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; + } + let id = renderInfo.node.name; + let escapedId = tf.escapeQuerySelector(id); + let gradientDefs = d3.select("svg#svg defs #linearGradients"); + let 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(); + let cumulativeProportion = 0; + // For each device, create a stop using the proportion of that device. + _.each(renderInfo.deviceColors, (d: any) => { + let 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). + */ +export function stylize(nodeGroup, renderInfo: render.RenderNodeInformation, + sceneBehavior, nodeClass?) { + nodeClass = nodeClass || Class.Node.SHAPE; + let isHighlighted = sceneBehavior.isNodeHighlighted(renderInfo.node.name); + let isSelected = sceneBehavior.isNodeSelected(renderInfo.node.name); + let isExtract = renderInfo.isInExtract || renderInfo.isOutExtract; + let 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. + let node = nodeGroup.select("." + nodeClass + " ." + Class.Node.COLOR_TARGET); + let 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. + let outlineColor = fillColor.substring(0, 3) === "url" ? + tf.graph.render.MetanodeColors.GRADIENT_OUTLINE : + d3.rgb(fillColor).darker().toString(); + node.style("stroke", outlineColor); + } +}; + +} // close module diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts new file mode 100644 index 0000000000..2e2467f039 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/scene/scene.ts @@ -0,0 +1,409 @@ +/// <reference path="../graph.ts" /> +/// <reference path="edge.ts" /> +/// <reference path="node.ts" /> +/// <reference path="../layout.ts" /> + +module tf.graph.scene { + +/** Enums element class of objects in the scene */ +export let Class = { + Node: { + // <g> element that contains nodes. + CONTAINER: "nodes", + // <g> element that contains detail about a node. + GROUP: "node", + // <g> element that contains visual elements (like rect, ellipse). + SHAPE: "nodeshape", + // <*> element(s) under SHAPE that should receive color updates. + COLOR_TARGET: "nodecolortarget", + // <text> element showing the node's label. + LABEL: "nodelabel", + // <g> element that contains all visuals for the expand/collapse + // button for expandable group nodes. + BUTTON_CONTAINER: "buttoncontainer", + // <circle> element that surrounds expand/collapse buttons. + BUTTON_CIRCLE: "buttoncircle", + // <path> element of the expand button. + EXPAND_BUTTON: "expandbutton", + // <path> element of the collapse button. + COLLAPSE_BUTTON: "collapsebutton" + }, + Edge: { + CONTAINER: "edges", + GROUP: "edge", + LINE: "edgeline", + REF_LINE: "refline", + STRUCTURAL: "structural" + }, + Annotation: { + OUTBOX: "out-annotations", + INBOX: "in-annotations", + GROUP: "annotation", + NODE: "annotation-node", + EDGE: "annotation-edge", + CONTROL_EDGE: "annotation-control-edge", + LABEL: "annotation-label", + ELLIPSIS: "annotation-ellipsis" + }, + Scene: { + GROUP: "scene", + CORE: "core", + INEXTRACT: "in-extract", + OUTEXTRACT: "out-extract" + }, + Subscene: { + GROUP: "subscene" + }, + OPNODE: "op", + METANODE: "meta", + SERIESNODE: "series", + BRIDGENODE: "bridge", + ELLIPSISNODE: "ellipsis" +}; + +/** + * Helper method for fitting the graph in the svg view. + * + * @param svg The main svg. + * @param zoomG The svg group used for panning and zooming. + * @param d3zoom The zoom behavior. + * @param callback Called when the fitting is done. + */ +export function fit(svg, zoomG, d3zoom, callback) { + let svgRect = svg.getBoundingClientRect(); + let sceneSize = zoomG.getBBox(); + let scale = 0.9 * Math.min( + svgRect.width / sceneSize.width, + svgRect.height / sceneSize.height, + 2 + ); + let params = layout.PARAMS.graph; + let zoomEvent = d3zoom.scale(scale) + .on("zoomend.fitted", () => { + // Remove the listener for the zoomend event, + // so we don't get called at the end of regular zoom events, + // just those that fit the graph to screen. + d3zoom.on("zoomend.fitted", null); + callback(); + }) + .translate([params.padding.paddingLeft, params.padding.paddingTop]) + .event; + d3.select(zoomG).transition().duration(500).call(zoomEvent); +}; + +/** + * Helper method for panning the graph to center on the provided node, + * if the node is currently off-screen. + * + * @param nodeName The node to center the graph on + * @param svg The root SVG element for the graph + * @param zoomG The svg group used for panning and zooming. + * @param d3zoom The zoom behavior. + * @return True if the graph had to be panned to display the + * provided node. + */ +export function panToNode(nodeName: String, svg, zoomG, d3zoom): boolean { + let node: any = d3.selectAll("[data-name='" + nodeName + "']." + + Class.Node.GROUP)[0][0]; + if (!node) { + return false; + } + let translate = d3zoom.translate(); + // Check if the selected node is off-screen in either + // X or Y dimension in either direction. + let nodeBox = node.getBBox(); + let nodeCtm = node.getScreenCTM(); + let pointTL = svg.createSVGPoint(); + let pointBR = svg.createSVGPoint(); + pointTL.x = nodeBox.x; + pointTL.y = nodeBox.y; + pointBR.x = nodeBox.x + nodeBox.width; + pointBR.y = nodeBox.y + nodeBox.height; + pointTL = pointTL.matrixTransform(nodeCtm); + pointBR = pointBR.matrixTransform(nodeCtm); + let isOutsideOfBounds = (start, end, bound) => { + return end < 0 || start > bound; + }; + let svgRect = svg.getBoundingClientRect(); + if (isOutsideOfBounds(pointTL.x, pointBR.x, svgRect.width) || + isOutsideOfBounds(pointTL.y, pointBR.y, svgRect.height)) { + // Determine the amount to transform the graph in both X and Y + // dimensions in order to center the selected node. This takes into + // acount the position of the node, the size of the svg scene, the + // amount the scene has been scaled by through zooming, and any previous + // transform already performed by this logic. + let centerX = (pointTL.x + pointBR.x) / 2; + let centerY = (pointTL.y + pointBR.y) / 2; + let dx = ((svgRect.width / 2) - centerX); + let dy = ((svgRect.height / 2) - centerY); + let zoomEvent = d3zoom.translate([translate[0] + dx, translate[1] + dy]) + .event; + d3.select(zoomG).transition().duration(500).call(zoomEvent); + return true; + } + return false; +}; + +/** + * Given a container d3 selection, select a child svg element of a given tag + * and class if exists or append / insert one otherwise. If multiple children + * matches the tag and class name, returns only the first one. + * + * @param container + * @param tagName tag name. + * @param className (optional) Class name. + * @param before (optional) reference DOM node for insertion. + * @return selection of the element + */ +export function selectOrCreateChild(container, tagName: string, + className?: string, before?) { + let child = selectChild(container, tagName, className); + if (!child.empty()) { + return child; + } + let newElement = document.createElementNS("http://www.w3.org/2000/svg", + tagName); + if (className) { + newElement.classList.add(className); + } + + if (before) { // if before exists, insert + container.node().insertBefore(newElement, before); + } else { // otherwise, append + container.node().appendChild(newElement); + } + return d3.select(newElement) + // need to bind data to emulate d3_selection.append + .datum(container.datum()); +}; + +/** + * Given a container d3 selection, select a child element of a given tag and + * class. If multiple children matches the tag and class name, returns only + * the first one. + * + * @param container + * @param tagName tag name. + * @param className (optional) Class name. + * @return selection of the element, or an empty selection + */ +export function selectChild(container, tagName: string, className?: string) { + let children = container.node().childNodes; + for (let i = 0; i < children.length; i++) { + let child = children[i]; + if (child.tagName === tagName && + (!className || child.classList.contains(className)) + ) { + return d3.select(child); + } + } + return d3.select(null); +}; + +/** + * Select or create a sceneGroup and build/update its nodes and edges. + * + * Structure Pattern: + * + * <g class="scene"> + * <g class="core"> + * <g class="edges"> + * ... stuff from tf.graph.scene.edges.build ... + * </g> + * <g class="nodes"> + * ... stuff from tf.graph.scene.nodes.build ... + * </g> + * </g> + * <g class="in-extract"> + * <g class="nodes"> + * ... stuff from tf.graph.scene.nodes.build ... + * </g> + * </g> + * <g class="out-extract"> + * <g class="nodes"> + * ... stuff from tf.graph.scene.nodes.build ... + * </g> + * </g> + * </g> + * + * @param container D3 selection of the parent. + * @param renderNode render node of a metanode or series node. + * @param sceneBehavior Parent scene module. + * @param sceneClass class attribute of the scene (default="scene"). + */ +export function buildGroup(container, + renderNode: render.RenderGroupNodeInformation, + sceneBehavior, + sceneClass: string) { + sceneClass = sceneClass || Class.Scene.GROUP; + let isNewSceneGroup = selectChild(container, "g", sceneClass).empty(); + let sceneGroup = selectOrCreateChild(container, "g", sceneClass); + + // core + let coreGroup = selectOrCreateChild(sceneGroup, "g", Class.Scene.CORE); + let coreNodes = _.reduce(renderNode.coreGraph.nodes(), (nodes, name) => { + let node = renderNode.coreGraph.node(name); + if (!node.excluded) { + nodes.push(node); + } + return nodes; + }, []); + + if (renderNode.node.type === NodeType.SERIES) { + // For series, we want the first item on top, so reverse the array so + // the first item in the series becomes last item in the top, and thus + // is rendered on the top. + coreNodes.reverse(); + } + + // Create the layer of edges for this scene (paths). + edge.buildGroup(coreGroup, renderNode.coreGraph, sceneBehavior); + + // Create the layer of nodes for this scene (ellipses, rects etc). + node.buildGroup(coreGroup, coreNodes, sceneBehavior); + + // In-extract + if (renderNode.isolatedInExtract.length > 0) { + let inExtractGroup = selectOrCreateChild(sceneGroup, "g", + Class.Scene.INEXTRACT); + node.buildGroup(inExtractGroup, renderNode.isolatedInExtract, + sceneBehavior); + } else { + selectChild(sceneGroup, "g", Class.Scene.INEXTRACT).remove(); + } + + // Out-extract + if (renderNode.isolatedOutExtract.length > 0) { + let outExtractGroup = selectOrCreateChild(sceneGroup, "g", + Class.Scene.OUTEXTRACT); + node.buildGroup(outExtractGroup, renderNode.isolatedOutExtract, + sceneBehavior); + } else { + selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT).remove(); + } + + position(sceneGroup, renderNode); + + // Fade in the scene group if it didn't already exist. + if (isNewSceneGroup) { + sceneGroup.attr("opacity", 0) + .transition().attr("opacity", 1); + } + + return sceneGroup; +}; + +/** + * Given a scene's svg group, set g.in-extract, g.coreGraph, g.out-extract svg + * groups' position relative to the scene. + * + * @param sceneGroup + * @param renderNode render node of a metanode or series node. + */ +function position(sceneGroup, renderNode: render.RenderGroupNodeInformation) { + // Translate scenes down by the label height so that when showing graphs in + // expanded metanodes, the graphs are below the labels. Do not shift them + // down for series nodes as series nodes don't have labels inside of their + // bounding boxes. + let yTranslate = renderNode.node.type === NodeType.SERIES ? + 0 : layout.PARAMS.subscene.meta.labelHeight; + + // core + translate(selectChild(sceneGroup, "g", Class.Scene.CORE), + 0, yTranslate); + + // in-extract + let inExtractX = renderNode.coreBox.width === 0 ? + 0 : renderNode.coreBox.width; + let hasInExtract = renderNode.isolatedInExtract.length > 0; + if (hasInExtract) { + translate(selectChild(sceneGroup, "g", Class.Scene.INEXTRACT), + inExtractX, yTranslate); + } + + // out-extract + let hasOutExtract = renderNode.isolatedOutExtract.length > 0; + if (hasOutExtract) { + let outExtractX = inExtractX + renderNode.inExtractBox.width + + renderNode.extractXOffset; + translate(selectChild(sceneGroup, "g", Class.Scene.OUTEXTRACT), + outExtractX, yTranslate); + } +}; + +/** Adds a click listener to a group that fires a graph-select event */ +export function addGraphClickListener(graphGroup, sceneBehavior) { + d3.select(graphGroup).on("click", () => { + sceneBehavior.fire("graph-select"); + }); +}; + +/** Helper for adding transform: translate(x0, y0) */ +export function translate(selection, x0: number, y0: number) { + selection.attr("transform", "translate(" + x0 + "," + y0 + ")"); +}; + +/** + * Helper for setting position of a svg rect + * @param rect rect to set position of. + * @param cx Center x. + * @param cy Center x. + * @param width Width to set. + * @param height Height to set. + */ +export function positionRect(rect, cx: number, cy: number, width: number, + height: number) { + rect.transition().attr({ + x: cx - width / 2, + y: cy - height / 2, + width: width, + height: height + }); +}; + +/** + * Helper for setting position of a svg expand/collapse button + * @param button container group + * @param renderNode the render node of the group node to position + * the button on. + */ +export function positionButton(button, + renderNode: render.RenderNodeInformation) { + // Position the button in the top-right corner of the group node, + // with space given the draw the button inside of the corner. + let x = renderNode.x + renderNode.width / 2 - 6; + let y = renderNode.y - renderNode.height / 2 + 6; + // For unexpanded series nodes, the button has special placement due + // to the unique visuals of this group node. + if (renderNode.node.type === NodeType.SERIES && !renderNode.expanded) { + x += 10; + y -= 2; + } + let translateStr = "translate(" + x + "," + y + ")"; + button.selectAll("path").transition().attr("transform", translateStr); + button.select("circle").transition().attr({ + cx: x, + cy: y, + r: layout.PARAMS.nodeSize.meta.expandButtonRadius + }); +}; + +/** + * Helper for setting position of a svg ellipse + * @param ellipse ellipse to set position of. + * @param cx Center x. + * @param cy Center x. + * @param width Width to set. + * @param height Height to set. + */ +export function positionEllipse(ellipse, cx: number, cy: number, + width: number, height: number) { + ellipse.transition().attr({ + cx: cx, + cy: cy, + rx: width / 2, + ry: height / 2 + }); +}; + +} // close module diff --git a/tensorflow/tensorboard/components/tf-graph-common/lib/template.ts b/tensorflow/tensorboard/components/tf-graph-common/lib/template.ts new file mode 100644 index 0000000000..b5aafc55e5 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/lib/template.ts @@ -0,0 +1,282 @@ +/// <reference path="graph.ts" /> +/// <reference path="hierarchy.ts" /> + +module tf.graph.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) + */ +export function detect(h, verifyTemplate): {[templateId: string]: string[]} { + // 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. + let nnGroups = clusterSimilarSubgraphs(h); + + // For each metanode, compare its subgraph (starting from shallower groups) + // and assign template id. + let 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(); +}; + +/** + * @return Unique string for a metanode based on depth, |V|, |E| and + * op type histogram. + */ + function getSignature(metanode) { + // depth=<number> |V|=<number> |E|=<number> + let 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 + let 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: hierarchy.Hierarchy) { + /** a dict from metanode.signature() => Array of tf.graph.Groups */ + let hashDict = _(h.getNodeMap()).reduce(function(hash, node: OpNode|Metanode, name) { + if (node.type !== NodeType.META) { + return hash; + } + let levelOfMetaNode = name.split("/").length - 1; + let signature = getSignature(node); + let 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 nn metanode with only one member + .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) { + let signature = nnGroupPair[0], + nnGroup = nnGroupPair[1].nodes, + clusters = []; + + nnGroup.forEach(function(metanode) { + // check with each existing cluster + for (let i = 0; i < clusters.length; i++) { + let 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: string[], graph: graphlib.Graph<Metanode|OpNode, Metaedge>, + prefix: string) { + return _.sortByAll(names, + function(name) { + let node = graph.node(name); + return (<OpNode>node).op; + }, + function(name) { + let node = graph.node(name); + return (<Metanode>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: graphlib.Graph<any, any>, g2: graphlib.Graph<any, any>) { + if (!tf.graph.hasSimilarDegreeSequence(g1, g2)) { + return false; + } + + // if we want to skip, just return true here. + // return true; + + // Verify sequence by running DFS + let g1prefix = g1.graph().name; + let g2prefix = g2.graph().name; + + let visited1 = {}; + let visited2 = {}; + let stack = []; + + /** + * push sources or successors into the stack + * if the visiting pattern has been similar. + */ + function stackPushIfNotDifferent(n1, n2) { + let 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]) { // implied && !visited2[sub2] + visited1[sub1] = visited2[sub2] = true; + stack.push({n1: n1, n2: n2}); + } + + return false; + } + + // check if have same # of sources then sort and push + let sources1 = g1.sources(); + let 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 (let i = 0; i < sources1.length; i++) { + let different = stackPushIfNotDifferent(sources1[i], sources2[i]); + if (different) { + return false; + } + } + + while (stack.length > 0) { + let cur = stack.pop(); + + // check node + let similar = isSimilarNode(g1.node(cur.n1), g2.node(cur.n2)); + if (!similar) { + return false; + } + + // check if have same # of successors then sort and push + let 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 (let j = 0; j < succ1.length; j++) { + let different = stackPushIfNotDifferent(succ1[j], succ2[j]); + if (different) { + return false; + } + } + } + + return true; +} + +/** + * Returns if two nodes have identical structure. + */ + function isSimilarNode(n1: OpNode|Metanode|SeriesNode, n2: OpNode|Metanode|SeriesNode): boolean { + if (n1.type === NodeType.META) { + // compare metanode + let metanode1 = <Metanode> n1; + let metanode2 = <Metanode> n2; + return metanode1.templateId && metanode2.templateId && metanode1.templateId === metanode2.templateId; + } else if (n1.type === NodeType.OP && n2.type === NodeType.OP) { + // compare leaf node + return (<OpNode>n1).op === (<OpNode>n2).op; + } else if (n1.type === NodeType.SERIES && n2.type === NodeType.SERIES) { + // compare series node sizes and operations + // (only need to check one op as all op nodes are identical in series) + let seriesnode1 = <SeriesNode> n1; + let seriesnode2 = <SeriesNode> n2; + let seriesnode1Count = seriesnode1.metagraph.nodeCount(); + return (seriesnode1Count === seriesnode2.metagraph.nodeCount() && + (seriesnode1Count === 0 || + ((<OpNode>seriesnode1.metagraph.node(seriesnode1.metagraph.nodes()[0])).op === + (<OpNode>seriesnode2.metagraph.node(seriesnode2.metagraph.nodes()[0])).op))); + } + return false; +} +} diff --git a/tensorflow/tensorboard/components/tf-graph-common/tf-graph-common.html b/tensorflow/tensorboard/components/tf-graph-common/tf-graph-common.html new file mode 100644 index 0000000000..e4cd153113 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-common/tf-graph-common.html @@ -0,0 +1,16 @@ +<script src="../../bower_components/d3/d3.js"></script> +<script src="../../bower_components/lodash/lodash.js"></script> +<script src="../../bower_components/graphlib/dist/graphlib.core.js"></script> + +<script src="lib/common.js"></script> +<script src="lib/graph.js"></script> +<script src="lib/parser.js"></script> +<script src="lib/hierarchy.js"></script> +<script src="lib/render.js"></script> +<script src="lib/template.js"></script> +<script src="lib/scene/scene.js"></script> +<script src="lib/scene/annotation.js"></script> +<script src="lib/scene/edge.js"></script> +<script src="lib/scene/node.js"></script> +<script src="lib/layout.js"></script> +<script src="lib/colors.js"></script> diff --git a/tensorflow/tensorboard/components/tf-graph-dashboard/tf-graph-dashboard.html b/tensorflow/tensorboard/components/tf-graph-dashboard/tf-graph-dashboard.html new file mode 100644 index 0000000000..779d2d3fe9 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-dashboard/tf-graph-dashboard.html @@ -0,0 +1,118 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../tf-graph-loader/tf-graph-loader.html"> +<link rel="import" href="../tf-graph-board/tf-graph-board.html"> +<link rel="import" href="../tf-graph/tf-graph-controls.html"> +<link rel="import" href="../tf-dashboard-common/warning-style.html"> + +<!-- +tf-graph-dashboard displays a graph from a TensorFlow run. + +It has simple behavior: Creates a url-generator and run-generator +to talk to the backend, and then passes the runsWithGraph (list of runs with +associated graphs) along with the url generator into tf-graph-board for display. + +If there are multiple runs with graphs, the first run's graph is shown +by default. The user can select a different run from a dropdown menu. +--> + +<dom-module id="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> +</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> diff --git a/tensorflow/tensorboard/components/tf-graph-info/tf-graph-info.html b/tensorflow/tensorboard/components/tf-graph-info/tf-graph-info.html new file mode 100644 index 0000000000..b7900f86de --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-info/tf-graph-info.html @@ -0,0 +1,65 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="tf-node-info.html"> +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html b/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html new file mode 100644 index 0000000000..5044bf2bb1 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-info/tf-node-info.html @@ -0,0 +1,345 @@ +<link rel="import" href="../../bower_components/iron-collapse/iron-collapse.html"> +<link rel="import" href="../../bower_components/iron-list/iron-list.html"> +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="../../bower_components/paper-item/all-imports.html"> +<link rel="import" href="../tf-graph-common/tf-graph-common.html"> +<link rel="import" href="../tf-graph/tf-graph-icon.html"> +<link rel="import" href="tf-node-list-item.html"> + +<dom-module id="tf-node-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> diff --git a/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html b/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html new file mode 100644 index 0000000000..f16e9e4511 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-info/tf-node-list-item.html @@ -0,0 +1,91 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../tf-graph/tf-graph-icon.html"> + +<dom-module id="tf-node-list-item"> + <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> diff --git a/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html b/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html new file mode 100644 index 0000000000..3f290f2152 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-graph-loader/tf-graph-loader.html @@ -0,0 +1,172 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> + +<!-- +An element which provides a filter parsing for pbtxt to graph output. +--> +<dom-module id="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> 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> diff --git a/tensorflow/tensorboard/components/tf-histogram-dashboard/tf-histogram-dashboard.html b/tensorflow/tensorboard/components/tf-histogram-dashboard/tf-histogram-dashboard.html new file mode 100644 index 0000000000..8f8f159964 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-histogram-dashboard/tf-histogram-dashboard.html @@ -0,0 +1,210 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../tf-event-dashboard/tf-data-coordinator.html"> +<link rel="import" href="../tf-event-dashboard/tf-tooltip-coordinator.html"> +<link rel="import" href="../tf-event-dashboard/tf-run-selector.html"> +<link rel="import" href="../tf-event-dashboard/tf-x-type-selector.html"> +<link rel="import" href="../tf-dashboard-common/tf-run-generator.html"> +<link rel="import" href="../tf-event-dashboard/tf-color-scale.html"> +<link rel="import" href="../tf-dashboard-common/tf-url-generator.html"> +<link rel="import" href="../tf-dashboard-common/tf-dashboard-layout.html"> +<link rel="import" href="../tf-dashboard-common/dashboard-style.html"> +<link rel="import" href="../tf-dashboard-common/warning-style.html"> +<link rel="import" href="../tf-categorizer/tf-categorizer.html"> +<link rel="import" href="../tf-event-dashboard/tf-chart.html"> +<link rel="import" href="../tf-collapsable-pane/tf-collapsable-pane.html"> +<link rel="import" href="../../bower_components/iron-collapse/iron-collapse.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="../imports/lodash.html"> + +<!-- +tf-histogram-dashboard is a complete frontend that loads runs from a backend, +and creates chart panes that display data for those runs. + +It provides a categorizer, run selector, and x type selector, by which the user +can customize how data is organized and displayed. + +Each chart has a button that can toggle whether it is "selected"; selectedRuns +charts are larger. + +Organizationally, the #plumbing div contains components that have no concrete +manifestation and just effect data bindings or data loading. The #sidebar contains +shared controls like the tf-categorizer, tf-run-selector, and tf-x-type-selector. +The #center div contains tf-charts embedded inside tf-collapsable-panes. +--> +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-image-dashboard/demo/image-loader-demo.html b/tensorflow/tensorboard/components/tf-image-dashboard/demo/image-loader-demo.html new file mode 100644 index 0000000000..7aafd247f3 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-image-dashboard/demo/image-loader-demo.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <script src="../../../bower_components/d3/d3.js"></script> + <script src="../../../bower_components/plottable/plottable.js"></script> + <link rel="stylesheet" type="text/css" href="../../../bower_components/plottable/plottable.css"> + <link rel="import" href="../tf-image-dashboard.html"> + <title>Event Dashboard Demo Demo</title> + </head> + <body> + <script> + TF.Urls.runsUrl = function() { + return "data/runs.json" + }; + TF.Urls.scalarsUrl = function(tag, run) { + return "data/" + run + "/" + tag + ".json"; + }; + </script> + + <dom-module id="x-demo"> + <style> + #loader { + width: 300px; + height: 300px; + } + </style> + <template> + <tf-image-loader + id="loader" + run="[[run]]" + tag="[[tag]]" + images-generator="[[imagesGenerator]]" + individual-image-generator="[[individualImageGenerator]]" + ></tf-image-loader> + </template> + <script> + var imagesUrl = function(tag, run) { + return "data/images/" + run + "/" + tag + ".json"; + }; + var individualImageUrl = function(query) { + return "data/individualImage/" + query + ".png"; + }; + Polymer({ + is: "x-demo", + properties: { + run: { + type: String, + value: "train", + }, + tag: { + type: String, + value: "reconstruction_07%2Fimage%2F2" + }, + imagesGenerator: { + type: Function, + value: function() { + return imagesUrl; + }, + }, + individualImageGenerator: { + type: Function, + value: function() { + return individualImageUrl; + }, + }, + }, + }); + </script> + </dom-module> + <x-demo id="demo"></x-demo> + </body> +</html> diff --git a/tensorflow/tensorboard/components/tf-image-dashboard/demo/index.html b/tensorflow/tensorboard/components/tf-image-dashboard/demo/index.html new file mode 100644 index 0000000000..4645b4b783 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-image-dashboard/demo/index.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="stylesheet" type="text/css" href="../../../lib/css/global.css"> + <link rel="import" href="../tf-image-dashboard.html"> + <title>Image Dashboard Demo</title> + <style> + #container{ + width: 500px; + height: 800px; + border: 2px solid grey; + } + html,body { + height: 100%; + } + </style> + </head> + <body> + <script> + TF.Urls.runsUrl = function() { + return "data/runs.json" + }; + TF.Urls.imagesUrl = function(tag, run) { + return "data/images/" + run + "/" + tag + ".json"; + }; + TF.Urls.individualImageUrl = function(query) { + return "data/individualImage/" + query + ".png"; + } + </script> + + <p>The image dashboard is deliberately inside a small container + so that it's easy to test that the scroll bars display properly.</p> + <p>Looks goofy though.</p> + <div id="container"> + <tf-image-dashboard id="demo"></tf-image-dashboard> + </div> + </body> +</html> diff --git a/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-dashboard.html b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-dashboard.html new file mode 100644 index 0000000000..726a420e9f --- /dev/null +++ b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-dashboard.html @@ -0,0 +1,90 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../tf-dashboard-common/tf-run-generator.html"> +<link rel="import" href="../tf-dashboard-common/tf-url-generator.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="tf-image-grid.html"> +<link rel="import" href="../tf-dashboard-common/warning-style.html"> + +<!-- +tf-image-dashboard displays a dashboard that loads images from a TensorFlow run. + +Right now it has very simple behavior: Creates a url-generator and run-generator +to talk to the backend, and then passes the runToImages map and urlGenerators into +a tf-image-grid for display. + +Likely we will add more in the future, e.g. a sidebar like in the event +dashboard that allows filtering and organizing the tags and runs, and a +mechanism for loading older images rather than always getting the most recent one. +--> +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-grid.html b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-grid.html new file mode 100644 index 0000000000..b7787b98c4 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-grid.html @@ -0,0 +1,166 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-styles/paper-styles.html"> +<link rel="import" href="tf-image-loader.html"> +<link rel="import" href="../imports/lodash.html"> +<link rel="import" href="../tf-dashboard-common/scrollbar-style.html"> + +<!-- +tf-image-grid creates a grid for examining image data. The columsn correspond +to runs and the rows correspond to tags. Each cell is an image. + +Structurally, it makes extensive use of flexbox for layout: it has a top-level +columnar flexbox that contains the topRow (run names) and then a +bottomContainer. The bottomContainer is another columnar flexbox which contains +repeated image-rows. Each image-row is a row flexbox which contains a tag name +cell, and then image cells. + +In the future, we should improve on the layout by making the tag names and run names have fixed positions +within the image-grid, so that when you scroll you always have context (e.g. row and column names in a spreadsheet). +For now, it just scrolls. + +The image grid provides internal scroll bars (with styling) so that it can be dropped into +a dashboard in a predictable fashion, even though the internal image grid may be enormous. + +Room for future improvement: + +- Make it obvious when an image didn't load due to the image not existing. +- Find some way to collapse sparse image grids into denser ones (when sparsity +is high) +- Fix column/row names +- Include hook for loading past images (by step/timestamp? or index?) + +--> +<dom-module id="tf-image-grid"> + <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> diff --git a/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-loader.html b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-loader.html new file mode 100644 index 0000000000..e70f189c73 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-image-dashboard/tf-image-loader.html @@ -0,0 +1,64 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../imports/lodash.html"> + +<!-- +tf-image-loader loads an individual image from the TensorBoard backend. + +Right now it always loads the most recent image. We should add support in the +future for loading older images. +--> +<dom-module id="tf-image-loader"> + <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]]"></img> + </template is="dom-if"> + </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> diff --git a/tensorflow/tensorboard/components/tf-multi-checkbox/demo/index.html b/tensorflow/tensorboard/components/tf-multi-checkbox/demo/index.html new file mode 100644 index 0000000000..e5661b98bc --- /dev/null +++ b/tensorflow/tensorboard/components/tf-multi-checkbox/demo/index.html @@ -0,0 +1,162 @@ +<!DOCTYPE html> +<html> +<head> +<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> +<script src="../../../bower_components/d3/d3.js"></script> +<link rel="import" href="../tf-multi-checkbox.html"> +<link rel="import" href="../../tf-event-dashboard/tf-color-scale.html"> +<link rel="stylesheet" type="text/css" href="../../../lib/css/global.css"> + +</head> +<body> +<script> +var seed = 1; +function random() { + var x = Math.sin(seed++) * 10000; + return x - Math.floor(x); +} +</script> +<style> +</style> + +<dom-module id="mc-demo"> + <template> + <tf-multi-checkbox + id="multiCheckbox" + names="[[names]]" + tooltips="[[_tooltips]]" + class-scale="[[classScale]]" + highlights="[[highlights]]" + ></tf-multi-checkbox> + <tf-color-scale + id="colorScale" + runs="[[names]]" + out-class-scale="{{classScale}}" + ></tf-color-scale> + <style> + </style> + </template> + <script> + + function randomTooltip() { + var s = ""; + while (random() < 0.8) { + s += String(10*random())[0]; + } + return s; + } + Polymer({ + is: "mc-demo", + properties: { + names: Array, + tooltips: Object, + autoGenerateTooltips: {value: true}, + _tooltips: Object, + classScale: Function, + highlights: Array, + }, + observers: [ + 'autogenerate(names, autoGenerateTooltips)', + 'randomHighlights(names)' + ], + autogenerate: function(names, autoGenerateTooltips) { + if (autoGenerateTooltips) { + var tooltips = {}; + names.forEach(function(n) { + if (random() > 0.5) { + tooltips[n] = randomTooltip(); + } + }); + this._tooltips = tooltips; + } + }, + randomHighlights: function(names) { + var h = []; + names.forEach(function(n) { + if (random() > 0.6) { + h.push(n); + } + }); + this.highlights = h; + } + }); + </script> +</dom-module> + +<dom-module id="x-demo"> +<style> +.small { + width: 200px; + height: 500px; +} +.large { + width: 500px; + height: 900px; +} +html,body { + height: 100%; +} +mc-demo { + padding: 5px; + border: 1px solid var(--paper-red-500); + display: inline-block; +} +</style> +<template> + <div class="demo-block"> + <mc-demo id="demo1" class="small" names="[[long_names]]"></mc-demo> + <mc-demo class="small" names="[[many_names]]"></mc-demo> + <mc-demo class="small" names="[[many_long_names]]"></mc-demo> + </div> + + <div class="demo-block"> + <mc-demo class="large" names="[[long_names]]"></mc-demo> + <mc-demo class="large" names="[[many_names]]"></mc-demo> + <mc-demo class="large" names="[[many_long_names]]"></mc-demo> + </div> + +</template> +<script> + +function long_names() { + return [ + "foo_bar very long name with spaces", + "the quick brown fox jumped over the lazy dog", + "supercalifragilisticexpialodcious/bar/foo/zod/longer/longer", + ]; +} + +function many_names() { + var out = []; + for (var i=0; i<20; i++) { + out.push("foo_bar-" + i); + out.push("bar_zod_bing-" + i); + out.push("lol-" + i); + } + return out; +} + +function many_long_names() { + var out = []; + for (var i=0; i<20; i++) { + out.push("foo_bar very very very long some spaces though-" + i); + out.push("bar_zod_bing_bas_womp_wub_wub_dub_wub_wub-" + i); + out.push("rightly_to_be_great_is_not_to_stir_without_great_argument_but_greatly_to_find_quarrel_in_a_straw_when_honors_at_the_stake-" + i); + } + return out; +} + +Polymer({ + is: "x-demo", + properties: { + long_names: {type: Array, value: long_names}, + many_names: {type: Array, value: many_names}, + many_long_names: {type: Array, value: many_long_names}, +}, +}); +</script> +</dom-module> + +<x-demo id="demo"></x-demo> +</body> +</html> diff --git a/tensorflow/tensorboard/components/tf-multi-checkbox/tf-multi-checkbox.html b/tensorflow/tensorboard/components/tf-multi-checkbox/tf-multi-checkbox.html new file mode 100644 index 0000000000..a5447e8f5e --- /dev/null +++ b/tensorflow/tensorboard/components/tf-multi-checkbox/tf-multi-checkbox.html @@ -0,0 +1,228 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-checkbox/paper-checkbox.html"> +<link rel="import" href="../imports/lodash.html"> +<link rel="import" href="../tf-dashboard-common/scrollbar-style.html"> +<link rel="import" href="../tf-dashboard-common/run-color-style.html"> +<!-- +tf-multi-checkbox creates a list of checkboxes that can be used to toggle on or off +a large number of values. Each checkbox displays a name, and may also have an +assosciated tooltip value. Checkboxes can be highlighted, hidden, and re-ordered. + +tf-multi-checkbox assumes that the names may be very long compared to the width +of the checkbox, and the number of names may also be very large, and works to +handle these situations gracefully. + +API: + +Properties in: +names: The string names to associate with checkboxes. +tooltips: An object mapping from name to tooltip value. +tooltipOrderer: A function that is used to compute how to order the names based +on tooltip values (when available). If tooltip values and a tooltip orderer are +present, the tooltipOrderer computes a numeric value for each tooltip, tooltips +with higher values are ordered first, tooltips with equal values are ordered +lexicogrpahically, and tooltips without a value are placed last. If the +tooltipOrderer is set to a falsey value (eg null), then the names are not +re-ordered based on tooltip value. +classScale: A function that maps from name to class name, which is applied as +the special property color-class. This is intended to be used to color the names. +hideMissingTooltips: If set, then when tooltips are present, any names that do +not have an associate non-empty tooltip value will be hidden. + +Properties out: +outSelected: An array of names that the user has checked. +If the user does not interact, everything will be checked. + +--> + +<dom-module id="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> diff --git a/tensorflow/tensorboard/components/tf-regex-group/demo/index.html b/tensorflow/tensorboard/components/tf-regex-group/demo/index.html new file mode 100644 index 0000000000..efef84e0fc --- /dev/null +++ b/tensorflow/tensorboard/components/tf-regex-group/demo/index.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + <head> + <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../tf-regex-group.html"> + </head> + <body> + <style> + .container { + width: 255px; + padding: 10px; + border: 1px solid #3f51b5; + border-radius: 5px; + } + :host { + margin: 0px; + } + </style> + <template id="page-template" is="dom-bind"> + <div class="container"> + <tf-regex-group regexes="{{regexes}}" id="demo"></tf-regex-group> + </div> + <p> Regexes:</p> + <template is="dom-repeat" items="[[regexes]]"> + <p>"<span>[[item]]</span>"</p> + </template> + </template> + </body> + <script> + + </script> +</html> diff --git a/tensorflow/tensorboard/components/tf-regex-group/index.html b/tensorflow/tensorboard/components/tf-regex-group/index.html new file mode 100644 index 0000000000..0238a8d326 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-regex-group/index.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> + + <title>tf-regex-group</title> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + + <script src="../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> + <link rel="import" href="../../bower_components/iron-component-page/iron-component-page.html"> + +</head> +<body> + + <iron-component-page></iron-component-page> + +</body> +</html> diff --git a/tensorflow/tensorboard/components/tf-regex-group/tf-regex-group.html b/tensorflow/tensorboard/components/tf-regex-group/tf-regex-group.html new file mode 100644 index 0000000000..e9673e85d9 --- /dev/null +++ b/tensorflow/tensorboard/components/tf-regex-group/tf-regex-group.html @@ -0,0 +1,151 @@ +<link rel="import" href="../../bower_components/polymer/polymer.html"> +<link rel="import" href="../../bower_components/paper-icon-button/paper-icon-button.html"> +<link rel="import" href="../../bower_components/iron-icons/iron-icons.html"> +<link rel="import" href="../../bower_components/paper-toggle-button/paper-toggle-button.html"> +<link rel="import" href="../../bower_components/paper-input/paper-input.html"> + +<!-- +`tf-regex-group` provides an input component for a group of regular expressions. + +Example: + <tf-regex-group regexes="{{regexes}}"></tf-regex-group> + +It contains a series of regular expression input fields. From this, it computes +`regexes', an array in which every element is either a string representing an +active, valid, nonempty regular expression, or the value `null` + +Public Properties: +`regexes` a readonly, notifying array of strings, where each string is an + active, valid, nonempty regex + +It maintains an invariant that the final regex should always be an empty string, +so the user can easily add more regular expressions. It does this by adding +a new empty regex when the final one is nonempty. + +Pressing "enter" moves focus to the next regex (or just blurs if there are no +more regexes). +--> +<dom-module id="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> |