diff options
-rw-r--r-- | WORKSPACE | 6 | ||||
-rw-r--r-- | tensorflow/tensorboard/TAG | 2 | ||||
-rw-r--r-- | tensorflow/tensorboard/dist/tf-tensorboard.html | 1730 |
3 files changed, 1226 insertions, 512 deletions
@@ -175,7 +175,7 @@ new_git_repository( name = "iron_menu_behavior", build_file = "bower.BUILD", remote = "https://github.com/polymerelements/iron-menu-behavior.git", - tag = "v1.1.7", + tag = "v1.1.8", ) new_git_repository( @@ -189,7 +189,7 @@ new_git_repository( name = "iron_overlay_behavior", build_file = "bower.BUILD", remote = "https://github.com/polymerelements/iron-overlay-behavior.git", - tag = "v1.8.2", + tag = "v1.8.3", ) new_git_repository( @@ -322,7 +322,7 @@ new_git_repository( name = "paper_menu_button", build_file = "bower.BUILD", remote = "https://github.com/polymerelements/paper-menu-button.git", - tag = "v1.1.1", + tag = "v1.3.0", ) new_git_repository( diff --git a/tensorflow/tensorboard/TAG b/tensorflow/tensorboard/TAG index 2bd5a0a98a..409940768f 100644 --- a/tensorflow/tensorboard/TAG +++ b/tensorflow/tensorboard/TAG @@ -1 +1 @@ -22 +23 diff --git a/tensorflow/tensorboard/dist/tf-tensorboard.html b/tensorflow/tensorboard/dist/tf-tensorboard.html index 1c0297dbdb..8fe8e895ac 100644 --- a/tensorflow/tensorboard/dist/tf-tensorboard.html +++ b/tensorflow/tensorboard/dist/tf-tensorboard.html @@ -18,7 +18,66 @@ Instead, use `gulp regenerate` to create a new version with your changes. --> <html><head><meta charset="UTF-8"> +<script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +var TF; +(function (TF) { + var TensorBoard; + (function (TensorBoard) { + TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY = 'TF.TensorBoard.autoReloadEnabled'; + var getAutoReloadFromLocalStorage = function () { + var val = window.localStorage.getItem(TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY); + return val === 'true' || val == null; // defaults to true + }; + TensorBoard.AutoReloadBehavior = { + properties: { + autoReloadEnabled: { + type: Boolean, + observer: '_autoReloadObserver', + value: getAutoReloadFromLocalStorage, + }, + _autoReloadId: { + type: Number, + }, + autoReloadIntervalSecs: { + type: Number, + value: 120, + }, + }, + detached: function () { window.clearTimeout(this._autoReloadId); }, + _autoReloadObserver: function (autoReload) { + window.localStorage.setItem(TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY, autoReload); + if (autoReload) { + var _this = this; + this._autoReloadId = window.setTimeout(this._doAutoReload.bind(this), this.autoReloadIntervalSecs * 1000); + } + else { + window.clearTimeout(this._autoReloadId); + } + }, + _doAutoReload: function () { + if (this.reload == null) { + throw new Error('AutoReloadBehavior requires a reload method'); + } + this.reload(); + this._autoReloadId = window.setTimeout(this._doAutoReload.bind(this), this.autoReloadIntervalSecs * 1000); + } + }; + })(TensorBoard = TF.TensorBoard || (TF.TensorBoard = {})); +})(TF || (TF = {})); +</script> </head><body><div hidden="" by-vulcanize=""><dom-module id="tf-globals" assetpath="../tf-globals/"> <script>/* Copyright 2015 Google Inc. All Rights Reserved. @@ -139,7 +198,327 @@ var TF; </style> </template> </dom-module> +<dom-module id="tf-storage" assetpath="../tf-storage/"> + <script>/* Copyright 2015 Google Inc. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/* tslint:disable:no-namespace variable-name */ +/** + * The Storage Module provides storage for URL parameters, and an API for + * getting and setting TensorBoard's stateful URI. + * + * It generates URI components like: events&runPrefix=train* + * which TensorBoard uses after like localhost:8000/#events&runPrefix=train* + * to store state in the URI. + */ +var TF; +(function (TF) { + var URIStorage; + (function (URIStorage) { + /** + * A key that users cannot use, since TensorBoard uses this to store info + * about the active tab. + */ + URIStorage.TAB = '__tab__'; + /** + * The name of the property for users to set on a Polymer component + * in order for its stored properties to be stored in the URI unambiguously. + * (No need to set this if you want mutliple instances of the component to + * share URI state) + * + * Example: + * <my-component disambiguator="0"></my-component> + * + * The disambiguator should be set to any unique value so that multiple + * instances of the component can store properties in URI storage. + * + * Because it's hard to dereference this variable in HTML property bindings, + * it is NOT safe to change the disambiguator string without find+replace + * across the codebase. + */ + URIStorage.DISAMBIGUATOR = 'disambiguator'; + /** + * Return a boolean stored in the URI, given a corresponding key. + * Undefined if not found. + */ + function getBoolean(key) { + var items = _componentToDict(_readComponent()); + var item = items[key]; + return item === 'true' ? true : item === 'false' ? false : undefined; + } + URIStorage.getBoolean = getBoolean; + /** + * Store a boolean in the URI, with a corresponding key. + */ + function setBoolean(key, value) { + var items = _componentToDict(_readComponent()); + items[key] = value.toString(); + _writeComponent(_dictToComponent(items)); + } + URIStorage.setBoolean = setBoolean; + /** + * Return a string stored in the URI, given a corresponding key. + * Undefined if not found. + */ + function getString(key) { + var items = _componentToDict(_readComponent()); + return items[key]; + } + URIStorage.getString = getString; + /** + * Store a string in the URI, with a corresponding key. + */ + function setString(key, value) { + var items = _componentToDict(_readComponent()); + items[key] = value; + _writeComponent(_dictToComponent(items)); + } + URIStorage.setString = setString; + /** + * Return a number stored in the URI, given a corresponding key. + * Undefined if not found. + */ + function getNumber(key) { + var items = _componentToDict(_readComponent()); + return items[key] === undefined ? undefined : +items[key]; + } + URIStorage.getNumber = getNumber; + /** + * Store a number in the URI, with a corresponding key. + */ + function setNumber(key, value) { + var items = _componentToDict(_readComponent()); + items[key] = '' + value; + _writeComponent(_dictToComponent(items)); + } + URIStorage.setNumber = setNumber; + /** + * Return an object stored in the URI, given a corresponding key. + * Undefined if not found. + */ + function getObject(key) { + var items = _componentToDict(_readComponent()); + return items[key] === undefined ? undefined : JSON.parse(atob(items[key])); + } + URIStorage.getObject = getObject; + /** + * Store an object in the URI, with a corresponding key. + */ + function setObject(key, value) { + var items = _componentToDict(_readComponent()); + items[key] = btoa(JSON.stringify(value)); + _writeComponent(_dictToComponent(items)); + } + URIStorage.setObject = setObject; + /** + * Get a unique storage name for a (Polymer component, propertyName) tuple. + * + * DISAMBIGUATOR must be set on the component, if other components use the + * same propertyName. + */ + function getURIStorageName(component, propertyName) { + var d = component[URIStorage.DISAMBIGUATOR]; + var components = d == null ? [propertyName] : [d, propertyName]; + return components.join('.'); + } + URIStorage.getURIStorageName = getURIStorageName; + /** + * Return a function that: + * (1) Initializes a Polymer boolean property with a default value, if its + * value is not already set + * (2) Sets up listener that updates Polymer property on hash change. + */ + function getBooleanInitializer(propertyName, defaultVal) { + return _getInitializer(getBoolean, propertyName, defaultVal); + } + URIStorage.getBooleanInitializer = getBooleanInitializer; + /** + * Return a function that: + * (1) Initializes a Polymer string property with a default value, if its + * value is not already set + * (2) Sets up listener that updates Polymer property on hash change. + */ + function getStringInitializer(propertyName, defaultVal) { + return _getInitializer(getString, propertyName, defaultVal); + } + URIStorage.getStringInitializer = getStringInitializer; + /** + * Return a function that: + * (1) Initializes a Polymer number property with a default value, if its + * value is not already set + * (2) Sets up listener that updates Polymer property on hash change. + */ + function getNumberInitializer(propertyName, defaultVal) { + return _getInitializer(getNumber, propertyName, defaultVal); + } + URIStorage.getNumberInitializer = getNumberInitializer; + /** + * Return a function that: + * (1) Initializes a Polymer Object property with a default value, if its + * value is not already set + * (2) Sets up listener that updates Polymer property on hash change. + * + * Generates a deep clone of the defaultVal to avoid mutation issues. + */ + function getObjectInitializer(propertyName, defaultVal) { + var clone = _.cloneDeep(defaultVal); + return _getInitializer(getObject, propertyName, clone); + } + URIStorage.getObjectInitializer = getObjectInitializer; + /** + * Return a function that updates URIStorage when a string property changes. + */ + function getBooleanObserver(propertyName, defaultVal) { + return _getObserver(getBoolean, setBoolean, propertyName, defaultVal); + } + URIStorage.getBooleanObserver = getBooleanObserver; + /** + * Return a function that updates URIStorage when a string property changes. + */ + function getStringObserver(propertyName, defaultVal) { + return _getObserver(getString, setString, propertyName, defaultVal); + } + URIStorage.getStringObserver = getStringObserver; + /** + * Return a function that updates URIStorage when a number property changes. + */ + function getNumberObserver(propertyName, defaultVal) { + return _getObserver(getNumber, setNumber, propertyName, defaultVal); + } + URIStorage.getNumberObserver = getNumberObserver; + /** + * Return a function that updates URIStorage when an object property changes. + * Generates a deep clone of the defaultVal to avoid mutation issues. + */ + function getObjectObserver(propertyName, defaultVal) { + var clone = _.cloneDeep(defaultVal); + return _getObserver(getObject, setObject, propertyName, clone); + } + URIStorage.getObjectObserver = getObjectObserver; + /** + * Read component from URI (e.g. returns "events&runPrefix=train*"). + */ + function _readComponent() { + return TF.Globals.USE_HASH ? window.location.hash.slice(1) : + TF.Globals.FAKE_HASH; + } + /** + * Write component to URI. + */ + function _writeComponent(component) { + if (TF.Globals.USE_HASH) { + window.location.hash = component; + } + else { + TF.Globals.FAKE_HASH = component; + } + } + /** + * Convert dictionary of strings into a URI Component. + * All key value entries get added as key value pairs in the component, + * with the exception of a key with the TAB value, which if present + * gets prepended to the URI Component string for backwards comptability + * reasons. + */ + function _dictToComponent(items) { + var component = ''; + // Add the tab name e.g. 'events', 'images', 'histograms' as a prefix + // for backwards compatbility. + if (items[URIStorage.TAB] !== undefined) { + component += items[URIStorage.TAB]; + } + // Join other strings with &key=value notation + var nonTab = _.pairs(items) + .filter(function (pair) { return pair[0] !== URIStorage.TAB; }) + .map(function (pair) { + return encodeURIComponent(pair[0]) + '=' + + encodeURIComponent(pair[1]); + }) + .join('&'); + return nonTab.length > 0 ? (component + '&' + nonTab) : component; + } + /** + * Convert a URI Component into a dictionary of strings. + * Component should consist of key-value pairs joined by a delimiter + * with the exception of the tabName. + * Returns dict consisting of all key-value pairs and + * dict[TAB] = tabName + */ + function _componentToDict(component) { + var items = {}; + var tokens = component.split('&'); + tokens.forEach(function (token) { + var kv = token.split('='); + // Special backwards compatibility for URI components like #events + if (kv.length === 1 && _.contains(TF.Globals.TABS, kv[0])) { + items[URIStorage.TAB] = kv[0]; + } + else if (kv.length === 2) { + items[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]); + } + }); + return items; + } + /** + * Return a function that: + * (1) Initializes a Polymer property with a default value, if its + * value is not already set + * (2) Sets up listener that updates Polymer property on hash change. + */ + function _getInitializer(get, propertyName, defaultVal) { + return function () { + var _this = this; + var URIStorageName = getURIStorageName(this, propertyName); + var setComponentValue = function () { + var uriValue = get(URIStorageName); + _this[propertyName] = uriValue !== undefined ? uriValue : defaultVal; + }; + // Set the value on the property. + setComponentValue(); + // Update it when the hashchanges. + window.addEventListener('hashchange', setComponentValue); + }; + } + /** + * Return a function that updates URIStorage when a property changes. + */ + function _getObserver(get, set, propertyName, defaultVal) { + return function () { + var URIStorageName = getURIStorageName(this, propertyName); + var newVal = this[propertyName]; + if (!_.isEqual(newVal, get(URIStorageName))) { + if (_.isEqual(newVal, defaultVal)) { + _unset(URIStorageName); + } + else { + set(URIStorageName, newVal); + } + } + }; + } + /** + * Delete a key from the URI. + */ + function _unset(key) { + var items = _componentToDict(_readComponent()); + delete items[key]; + _writeComponent(_dictToComponent(items)); + } + })(URIStorage = TF.URIStorage || (TF.URIStorage = {})); +})(TF || (TF = {})); +</script> +</dom-module> <dom-module id="tf-multi-checkbox" assetpath="../tf-multi-checkbox/"> @@ -148,7 +527,7 @@ var TF; <template> <div id="outer-container" class="scrollbar"> - <paper-input id="runs-regex" no-label-float="" label="Write a regex to filter runs" bind-value="{{regexInput}}"></paper-input> + <paper-input id="runs-regex" no-label-float="" label="Write a regex to filter runs" value="{{regexInput}}"></paper-input> <template is="dom-repeat" items="[[namesMatchingRegex]]"> <div class="run-row"> <div class="checkbox-container vertical-align-container"> @@ -229,7 +608,12 @@ var TF; is: "tf-multi-checkbox", properties: { names: Array, // All the runs in consideration - regexInput: {type: String, value: "",}, // Regex for filtering the runs + + regexInput: { + type: String, + value: TF.URIStorage.getStringInitializer("regexInput", ""), + observer: "_regexInputObserver" + }, // Regex for filtering the runs regex: { type: Object, computed: "makeRegex(regexInput)" @@ -320,6 +704,7 @@ var TF; _initializeRuns: function(change) { this.outSelected = change.base.slice(); }, + _regexInputObserver: TF.URIStorage.getStringObserver("regexInput", ""), toggleAll: function() { var _this = this; var allOn = this.namesMatchingRegex @@ -397,6 +782,128 @@ var TF; }); </script> </dom-module> + +<dom-module id="tf-smoothing-input" assetpath="../tf-event-dashboard/"> + <template> + <div class="smoothing-line"> + <paper-checkbox checked="{{enabled}}">Exponential smoothing</paper-checkbox> + <div class="smoothing-block"> + <paper-slider value="{{decay}}" immediate-value="{{_immediateDecayNumberForPaperSlider}}" type="number" step="[[step]]" min="[[min]]" max="[[max]]" disabled="[[!enabled]]"></paper-slider> + <paper-input id="input" label="decay" no-label-float="" value="{{_inputDecayStringForPaperInput}}" type="number" step="[[step]]" min="[[min]]" max="[[max]]" disabled="[[!enabled]]"></paper-input> + </div> + </div> + <style> + .smoothing-line { + margin-top: 15px; + } + + .smoothing-block { + display: flex; + } + + paper-checkbox { + --paper-checkbox-checked-color: var(--tb-ui-dark-accent); + --paper-checkbox-unchecked-color: var(--tb-ui-dark-accent); + font-size: 14px; + } + + paper-slider { + margin-left: 12px; + --paper-slider-knob-color: var(--tb-orange-strong); + --paper-slider-active-color: var(--tb-orange-strong); + flex-grow: 2; + } + + paper-input { + --paper-input-container-focus-color: var(--tb-orange-strong); + --paper-input-container-input: { + font-size: 14px; + }; + --paper-input-container-label: { + font-size: 14px; + }; + width: 60px; + } + </style> + </template> + <script> + Polymer({ + is: "tf-smoothing-input", + + properties: { + enabled: { + type: Boolean, + value: false, + notify: true, + observer: '_enabledChanged' + }, + + step: Number, + max: Number, + min: Number, + + decay: { + type: Number, + value: 0.9, + notify: true + }, + + _immediateDecayNumberForPaperSlider: { + type: Number, + notify: true, + observer: '_immediateDecayNumberForPaperSliderChanged' + }, + + // Paper input treats values as strings even if you specify them as + // numbers. + _inputDecayStringForPaperInput: { + type: String, + notify: true, + observer: '_inputDecayStringForPaperInputChanged' + } + }, + + _updateDecay: _.debounce(function(val) { + this.decay = val; + }, 250), + + _enabledChanged: function() { + // Workaround for paper-input bug on firefox :( + // Element crashes if we change value while it is disabled. + if (this.enabled) { + this.$.input.label = ''; + this._immediateDecayNumberForPaperSliderChanged(); + } + }, + + _immediateDecayNumberForPaperSliderChanged: function() { + if(!this.enabled) { + return; + } + this._inputDecayStringForPaperInput = + this._immediateDecayNumberForPaperSlider.toString(); + this._updateDecay.call(this, this._immediateDecayNumberForPaperSlider); + }, + + _inputDecayStringForPaperInputChanged: function() { + if(!this.enabled) { + return; + } + if (+this._inputDecayStringForPaperInput < 0) { + this._inputDecayStringForPaperInput = '0'; + } + else if (+this._inputDecayStringForPaperInput > 1) { + this._inputDecayStringForPaperInput = '1'; + } + + var d = +this._inputDecayStringForPaperInput; + if (!isNaN(d)) { + this._updateDecay.call(this, d); + } + } + }); + </script> +</dom-module> <style is="custom-style"> :root { @@ -919,7 +1426,7 @@ var Categorizer; </script> </dom-module> -<dom-module id="tf-chart" assetpath="../tf-event-dashboard/"> +<dom-module id="tf-line-chart" assetpath="../tf-event-dashboard/"> <template> <svg id="chartsvg"></svg> <div id="tooltip"> @@ -997,6 +1504,11 @@ var Categorizer; opacity: 1; } + .ghost { + opacity: 0.2; + stroke-width: 1px; + } + </style> </template> <script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. @@ -1119,8 +1631,6 @@ var Plottable; } if (!this.isZoomed) { this.isZoomed = true; - this.xDomainToRestore = this.xScale().domain(); - this.yDomainToRestore = this.yScale().domain(); } this.interpolateZoom(x0, x1, y0, y1); }; @@ -1130,7 +1640,21 @@ var Plottable; return; } this.isZoomed = false; - this.interpolateZoom(this.xDomainToRestore[0], this.xDomainToRestore[1], this.yDomainToRestore[0], this.yDomainToRestore[1]); + // Some Plottable magic follows which ensures that when we un-zoom, we + // un-zoom to the current extent of the data; i.e. if new data was loaded + // since we zoomed, we should un-zoom to the extent of the new data. + // this basically replicates the autoDomain logic in Plottable. + // it uses the internal methods to get the same boundaries that autoDomain + // would, but allows us to interpolate the zoom with a nice animation. + var xScale = this.xScale(); + var yScale = this.yScale(); + xScale._domainMin = null; + xScale._domainMax = null; + yScale._domainMin = null; + yScale._domainMax = null; + var xDomain = xScale._getExtent(); + var yDomain = yScale._getExtent(); + this.interpolateZoom(xDomain[0], xDomain[1], yDomain[0], yDomain[1]); }; // If we are zooming, disable interactions, to avoid contention DragZoomLayer.prototype.isZooming = function (isZooming) { @@ -1178,12 +1702,7 @@ var Plottable; Plottable.DragZoomLayer = DragZoomLayer; })(Plottable || (Plottable = {})); </script> - <script>var __extends = (this && this.__extends) || function (d, b) { - for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); -}; -/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. + <script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1197,63 +1716,40 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ +/* tslint:disable:no-namespace variable-name */ var TF; (function (TF) { - var Y_TOOLTIP_FORMATTER_PRECISION = 4; - var STEP_FORMATTER_PRECISION = 4; - var Y_AXIS_FORMATTER_PRECISION = 3; - var TOOLTIP_Y_PIXEL_OFFSET = 20; - var TOOLTIP_CIRCLE_SIZE = 4; - var NAN_SYMBOL_SIZE = 6; - var BaseChart = (function () { - function BaseChart(tag, dataFn, xType, colorScale, tooltip) { + var LineChart = (function () { + function LineChart(tag, dataFn, xType, colorScale, tooltip) { this.dataFn = dataFn; this.run2datasets = {}; this.tag = tag; this.colorScale = colorScale; this.tooltip = tooltip; + this.datasets = []; + this.smoothDatasets = []; + this.run2smoothDatasets = {}; + // lastPointDataset is a dataset that contains just the last point of + // every dataset we're currently drawing. + this.lastPointsDataset = new Plottable.Dataset(); + this.nanDataset = new Plottable.Dataset(); + // need to do a single bind, so we can deregister the callback from + // old Plottable.Datasets. (Deregistration is done by identity checks.) + this.onDatasetChanged = this._onDatasetChanged.bind(this); + this.buildChart(xType); } - /** - * Change the runs on the chart. The work of actually setting the dataset - * on the plot is deferred to the subclass because it is impl-specific. - * Changing runs automatically triggers a reload; this ensures that the - * newly selected run will have data, and that all the runs will be current - * (it would be weird if one run was ahead of the others, and the display - * depended on the order in which runs were added) - */ - BaseChart.prototype.changeRuns = function (runs) { - this.runs = runs; - this.reload(); - }; - /** - * Reload data for each run in view. - */ - BaseChart.prototype.reload = function () { - var _this = this; - this.runs.forEach(function (run) { - var dataset = _this.getDataset(run); - _this.dataFn(_this.tag, run).then(function (x) { return dataset.data(x); }); - }); - }; - BaseChart.prototype.getDataset = function (run) { - if (this.run2datasets[run] === undefined) { - this.run2datasets[run] = - new Plottable.Dataset([], { run: run, tag: this.tag }); - } - return this.run2datasets[run]; - }; - BaseChart.prototype.buildChart = function (xType) { + LineChart.prototype.buildChart = function (xType) { if (this.outer) { this.outer.destroy(); } - var xComponents = getXComponents(xType); + var xComponents = TF.ChartHelpers.getXComponents(xType); this.xAccessor = xComponents.accessor; this.xScale = xComponents.scale; this.xAxis = xComponents.axis; this.xAxis.margin(0).tickLabelPadding(3); this.yScale = new Plottable.Scales.Linear(); this.yAxis = new Plottable.Axes.Numeric(this.yScale, 'left'); - var yFormatter = multiscaleFormatter(Y_AXIS_FORMATTER_PRECISION); + var yFormatter = TF.ChartHelpers.multiscaleFormatter(TF.ChartHelpers.Y_AXIS_FORMATTER_PRECISION); this.yAxis.margin(0).tickLabelPadding(5).formatter(yFormatter); this.yAxis.usesTextWidthApproximation(true); this.dzl = new Plottable.DragZoomLayer(this.xScale, this.yScale); @@ -1267,35 +1763,6 @@ var TF; [null, this.xAxis] ]); }; - BaseChart.prototype.buildPlot = function (xAccessor, xScale, yScale) { - throw new Error('Abstract method not implemented.'); - }; - BaseChart.prototype.renderTo = function (target) { - this.outer.renderTo(target); - }; - BaseChart.prototype.redraw = function () { - this.outer.redraw(); - }; - BaseChart.prototype.destroy = function () { - this.outer.destroy(); - }; - return BaseChart; - }()); - TF.BaseChart = BaseChart; - var LineChart = (function (_super) { - __extends(LineChart, _super); - function LineChart(tag, dataFn, xType, colorScale, tooltip) { - _super.call(this, tag, dataFn, xType, colorScale, tooltip); - this.datasets = []; - // lastPointDataset is a dataset that contains just the last point of - // every dataset we're currently drawing. - this.lastPointsDataset = new Plottable.Dataset(); - this.nanDataset = new Plottable.Dataset(); - // need to do a single bind, so we can deregister the callback from - // old Plottable.Datasets. (Deregistration is done by identity checks.) - this.updateSpecialDatasets = this._updateSpecialDatasets.bind(this); - this.buildChart(xType); - } LineChart.prototype.buildPlot = function (xAccessor, xScale, yScale) { var _this = this; this.yAccessor = function (d) { return d.scalar; }; @@ -1307,6 +1774,13 @@ var TF; }); this.linePlot = linePlot; var group = this.setupTooltips(linePlot); + var smoothLinePlot = new Plottable.Plots.Line(); + smoothLinePlot.x(xAccessor, xScale); + smoothLinePlot.y(this.yAccessor, yScale); + smoothLinePlot.attr('stroke', function (d, i, dataset) { + return _this.colorScale.scale(dataset.metadata().run); + }); + this.smoothLinePlot = smoothLinePlot; // The scatterPlot will display the last point for each dataset. // This way, if there is only one datum for the series, it is still // visible. We hide it when tooltips are active to keep things clean. @@ -1315,7 +1789,7 @@ var TF; scatterPlot.y(this.yAccessor, yScale); scatterPlot.attr('fill', function (d) { return _this.colorScale.scale(d.run); }); scatterPlot.attr('opacity', 1); - scatterPlot.size(TOOLTIP_CIRCLE_SIZE * 2); + scatterPlot.size(TF.ChartHelpers.TOOLTIP_CIRCLE_SIZE * 2); scatterPlot.datasets([this.lastPointsDataset]); this.scatterPlot = scatterPlot; var nanDisplay = new Plottable.Plots.Scatter(); @@ -1323,19 +1797,31 @@ var TF; nanDisplay.y(function (x) { return x.displayY; }, yScale); nanDisplay.attr('fill', function (d) { return _this.colorScale.scale(d.run); }); nanDisplay.attr('opacity', 1); - nanDisplay.size(NAN_SYMBOL_SIZE * 2); + nanDisplay.size(TF.ChartHelpers.NAN_SYMBOL_SIZE * 2); nanDisplay.datasets([this.nanDataset]); nanDisplay.symbol(Plottable.SymbolFactories.triangleUp); this.nanDisplay = nanDisplay; - return new Plottable.Components.Group([nanDisplay, scatterPlot, group]); + return new Plottable.Components.Group([nanDisplay, scatterPlot, smoothLinePlot, group]); + }; + /** Updates the chart when a dataset changes. Called every time the data of + * a dataset changes to update the charts. + */ + LineChart.prototype._onDatasetChanged = function (dataset) { + if (this.smoothingEnabled) { + this.smoothDataset(this.getSmoothDataset(dataset.metadata().run)); + this.updateSpecialDatasets(this.smoothDatasets); + } + else { + this.updateSpecialDatasets(this.datasets); + } }; /** Constructs special datasets. Each special dataset contains exceptional - * values from all of the regular datasetes, e.g. last points in series, or + * values from all of the regular datasets, e.g. last points in series, or * NaN values. Those points will have a `run` and `relative` property added * (since usually those are context in the surrounding dataset). */ - LineChart.prototype._updateSpecialDatasets = function () { - var lastPointsData = this.datasets + LineChart.prototype.updateSpecialDatasets = function (datasets) { + var lastPointsData = datasets .map(function (d) { var datum = null; // filter out NaNs to ensure last point is a clean one @@ -1344,7 +1830,8 @@ var TF; var idx = nonNanData.length - 1; datum = nonNanData[idx]; datum.run = d.metadata().run; - datum.relative = relativeAccessor(datum, -1, d); + datum.relative = + TF.ChartHelpers.relativeAccessor(datum, -1, d); } return datum; }) @@ -1374,13 +1861,13 @@ var TF; else { data[i].run = d.metadata().run; data[i].displayY = displayY; - data[i].relative = relativeAccessor(data[i], -1, d); + data[i].relative = TF.ChartHelpers.relativeAccessor(data[i], -1, d); nanData.push(data[i]); } } return nanData; }; - var nanData = _.flatten(this.datasets.map(datasetToNaNData)); + var nanData = _.flatten(datasets.map(datasetToNaNData)); this.nanDataset.data(nanData); }; LineChart.prototype.setupTooltips = function (plot) { @@ -1415,13 +1902,14 @@ var TF; dataset: null, }; var centerBBox = _this.gridlines.content().node().getBBox(); - var points = plot.datasets().map(function (dataset) { return _this.findClosestPoint(target, dataset); }); + var datasets = _this.smoothingEnabled ? _this.smoothDatasets : plot.datasets(); + var points = datasets.map(function (dataset) { return _this.findClosestPoint(target, dataset); }); var pointsToCircle = points.filter(function (p) { return p != null && Plottable.Utils.DOM.intersectsBBox(p.x, p.y, centerBBox); }); var pts = pointsComponent.content().selectAll('.point').data(pointsToCircle, function (p) { return p.dataset.metadata().run; }); if (points.length !== 0) { pts.enter().append('circle').classed('point', true); - pts.attr('r', TOOLTIP_CIRCLE_SIZE) + pts.attr('r', TF.ChartHelpers.TOOLTIP_CIRCLE_SIZE) .attr('cx', function (p) { return p.x; }) .attr('cy', function (p) { return p.y; }) .style('stroke', 'none') @@ -1440,7 +1928,7 @@ var TF; var _this = this; // Formatters for value, step, and wall_time this.scatterPlot.attr('opacity', 0); - var valueFormatter = multiscaleFormatter(Y_TOOLTIP_FORMATTER_PRECISION); + var valueFormatter = TF.ChartHelpers.multiscaleFormatter(TF.ChartHelpers.Y_TOOLTIP_FORMATTER_PRECISION); var dist = function (p) { return Math.pow(p.x - target.x, 2) + Math.pow(p.y - target.y, 2); }; @@ -1483,9 +1971,9 @@ var TF; rows.append('td').text(function (d) { return isNaN(d.datum.scalar) ? 'NaN' : valueFormatter(d.datum.scalar); }); - rows.append('td').text(function (d) { return stepFormatter(d.datum.step); }); - rows.append('td').text(function (d) { return timeFormatter(d.datum.wall_time); }); - rows.append('td').text(function (d) { return relativeFormatter(relativeAccessor(d.datum, -1, d.dataset)); }); + rows.append('td').text(function (d) { return TF.ChartHelpers.stepFormatter(d.datum.step); }); + rows.append('td').text(function (d) { return TF.ChartHelpers.timeFormatter(d.datum.wall_time); }); + rows.append('td').text(function (d) { return TF.ChartHelpers.relativeFormatter(TF.ChartHelpers.relativeAccessor(d.datum, -1, d.dataset)); }); // compute left position var documentWidth = document.body.clientWidth; var node = this.tooltip.node(); @@ -1495,12 +1983,13 @@ var TF; var left = Math.min(0, documentWidth - parentRect.left - nodeRect.width - 60); this.tooltip.style('left', left + 'px'); // compute top position - if (parentRect.bottom + nodeRect.height + TOOLTIP_Y_PIXEL_OFFSET < + if (parentRect.bottom + nodeRect.height + + TF.ChartHelpers.TOOLTIP_Y_PIXEL_OFFSET < document.body.clientHeight) { - this.tooltip.style('top', parentRect.bottom + TOOLTIP_Y_PIXEL_OFFSET); + this.tooltip.style('top', parentRect.bottom + TF.ChartHelpers.TOOLTIP_Y_PIXEL_OFFSET); } else { - this.tooltip.style('bottom', parentRect.top - TOOLTIP_Y_PIXEL_OFFSET); + this.tooltip.style('bottom', parentRect.top - TF.ChartHelpers.TOOLTIP_Y_PIXEL_OFFSET); } this.tooltip.style('opacity', 1); }; @@ -1531,191 +2020,253 @@ var TF; return prevDist < nextDist ? prev : next; } }; + LineChart.prototype.getSmoothDataset = function (run) { + if (this.run2smoothDatasets[run] === undefined) { + this.run2smoothDatasets[run] = + new Plottable.Dataset([], { run: run, tag: this.tag }); + } + return this.run2smoothDatasets[run]; + }; + LineChart.prototype.smoothDataset = function (dataset) { + var _this = this; + var data = this.getDataset(dataset.metadata().run).data(); + // EMA with first step initialized to first element. + var smoothedData = _.cloneDeep(data); + smoothedData.forEach(function (d, i) { + if (i === 0) { + return; + } + d.scalar = (1.0 - _this.smoothingDecay) * d.scalar + + _this.smoothingDecay * smoothedData[i - 1].scalar; + }); + dataset.data(smoothedData); + }; + LineChart.prototype.getDataset = function (run) { + if (this.run2datasets[run] === undefined) { + this.run2datasets[run] = + new Plottable.Dataset([], { run: run, tag: this.tag }); + } + return this.run2datasets[run]; + }; + /** + * Change the runs on the chart. + * Changing runs automatically triggers a reload; this ensures that the + * newly selected run will have data, and that all the runs will be current + * (it would be weird if one run was ahead of the others, and the display + * depended on the order in which runs were added) + */ LineChart.prototype.changeRuns = function (runs) { var _this = this; - _super.prototype.changeRuns.call(this, runs); + this.runs = runs; + this.reload(); runs.reverse(); // draw first run on top - this.datasets.forEach(function (d) { return d.offUpdate(_this.updateSpecialDatasets); }); + this.datasets.forEach(function (d) { return d.offUpdate(_this.onDatasetChanged); }); this.datasets = runs.map(function (r) { return _this.getDataset(r); }); - this.datasets.forEach(function (d) { return d.onUpdate(_this.updateSpecialDatasets); }); + this.datasets.forEach(function (d) { return d.onUpdate(_this.onDatasetChanged); }); this.linePlot.datasets(this.datasets); + if (this.smoothingEnabled) { + this.smoothDatasets = runs.map(function (r) { return _this.getSmoothDataset(r); }); + this.smoothLinePlot.datasets(this.smoothDatasets); + } }; - return LineChart; - }(BaseChart)); - TF.LineChart = LineChart; - var HistogramChart = (function (_super) { - __extends(HistogramChart, _super); - function HistogramChart(tag, dataFn, xType, colorScale, tooltip) { - _super.call(this, tag, dataFn, xType, colorScale, tooltip); - this.buildChart(xType); - } - HistogramChart.prototype.changeRuns = function (runs) { + LineChart.prototype.smoothingUpdate = function (decay) { var _this = this; - _super.prototype.changeRuns.call(this, runs); - var datasets = runs.map(function (r) { return _this.getDataset(r); }); - this.plots.forEach(function (p) { return p.datasets(datasets); }); + if (!this.smoothingEnabled) { + this.linePlot.addClass('ghost'); + this.smoothingEnabled = true; + this.smoothDatasets = this.runs.map(function (r) { return _this.getSmoothDataset(r); }); + this.smoothLinePlot.datasets(this.smoothDatasets); + } + this.smoothingDecay = decay; + this.smoothDatasets.forEach(function (d) { return _this.smoothDataset(d); }); + this.updateSpecialDatasets(this.smoothDatasets); }; - HistogramChart.prototype.buildPlot = function (xAccessor, xScale, yScale) { - var _this = this; - var percents = [0, 228, 1587, 3085, 5000, 6915, 8413, 9772, 10000]; - var opacities = _.range(percents.length - 1) - .map(function (i) { return (percents[i + 1] - percents[i]) / 2500; }); - var accessors = percents.map(function (p, i) { return function (datum) { return datum[i][1]; }; }); - var median = 4; - var medianAccessor = accessors[median]; - var plots = _.range(accessors.length - 1).map(function (i) { - var p = new Plottable.Plots.Area(); - p.x(xAccessor, xScale); - var y0 = i > median ? accessors[i] : accessors[i + 1]; - var y = i > median ? accessors[i + 1] : accessors[i]; - p.y(y, yScale); - p.y0(y0); - p.attr('fill', function (d, i, dataset) { - return _this.colorScale.scale(dataset.metadata().run); - }); - p.attr('stroke', function (d, i, dataset) { - return _this.colorScale.scale(dataset.metadata().run); - }); - p.attr('stroke-weight', function (d, i, m) { return '0.5px'; }); - p.attr('stroke-opacity', function () { return opacities[i]; }); - p.attr('fill-opacity', function () { return opacities[i]; }); - return p; - }); - var medianPlot = new Plottable.Plots.Line(); - medianPlot.x(xAccessor, xScale); - medianPlot.y(medianAccessor, yScale); - medianPlot.attr('stroke', function (d, i, m) { return _this.colorScale.scale(m.run); }); - this.plots = plots; - return new Plottable.Components.Group(plots); - }; - return HistogramChart; - }(BaseChart)); - TF.HistogramChart = HistogramChart; - /* Create a formatter function that will switch between exponential and - * regular display depending on the scale of the number being formatted, - * and show `digits` significant digits. - */ - function multiscaleFormatter(digits) { - return function (v) { - var absv = Math.abs(v); - if (absv < 1E-15) { - // Sometimes zero-like values get an annoying representation - absv = 0; - } - var f; - if (absv >= 1E4) { - f = d3.format('.' + digits + 'e'); - } - else if (absv > 0 && absv < 0.01) { - f = d3.format('.' + digits + 'e'); + LineChart.prototype.smoothingDisable = function () { + if (this.smoothingEnabled) { + this.linePlot.removeClass('ghost'); + this.smoothDatasets = []; + this.smoothLinePlot.datasets(this.smoothDatasets); + this.smoothingEnabled = false; + this.updateSpecialDatasets(this.datasets); } - else { - f = d3.format('.' + digits + 'g'); - } - return f(v); }; - } - function accessorize(key) { - return function (d, index, dataset) { return d[key]; }; - } - var stepFormatter = Plottable.Formatters.siSuffix(STEP_FORMATTER_PRECISION); - function stepX() { - var scale = new Plottable.Scales.Linear(); - var axis = new Plottable.Axes.Numeric(scale, 'bottom'); - axis.formatter(stepFormatter); - return { - scale: scale, - axis: axis, - accessor: function (d) { return d.step; }, - }; - } - var timeFormatter = Plottable.Formatters.time('%a %b %e, %H:%M:%S'); - function wallX() { - var scale = new Plottable.Scales.Time(); - return { - scale: scale, - axis: new Plottable.Axes.Time(scale, 'bottom'), - accessor: function (d) { return d.wall_time; }, + /** + * Reload data for each run in view. + */ + LineChart.prototype.reload = function () { + var _this = this; + this.runs.forEach(function (run) { + var dataset = _this.getDataset(run); + _this.dataFn(_this.tag, run).then(function (x) { return dataset.data(x); }); + }); }; - } - var relativeAccessor = function (d, index, dataset) { - // We may be rendering the final-point datum for scatterplot. - // If so, we will have already provided the 'relative' property - if (d.relative != null) { - return d.relative; + LineChart.prototype.renderTo = function (target) { this.outer.renderTo(target); }; + LineChart.prototype.redraw = function () { this.outer.redraw(); }; + LineChart.prototype.destroy = function () { this.outer.destroy(); }; + return LineChart; + }()); + TF.LineChart = LineChart; +})(TF || (TF = {})); +</script> + <script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/* tslint:disable:no-namespace variable-name */ +var TF; +(function (TF) { + var ChartHelpers; + (function (ChartHelpers) { + ChartHelpers.Y_TOOLTIP_FORMATTER_PRECISION = 4; + ChartHelpers.STEP_FORMATTER_PRECISION = 4; + ChartHelpers.Y_AXIS_FORMATTER_PRECISION = 3; + ChartHelpers.TOOLTIP_Y_PIXEL_OFFSET = 20; + ChartHelpers.TOOLTIP_CIRCLE_SIZE = 4; + ChartHelpers.NAN_SYMBOL_SIZE = 6; + /* Create a formatter function that will switch between exponential and + * regular display depending on the scale of the number being formatted, + * and show `digits` significant digits. + */ + function multiscaleFormatter(digits) { + return function (v) { + var absv = Math.abs(v); + if (absv < 1E-15) { + // Sometimes zero-like values get an annoying representation + absv = 0; + } + var f; + if (absv >= 1E4) { + f = d3.format('.' + digits + 'e'); + } + else if (absv > 0 && absv < 0.01) { + f = d3.format('.' + digits + 'e'); + } + else { + f = d3.format('.' + digits + 'g'); + } + return f(v); + }; } - var data = 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].wall_time : 0; - return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours - }; - var relativeFormatter = function (n) { - // we will always show 2 units of precision, e.g days and hours, or - // minutes and seconds, but not hours and minutes and seconds - var ret = ''; - var days = Math.floor(n / 24); - n -= (days * 24); - if (days) { - ret += days + 'd '; + ChartHelpers.multiscaleFormatter = multiscaleFormatter; + function accessorize(key) { + return function (d, index, dataset) { return d[key]; }; } - var hours = Math.floor(n); - n -= hours; - n *= 60; - if (hours || days) { - ret += hours + 'h '; + ChartHelpers.accessorize = accessorize; + ChartHelpers.stepFormatter = Plottable.Formatters.siSuffix(ChartHelpers.STEP_FORMATTER_PRECISION); + function stepX() { + var scale = new Plottable.Scales.Linear(); + var axis = new Plottable.Axes.Numeric(scale, 'bottom'); + axis.formatter(ChartHelpers.stepFormatter); + return { + scale: scale, + axis: axis, + accessor: function (d) { return d.step; }, + }; } - var minutes = Math.floor(n); - n -= minutes; - n *= 60; - if (minutes || hours || days) { - ret += minutes + 'm '; + ChartHelpers.stepX = stepX; + ChartHelpers.timeFormatter = Plottable.Formatters.time('%a %b %e, %H:%M:%S'); + function wallX() { + var scale = new Plottable.Scales.Time(); + return { + scale: scale, + axis: new Plottable.Axes.Time(scale, 'bottom'), + accessor: function (d) { return d.wall_time; }, + }; } - var seconds = Math.floor(n); - return ret + seconds + 's'; - }; - function relativeX() { - var scale = new Plottable.Scales.Linear(); - return { - scale: scale, - axis: new Plottable.Axes.Numeric(scale, 'bottom'), - accessor: relativeAccessor, + ChartHelpers.wallX = wallX; + ChartHelpers.relativeAccessor = function (d, index, dataset) { + // We may be rendering the final-point datum for scatterplot. + // If so, we will have already provided the 'relative' property + if (d.relative != null) { + return d.relative; + } + var data = 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].wall_time : 0; + return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours }; - } - // a very literal definition of NaN: true for NaN for a non-number type - // or null, etc. False for Infinity or -Infinity - var isNaN = function (x) { return +x !== x; }; - function getXComponents(xType) { - switch (xType) { - case 'step': - return stepX(); - case 'wall_time': - return wallX(); - case 'relative': - return relativeX(); - default: - throw new Error('invalid xType: ' + xType); + ChartHelpers.relativeFormatter = function (n) { + // we will always show 2 units of precision, e.g days and hours, or + // minutes and seconds, but not hours and minutes and seconds + var ret = ''; + var days = Math.floor(n / 24); + n -= (days * 24); + if (days) { + ret += days + 'd '; + } + var hours = Math.floor(n); + n -= hours; + n *= 60; + if (hours || days) { + ret += hours + 'h '; + } + var minutes = Math.floor(n); + n -= minutes; + n *= 60; + if (minutes || hours || days) { + ret += minutes + 'm '; + } + var seconds = Math.floor(n); + return ret + seconds + 's'; + }; + function relativeX() { + var scale = new Plottable.Scales.Linear(); + return { + scale: scale, + axis: new Plottable.Axes.Numeric(scale, 'bottom'), + accessor: ChartHelpers.relativeAccessor, + }; } - } + ChartHelpers.relativeX = relativeX; + // a very literal definition of NaN: true for NaN for a non-number type + // or null, etc. False for Infinity or -Infinity + ChartHelpers.isNaN = function (x) { return +x !== x; }; + function getXComponents(xType) { + switch (xType) { + case 'step': + return stepX(); + case 'wall_time': + return wallX(); + case 'relative': + return relativeX(); + default: + throw new Error('invalid xType: ' + xType); + } + } + ChartHelpers.getXComponents = getXComponents; + })(ChartHelpers = TF.ChartHelpers || (TF.ChartHelpers = {})); })(TF || (TF = {})); </script> <script> Polymer({ - is: "tf-chart", + is: "tf-line-chart", properties: { - type: String, // "scalar" or "compressedHistogram" _chart: Object, colorScale: Object, tag: String, selectedRuns: Array, + smoothingDecay: Number, + smoothingEnabled: Boolean, xType: String, dataProvider: Function, _initialized: Boolean, }, observers: [ - "_makeChart(type, tag, dataProvider, xType, colorScale, _initialized)", - "_changeRuns(_chart, selectedRuns.*)" + "_makeChart(tag, dataProvider, xType, colorScale, _initialized)", + "_changeRuns(_chart, selectedRuns.*)", + "_smoothingChanged(smoothingDecay, smoothingEnabled, _chart)" ], _changeRuns: function(chart) { if (chart.tag !== this.tag) { @@ -1730,30 +2281,32 @@ var TF; reload: function() { this._chart.reload(); }, - _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, dataProvider, xType, colorScale, _initialized) { + _makeChart: function(tag, dataProvider, xType, colorScale, _initialized) { if (!_initialized) { return; } if (this._chart) this._chart.destroy(); - var cns = this._constructor(type); var tooltip = d3.select(this.$.tooltip); this.scopeSubtree(this.$.tooltip, true); - var chart = new cns(tag, dataProvider, xType, colorScale, tooltip); + var chart = new TF.LineChart(tag, dataProvider, xType, colorScale, tooltip); var svg = d3.select(this.$.chartsvg); this.async(function() { chart.renderTo(svg); + this.scopeSubtree(this.$.chartsvg, true); this._chart = chart; }, 350); }, + _smoothingChanged: function() { + if(!this._chart) { + return; + } + if(this.smoothingEnabled) { + this._chart.smoothingUpdate(this.smoothingDecay); + } + else { + this._chart.smoothingDisable(); + } + }, attached: function() { this._initialized = true; }, @@ -2168,7 +2721,7 @@ var TF; /** * ReloadBehavior: A simple behavior for dashboards where the * frontendReload() function should find every child element with a - * given tag name (e.g. "tf-chart" or "tf-image-loader") + * given tag name (e.g. "tf-line-chart" or "tf-image-loader") * and call a `reload` method on that child. * May later extend it so it has more sophisticated logic, e.g. reloading * only tags that are in view. @@ -2895,7 +3448,8 @@ var TF; <div class="sidebar"> <div class="sidebar-section"> <tf-categorizer id="categorizer" tags="[[_visibleTags]]" categories="{{categories}}"></tf-categorizer> - <paper-checkbox id="download-option" checked="{{_show_download_links}}">Data download links</paper-checkbox> + <paper-checkbox id="download-option" checked="{{_showDownloadLinks}}">Data download links</paper-checkbox> + <tf-smoothing-input enabled="{{_smoothingEnabled}}" decay="{{_smoothingDecay}}" step="0.001" min="0" max="1"></tf-smoothing-input> </div> <div class="sidebar-section"> <tf-x-type-selector id="xTypeSelector" out-x-type="{{xType}}"></tf-x-type-selector> @@ -2913,10 +3467,10 @@ var TF; <div class="card"> <span class="card-title">[[tag]]</span> <div class="card-content"> - <tf-chart tag="[[tag]]" data-provider="[[dataProvider]]" type="scalar" id="chart" selected-runs="[[validRuns(tag, selectedRuns.*, run2tag.*)]]" x-type="[[xType]]" color-scale="[[colorScale]]" on-keyup="toggleSelected" tabindex="2"></tf-chart> - <paper-icon-button class="expand-button" shift$="[[_show_download_links]]" icon="fullscreen" on-tap="toggleSelected"></paper-icon-button> + <tf-line-chart tag="[[tag]]" data-provider="[[dataProvider]]" id="chart" selected-runs="[[validRuns(tag, selectedRuns.*, run2tag.*)]]" x-type="[[xType]]" color-scale="[[colorScale]]" smoothing-decay="[[_smoothingDecay]]" smoothing-enabled="[[_smoothingEnabled]]" on-keyup="toggleSelected" tabindex="2"></tf-line-chart> + <paper-icon-button class="expand-button" shift$="[[_showDownloadLinks]]" icon="fullscreen" on-tap="toggleSelected"></paper-icon-button> </div> - <template is="dom-if" if="[[_show_download_links]]"> + <template is="dom-if" if="[[_showDownloadLinks]]"> <div class="card-bottom-row"> <tf-downloader selected-runs="[[selectedRuns]]" tag="[[tag]]" url-fn="[[scalarUrl]]" run-to-tag="[[run2tag]]"> </tf-downloader> @@ -2938,7 +3492,7 @@ var TF; Polymer({ is: "tf-event-dashboard", behaviors: [ - TF.Dashboard.ReloadBehavior("tf-chart"), + TF.Dashboard.ReloadBehavior("tf-line-chart"), TF.Backend.Behavior, ], properties: { @@ -2952,7 +3506,15 @@ var TF; type: Array, computed: "_getVisibleTags(selectedRuns.*, run2tag.*)" }, - _show_download_links: Boolean, + _showDownloadLinks: { + type: Boolean, + notify: true, + value: TF.URIStorage.getBooleanInitializer('_showDownloadLinks', + false), + observer: '_showDownloadLinksObserver' + }, + _smoothingDecay: Number, + _smoothingEnabled: Boolean, colorScale: { type: Object, notify: true, @@ -2963,9 +3525,9 @@ var TF; this.fire("rendered"); }); }, - observers: ['redraw(_show_download_links)'], - redraw: function(_show_download_links) { - var els = this.getElementsByTagName("tf-chart"); + observers: ['redraw(_showDownloadLinks)'], + redraw: function(_showDownloadLinks) { + var els = this.getElementsByTagName("tf-line-chart"); for (var i=0; i<els.length; i++) { els[i].redraw(); } @@ -2978,6 +3540,8 @@ var TF; _getScalarUrl: function() { return this.router.scalars; }, + _showDownloadLinksObserver: TF.URIStorage.getBooleanObserver( + '_showDownloadLinks', false), toggleSelected: function(e) { var currentTarget = Polymer.dom(e.currentTarget); var parentDiv = currentTarget.parentNode.parentNode; @@ -2997,6 +3561,338 @@ var TF; }); </script> </dom-module> +<dom-module id="tf-obsolete-histogram-chart" assetpath="../tf-histogram-dashboard/"> + <template> + <svg id="chartsvg"></svg> + <style> + :host { + -webkit-user-select: none; + -moz-user-select: none; + display: flex; + flex-direction: column; + flex-grow: 1; + flex-shrink: 1; + position: relative; + } + svg { + -webkit-user-select: none; + -moz-user-select: none; + flex-grow: 1; + flex-shrink: 1; + } + + </style> + </template> + <script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/* tslint:disable:no-namespace variable-name */ +var TF; +(function (TF) { + var HistogramChart = (function () { + function HistogramChart(tag, dataFn, xType, colorScale, tooltip) { + this.dataFn = dataFn; + this.run2datasets = {}; + this.tag = tag; + this.colorScale = colorScale; + this.tooltip = tooltip; + this.buildChart(xType); + } + /** + * Change the runs on the chart. The work of actually setting the dataset + * on the plot is deferred to the subclass because it is impl-specific. + * Changing runs automatically triggers a reload; this ensures that the + * newly selected run will have data, and that all the runs will be current + * (it would be weird if one run was ahead of the others, and the display + * depended on the order in which runs were added) + */ + HistogramChart.prototype.changeRuns = function (runs) { + var _this = this; + this.runs = runs; + this.reload(); + var datasets = runs.map(function (r) { return _this.getDataset(r); }); + this.plots.forEach(function (p) { return p.datasets(datasets); }); + }; + /** + * Reload data for each run in view. + */ + HistogramChart.prototype.reload = function () { + var _this = this; + this.runs.forEach(function (run) { + var dataset = _this.getDataset(run); + _this.dataFn(_this.tag, run).then(function (x) { return dataset.data(x); }); + }); + }; + HistogramChart.prototype.getDataset = function (run) { + if (this.run2datasets[run] === undefined) { + this.run2datasets[run] = + new Plottable.Dataset([], { run: run, tag: this.tag }); + } + return this.run2datasets[run]; + }; + HistogramChart.prototype.buildChart = function (xType) { + if (this.outer) { + this.outer.destroy(); + } + var xComponents = TF.ChartHelpers.getXComponents(xType); + this.xAccessor = xComponents.accessor; + this.xScale = xComponents.scale; + this.xAxis = xComponents.axis; + this.xAxis.margin(0).tickLabelPadding(3); + this.yScale = new Plottable.Scales.Linear(); + this.yAxis = new Plottable.Axes.Numeric(this.yScale, 'left'); + var yFormatter = TF.ChartHelpers.multiscaleFormatter(TF.ChartHelpers.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); + this.center = new Plottable.Components.Group([this.gridlines, center]); + this.outer = new Plottable.Components.Table([[this.yAxis, this.center], [null, this.xAxis]]); + }; + HistogramChart.prototype.buildPlot = function (xAccessor, xScale, yScale) { + var _this = this; + var percents = [0, 228, 1587, 3085, 5000, 6915, 8413, 9772, 10000]; + var opacities = _.range(percents.length - 1) + .map(function (i) { return (percents[i + 1] - percents[i]) / 2500; }); + var accessors = percents.map(function (p, i) { return function (datum) { return datum[i][1]; }; }); + var median = 4; + var medianAccessor = accessors[median]; + var plots = _.range(accessors.length - 1).map(function (i) { + var p = new Plottable.Plots.Area(); + p.x(xAccessor, xScale); + var y0 = i > median ? accessors[i] : accessors[i + 1]; + var y = i > median ? accessors[i + 1] : accessors[i]; + p.y(y, yScale); + p.y0(y0); + p.attr('fill', function (d, i, dataset) { + return _this.colorScale.scale(dataset.metadata().run); + }); + p.attr('stroke', function (d, i, dataset) { + return _this.colorScale.scale(dataset.metadata().run); + }); + p.attr('stroke-weight', function (d, i, m) { return '0.5px'; }); + p.attr('stroke-opacity', function () { return opacities[i]; }); + p.attr('fill-opacity', function () { return opacities[i]; }); + return p; + }); + var medianPlot = new Plottable.Plots.Line(); + medianPlot.x(xAccessor, xScale); + medianPlot.y(medianAccessor, yScale); + medianPlot.attr('stroke', function (d, i, m) { return _this.colorScale.scale(m.run); }); + this.plots = plots; + return new Plottable.Components.Group(plots); + }; + HistogramChart.prototype.renderTo = function (target) { this.outer.renderTo(target); }; + HistogramChart.prototype.redraw = function () { this.outer.redraw(); }; + HistogramChart.prototype.destroy = function () { this.outer.destroy(); }; + return HistogramChart; + }()); + TF.HistogramChart = HistogramChart; +})(TF || (TF = {})); +</script> + <script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/* tslint:disable:no-namespace variable-name */ +var TF; +(function (TF) { + var ChartHelpers; + (function (ChartHelpers) { + ChartHelpers.Y_TOOLTIP_FORMATTER_PRECISION = 4; + ChartHelpers.STEP_FORMATTER_PRECISION = 4; + ChartHelpers.Y_AXIS_FORMATTER_PRECISION = 3; + ChartHelpers.TOOLTIP_Y_PIXEL_OFFSET = 20; + ChartHelpers.TOOLTIP_CIRCLE_SIZE = 4; + ChartHelpers.NAN_SYMBOL_SIZE = 6; + /* Create a formatter function that will switch between exponential and + * regular display depending on the scale of the number being formatted, + * and show `digits` significant digits. + */ + function multiscaleFormatter(digits) { + return function (v) { + var absv = Math.abs(v); + if (absv < 1E-15) { + // Sometimes zero-like values get an annoying representation + absv = 0; + } + var f; + if (absv >= 1E4) { + f = d3.format('.' + digits + 'e'); + } + else if (absv > 0 && absv < 0.01) { + f = d3.format('.' + digits + 'e'); + } + else { + f = d3.format('.' + digits + 'g'); + } + return f(v); + }; + } + ChartHelpers.multiscaleFormatter = multiscaleFormatter; + function accessorize(key) { + return function (d, index, dataset) { return d[key]; }; + } + ChartHelpers.accessorize = accessorize; + ChartHelpers.stepFormatter = Plottable.Formatters.siSuffix(ChartHelpers.STEP_FORMATTER_PRECISION); + function stepX() { + var scale = new Plottable.Scales.Linear(); + var axis = new Plottable.Axes.Numeric(scale, 'bottom'); + axis.formatter(ChartHelpers.stepFormatter); + return { + scale: scale, + axis: axis, + accessor: function (d) { return d.step; }, + }; + } + ChartHelpers.stepX = stepX; + ChartHelpers.timeFormatter = Plottable.Formatters.time('%a %b %e, %H:%M:%S'); + function wallX() { + var scale = new Plottable.Scales.Time(); + return { + scale: scale, + axis: new Plottable.Axes.Time(scale, 'bottom'), + accessor: function (d) { return d.wall_time; }, + }; + } + ChartHelpers.wallX = wallX; + ChartHelpers.relativeAccessor = function (d, index, dataset) { + // We may be rendering the final-point datum for scatterplot. + // If so, we will have already provided the 'relative' property + if (d.relative != null) { + return d.relative; + } + var data = 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].wall_time : 0; + return (+d.wall_time - first) / (60 * 60 * 1000); // ms to hours + }; + ChartHelpers.relativeFormatter = function (n) { + // we will always show 2 units of precision, e.g days and hours, or + // minutes and seconds, but not hours and minutes and seconds + var ret = ''; + var days = Math.floor(n / 24); + n -= (days * 24); + if (days) { + ret += days + 'd '; + } + var hours = Math.floor(n); + n -= hours; + n *= 60; + if (hours || days) { + ret += hours + 'h '; + } + var minutes = Math.floor(n); + n -= minutes; + n *= 60; + if (minutes || hours || days) { + ret += minutes + 'm '; + } + var seconds = Math.floor(n); + return ret + seconds + 's'; + }; + function relativeX() { + var scale = new Plottable.Scales.Linear(); + return { + scale: scale, + axis: new Plottable.Axes.Numeric(scale, 'bottom'), + accessor: ChartHelpers.relativeAccessor, + }; + } + ChartHelpers.relativeX = relativeX; + // a very literal definition of NaN: true for NaN for a non-number type + // or null, etc. False for Infinity or -Infinity + ChartHelpers.isNaN = function (x) { return +x !== x; }; + function getXComponents(xType) { + switch (xType) { + case 'step': + return stepX(); + case 'wall_time': + return wallX(); + case 'relative': + return relativeX(); + default: + throw new Error('invalid xType: ' + xType); + } + } + ChartHelpers.getXComponents = getXComponents; + })(ChartHelpers = TF.ChartHelpers || (TF.ChartHelpers = {})); +})(TF || (TF = {})); +</script> + <script> + Polymer({ + is: "tf-obsolete-histogram-chart", + properties: { + _chart: Object, + colorScale: Object, + tag: String, + selectedRuns: Array, + xType: String, + dataProvider: Function, + _initialized: Boolean, + }, + observers: [ + "_makeChart(tag, dataProvider, xType, colorScale, _initialized)", + "_changeRuns(_chart, selectedRuns.*)" + ], + _changeRuns: function(chart) { + if (chart.tag !== this.tag) { + return; // hack around some weird polymer bug :( + } + this._chart.changeRuns(this.selectedRuns); + this.redraw(); + }, + redraw: function() { + this._chart.redraw(); + }, + reload: function() { + this._chart.reload(); + }, + _makeChart: function(tag, dataProvider, xType, colorScale, _initialized) { + if (!_initialized) { + return; + } + if (this._chart) this._chart.destroy(); + var chart = new TF.HistogramChart(tag, dataProvider, xType, colorScale); + var svg = d3.select(this.$.chartsvg); + this.async(function() { + chart.renderTo(svg); + this._chart = chart; + }, 350); + }, + attached: function() { + this._initialized = true; + }, + detached: function() { + this._initialized = false; + } + }); + </script> +</dom-module> <dom-module id="tf-histogram-dashboard" assetpath="../tf-histogram-dashboard/"> <template> @@ -3028,7 +3924,7 @@ var TF; <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-provider="[[dataProvider]]" color-scale="[[colorScale]]" on-keyup="toggleSelected" tabindex="2"></tf-chart> + <tf-obsolete-histogram-chart tag="[[tag]]" id="chart" selected-runs="[[_array(run)]]" x-type="[[xType]]" data-provider="[[dataProvider]]" color-scale="[[colorScale]]" on-keyup="toggleSelected" tabindex="2"></tf-obsolete-histogram-chart> <paper-icon-button class="expand-button" icon="fullscreen" on-tap="toggleSelected"></paper-icon-button> </div> </div> @@ -3048,7 +3944,7 @@ var TF; Polymer({ is: "tf-histogram-dashboard", behaviors: [ - TF.Dashboard.ReloadBehavior("tf-chart"), + TF.Dashboard.ReloadBehavior("tf-obsolete-histogram-chart"), TF.Backend.Behavior, ], properties: { @@ -10051,6 +10947,24 @@ var tf; return _.map(strings, function (str) { return str.substring(largestIndex); }); } util.removeCommonPrefix = removeCommonPrefix; + /** + * Given a queryString, aka ?foo=1&bar=2, return the object representation. + */ + function getQueryParams(queryString) { + if (queryString.charAt(0) === '?') { + queryString = queryString.slice(1); + } + var queryParams = _.chain(queryString.split('&')) + .map(function (item) { + if (item) { + return item.split('='); + } + }) + .compact() + .value(); + return _.object(queryParams); + } + util.getQueryParams = getQueryParams; })(util = graph.util || (graph.util = {})); })(graph = tf.graph || (tf.graph = {})); })(tf || (tf = {})); @@ -13018,24 +13932,28 @@ span.counter { </paper-menu> </paper-dropdown-menu> </div> - <div class="control-holder"> - <div class="title">Session runs <span class="counter">([[_numSessionRuns(metadataTags)]])</span></div> - <paper-dropdown-menu no-label-float="" no-animations="" noink="" class="run-dropdown"> - <paper-menu id="select" class="dropdown-content" selected="{{selectedMetadataTag}}"> - <template is="dom-repeat" items="[[metadataTags]]"> - <paper-item>[[item.tag]]</paper-item> - </template> - <paper-item>None</paper-item> - </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"> + <template is="dom-if" if="[[showSessionRunsDropdown]]"> + <div class="control-holder"> + <div class="title">Session runs <span class="counter">([[_numSessionRuns(metadataTags)]])</span></div> + <paper-dropdown-menu no-label-float="" no-animations="" noink="" class="run-dropdown"> + <paper-menu id="select" class="dropdown-content" selected="{{selectedMetadataTag}}"> + <template is="dom-repeat" items="[[metadataTags]]"> + <paper-item>[[item.tag]]</paper-item> + </template> + <paper-item>None</paper-item> + </paper-menu> + </paper-dropdown-menu> </div> - </div> + </template> + <template is="dom-if" if="[[showUploadButton]]"> + <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> + </template> <div class="control-holder"> <div class="title"> Trace inputs @@ -13299,6 +14217,14 @@ Polymer({ _currentGradientParams: { type: Object, computed: '_getCurrentGradientParams(colorByParams, colorBy)' + }, + showSessionRunsDropdown: { + type: Boolean, + value: true + }, + showUploadButton: { + type: Boolean, + value: true } }, listeners: { @@ -13432,7 +14358,7 @@ Polymer({ } }, _getFile: function() { - this.$.file.click(); + this.$$("#file").click(); }, _setDownloadFilename: function(graphPath) { // Strip off everything before the last "/" and strip off the file @@ -13519,219 +14445,7 @@ Polymer({ }); })(); </script> -<dom-module id="tf-storage" assetpath="../tf-storage/"> - <script>/* Copyright 2015 Google Inc. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -==============================================================================*/ -/* tslint:disable:no-namespace */ -/** - * The Storage Module provides storage for URL parameters, and an API for - * getting and setting TensorBoard's stateful URI. - * - * It generates URI components like: events&runPrefix=train* - * which TensorBoard uses after like localhost:8000/#events&runPrefix=train* - * to store state in the URI. - */ -var TF; -(function (TF) { - var URIStorage; - (function (URIStorage) { - /** - * A key that users cannot use, since TensorBoard uses this to store info - * about the active tab. - */ - URIStorage.TAB = '__tab__'; - /** - * Return a string stored in the URI, given a corresonding key. - * Null if not found. - */ - function getString(key) { - var items = _componentToDict(_readComponent()); - return _.isUndefined(items[key]) ? null : items[key]; - } - URIStorage.getString = getString; - /** - * Store a string in the URI, with a corresponding key. - */ - function setString(key, value) { - var items = _componentToDict(_readComponent()); - items[key] = value; - _writeComponent(_dictToComponent(items)); - } - URIStorage.setString = setString; - /** - * Return a number stored in the URI, given a corresponding key. - */ - function getNumber(key) { - var items = _componentToDict(_readComponent()); - return _.isUndefined(items[key]) ? null : +items[key]; - } - URIStorage.getNumber = getNumber; - /** - * Store a number in the URI, with a corresponding key. - */ - function setNumber(key, value) { - var items = _componentToDict(_readComponent()); - items[key] = '' + value; - _writeComponent(_dictToComponent(items)); - } - URIStorage.setNumber = setNumber; - /** - * Return an object stored in the URI, given a corresponding key. - */ - function getObject(key) { - var items = _componentToDict(_readComponent()); - return _.isUndefined(items[key]) ? null : JSON.parse(atob(items[key])); - } - URIStorage.getObject = getObject; - /** - * Store an object in the URI, with a corresponding key. - */ - function setObject(key, value) { - var items = _componentToDict(_readComponent()); - items[key] = btoa(JSON.stringify(value)); - _writeComponent(_dictToComponent(items)); - } - URIStorage.setObject = setObject; - /** - * Read component from URI (e.g. returns "events&runPrefix=train*"). - */ - function _readComponent() { - return TF.Globals.USE_HASH ? window.location.hash.slice(1) : - TF.Globals.FAKE_HASH; - } - /** - * Write component to URI. - */ - function _writeComponent(component) { - if (TF.Globals.USE_HASH) { - window.location.hash = component; - } - else { - TF.Globals.FAKE_HASH = component; - } - } - /** - * Convert dictionary of strings into a URI Component. - * All key value entries get added as key value pairs in the component, - * with the exception of a key with the TAB value, which if present - * gets prepended to the URI Component string for backwards comptability - * reasons. - */ - function _dictToComponent(items) { - var component = ''; - // Add the tab name e.g. 'events', 'images', 'histograms' as a prefix - // for backwards compatbility. - if (items[URIStorage.TAB] !== undefined) { - component += items[URIStorage.TAB]; - } - // Join other strings with &key=value notation - var nonTab = _.pairs(items) - .filter(function (pair) { return pair[0] !== URIStorage.TAB; }) - .map(function (pair) { - return encodeURIComponent(pair[0]) + '=' + - encodeURIComponent(pair[1]); - }) - .join('&'); - return nonTab.length > 0 ? (component + '&' + nonTab) : component; - } - /** - * Convert a URI Component into a dictionary of strings. - * Component should consist of key-value pairs joined by a delimiter - * with the exception of the tabName. - * Returns dict consisting of all key-value pairs and - * dict[TAB] = tabName - */ - function _componentToDict(component) { - var items = {}; - var tokens = component.split('&'); - tokens.forEach(function (token) { - var kv = token.split('='); - // Special backwards compatibility for URI components like #events - if (kv.length === 1 && _.contains(TF.Globals.TABS, kv[0])) { - items[URIStorage.TAB] = kv[0]; - } - else if (kv.length === 2) { - items[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]); - } - }); - return items; - } - })(URIStorage = TF.URIStorage || (TF.URIStorage = {})); -})(TF || (TF = {})); -</script> -</dom-module> -<script>/* Copyright 2015 The TensorFlow Authors. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -==============================================================================*/ -var TF; -(function (TF) { - var TensorBoard; - (function (TensorBoard) { - TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY = 'TF.TensorBoard.autoReloadEnabled'; - var getAutoReloadFromLocalStorage = function () { - var val = window.localStorage.getItem(TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY); - return val === 'true' || val == null; // defaults to true - }; - TensorBoard.AutoReloadBehavior = { - properties: { - autoReloadEnabled: { - type: Boolean, - observer: '_autoReloadObserver', - value: getAutoReloadFromLocalStorage, - }, - _autoReloadId: { - type: Number, - }, - autoReloadIntervalSecs: { - type: Number, - value: 120, - }, - }, - detached: function () { window.clearTimeout(this._autoReloadId); }, - _autoReloadObserver: function (autoReload) { - window.localStorage.setItem(TensorBoard.AUTORELOAD_LOCALSTORAGE_KEY, autoReload); - if (autoReload) { - var _this = this; - this._autoReloadId = window.setTimeout(this._doAutoReload.bind(this), this.autoReloadIntervalSecs * 1000); - } - else { - window.clearTimeout(this._autoReloadId); - } - }, - _doAutoReload: function () { - if (this.reload == null) { - throw new Error('AutoReloadBehavior requires a reload method'); - } - this.reload(); - this._autoReloadId = window.setTimeout(this._doAutoReload.bind(this), this.autoReloadIntervalSecs * 1000); - } - }; - })(TensorBoard = TF.TensorBoard || (TF.TensorBoard = {})); -})(TF || (TF = {})); -</script></div><dom-module id="tf-tensorboard"> +</div><dom-module id="tf-tensorboard"> <template> <paper-dialog with-backdrop="" id="settings"> <h2>Settings</h2> |