@@ -137,12 +138,15 @@
xComponentsCreationMethod: Object,
xType: String,
yValueAccessor: Object,
- tooltipColumns: Array,
fillArea: Object,
smoothingEnabled: Boolean,
smoothingWeight: Number,
+
+ tooltipColumns: Array,
tooltipSortingMethod: String,
+ tooltipPosition: String,
+
ignoreYOutliers: Boolean,
defaultXRange: Array,
diff --git a/tensorboard/components/vz_chart_helpers/BUILD b/tensorboard/components/vz_chart_helpers/BUILD
index d489ceaf682..5d2565bbf16 100644
--- a/tensorboard/components/vz_chart_helpers/BUILD
+++ b/tensorboard/components/vz_chart_helpers/BUILD
@@ -9,12 +9,15 @@ tf_web_library(
srcs = [
"vz-chart-helpers.html",
"vz-chart-helpers.ts",
+ "vz-chart-tooltip.html",
+ "vz-chart-tooltip.ts",
],
path = "/vz-chart-helpers",
deps = [
"//tensorboard/components/tf_imports:d3",
"//tensorboard/components/tf_imports:lodash",
"//tensorboard/components/tf_imports:plottable",
+ "//tensorboard/components/tf_imports:polymer",
"//tensorboard/components/vz_sorting",
],
)
diff --git a/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html
new file mode 100644
index 00000000000..a94db3bf8a9
--- /dev/null
+++ b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.html
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.ts b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.ts
new file mode 100644
index 00000000000..6697512379c
--- /dev/null
+++ b/tensorboard/components/vz_chart_helpers/vz-chart-tooltip.ts
@@ -0,0 +1,173 @@
+/* Copyright 2018 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.
+==============================================================================*/
+namespace vz_chart_helper {
+
+export enum TooltipPosition {
+ /**
+ * Positions the tooltip to the bottom of the chart in most case. Positions
+ * the tooltip above the chart if there isn't sufficient space below.
+ */
+ AUTO = 'auto',
+ /**
+ * Position the tooltip on the bottom of the chart.
+ */
+ BOTTOM = 'bottom',
+ /**
+ * Positions the tooltip to the right of the chart.
+ */
+ RIGHT = 'right',
+}
+
+export interface VzChartTooltip extends Element {
+ content(): Element;
+ hide(): void;
+ updateAndPosition(anchorNode: Element, newDom: Array
): void;
+}
+
+Polymer({
+ is: 'vz-chart-tooltip',
+ properties: {
+ /**
+ * Possible values are TooltipPosition.BOTTOM and TooltipPosition.RIGHT.
+ */
+ position: {
+ type: String,
+ value: TooltipPosition.AUTO,
+ },
+
+ /**
+ * Minimum instance from the edge of the screen.
+ */
+ minDistFromEdge: {
+ type: Number,
+ value: 15,
+ },
+ },
+
+ ready() {
+ this._styleCache = null;
+ this._raf = null;
+ this._tunnel = null;
+ },
+
+ attached() {
+ this._tunnel = this._createTunnel();
+ },
+
+ detached() {
+ this.hide();
+ this._removeTunnel(this._tunnel);
+ this._tunnel = null;
+ },
+
+ hide() {
+ window.cancelAnimationFrame(this._raf);
+ this._styleCache = null;
+ this.content().style.opacity = 0;
+ },
+
+ content(): Element {
+ return this._tunnel.firstElementChild;
+ },
+
+ /**
+ * CSS Scopes the newly added DOM (in most tooltip where columns are
+ * invariable, only newly added rows are necessary to be scoped) and positions
+ * the tooltip with respect to the anchorNode.
+ */
+ updateAndPosition(anchorNode: Element, newDom: Element[]) {
+ newDom.forEach(row => this.scopeSubtree(row));
+
+ window.cancelAnimationFrame(this._raf);
+ this._raf = window.requestAnimationFrame(() => {
+ if (!this.isAttached) return;
+ this._repositionImpl(anchorNode);
+ });
+ },
+
+ _repositionImpl(anchorNode: Element) {
+ const tooltipContent = this.content();
+
+ const nodeRect = anchorNode.getBoundingClientRect();
+ const tooltipRect = tooltipContent.getBoundingClientRect();
+ const viewportHeight = window.innerHeight;
+ const documentWidth = document.body.clientWidth;
+
+ const anchorTop = nodeRect.top;
+ const anchorBottom = anchorTop + nodeRect.height;
+ const effectiveTooltipHeight = tooltipRect.height +
+ vz_chart_helpers.TOOLTIP_Y_PIXEL_OFFSET;
+
+ let bottom = null;
+ let left = Math.max(this.minDistFromEdge, nodeRect.left);
+ let right = null;
+ let top = anchorTop;
+
+ if (this.position == TooltipPosition.RIGHT) {
+ left = nodeRect.right;
+ } else {
+ top = anchorBottom + vz_chart_helpers.TOOLTIP_Y_PIXEL_OFFSET;
+
+ // prevent it from falling off the right side of the screen.
+ if (documentWidth < left + tooltipRect.width + this.minDistFromEdge) {
+ left = null;
+ right = this.minDistFromEdge;
+ }
+ }
+
+ // If there is not enough space to render tooltip below the anchorNode in
+ // the viewport and there is enough space above, place it above the
+ // anchorNode.
+ if (this.position == TooltipPosition.AUTO &&
+ nodeRect.top - effectiveTooltipHeight > 0 &&
+ viewportHeight < nodeRect.top + nodeRect.height +
+ effectiveTooltipHeight) {
+ top = null;
+ bottom = viewportHeight - anchorTop +
+ vz_chart_helpers.TOOLTIP_Y_PIXEL_OFFSET;
+ }
+
+ const newStyle = {
+ opacity: 1,
+ left: left ? `${left}px` : null,
+ right: right ? `${right}px` : null,
+ top: top ? `${top}px` : null,
+ bottom: bottom ? `${bottom}px` : null,
+ };
+
+ // Do not update the style (which can cause re-layout) if it has not
+ // changed.
+ if (!_.isEqual(this._styleCache, newStyle)) {
+ Object.assign(tooltipContent.style, newStyle);
+ this._styleCache = newStyle;
+ }
+ },
+
+ _createTunnel(): Element {
+ const div = document.createElement('div');
+ div.classList.add(`${this.is}-tunnel`);
+ const template = this.instanceTemplate(this.$.template);
+ this.scopeSubtree(template);
+ div.appendChild(template);
+ document.body.appendChild(div);
+ return div;
+ },
+
+ _removeTunnel(tunnel: Element) {
+ document.body.removeChild(tunnel);
+ },
+});
+
+} // namespace vz_chart_helper
diff --git a/tensorboard/components/vz_line_chart2/line-chart.ts b/tensorboard/components/vz_line_chart2/line-chart.ts
index 1a8c7f589a0..f4f16c4d1df 100644
--- a/tensorboard/components/vz_line_chart2/line-chart.ts
+++ b/tensorboard/components/vz_line_chart2/line-chart.ts
@@ -70,7 +70,7 @@ export class LineChart {
private symbolFunction: vz_chart_helpers.SymbolFn;
private tooltipColumns: vz_chart_helpers.TooltipColumn[];
- private tooltip: d3.Selection;
+ private tooltip: vz_chart_helper.VzChartTooltip;
private tooltipInteraction: Plottable.Interactions.Pointer;
private tooltipPointsComponent: Plottable.Component;
@@ -91,7 +91,6 @@ export class LineChart {
private smoothingWeight: number;
private smoothingEnabled: boolean;
private tooltipSortingMethod: string;
- private tooltipPosition: string;
private _ignoreYOutliers: boolean;
private _lastMousePosition: Plottable.Point;
@@ -101,7 +100,6 @@ export class LineChart {
private _defaultYRange: number[];
private _tooltipUpdateAnimationFrame: number;
- private _tooltipPositionAnimationFrame: number;
private targetSVG: d3.Selection;
@@ -110,7 +108,7 @@ export class LineChart {
yValueAccessor: Plottable.IAccessor,
yScaleType: string,
colorScale: Plottable.Scales.Color,
- tooltip: d3.Selection,
+ tooltip: vz_chart_helper.VzChartTooltip,
tooltipColumns: vz_chart_helpers.TooltipColumn[],
fillArea: FillArea,
defaultXRange?: number[],
@@ -494,8 +492,7 @@ export class LineChart {
private hideTooltips(): void {
window.cancelAnimationFrame(this._tooltipUpdateAnimationFrame);
- window.cancelAnimationFrame(this._tooltipPositionAnimationFrame);
- this.tooltip.style('opacity', 0);
+ this.tooltip.hide();
this.scatterPlot.attr('display', 'block');
this.tooltipPointsComponent.content().selectAll('.point').remove();
}
@@ -516,10 +513,34 @@ export class LineChart {
target: vz_chart_helpers.Point,
tooltipColumns: vz_chart_helpers.TooltipColumn[]) {
if (!points.length) {
- this.tooltip.style('opacity', 0);
+ this.tooltip.hide();
return;
}
+ const {colorScale} = this;
+ const swatchCol = {
+ title: '',
+ static: false,
+ evalType: TooltipColumnEvalType.DOM,
+ evaluate(d: vz_chart_helpers.Point) {
+ d3.select(this)
+ .select('span')
+ .style(
+ 'background-color',
+ () => colorScale.scale(d.dataset.metadata().name));
+ return '';
+ },
+ enter(d: vz_chart_helpers.Point) {
+ d3.select(this)
+ .append('span')
+ .classed('swatch', true)
+ .style(
+ 'background-color',
+ () => colorScale.scale(d.dataset.metadata().name));
+ },
+ };
+ tooltipColumns = [swatchCol, ...tooltipColumns];
+
// Formatters for value, step, and wall_time
let valueFormatter = vz_chart_helpers.multiscaleFormatter(
vz_chart_helpers.Y_TOOLTIP_FORMATTER_PRECISION);
@@ -546,7 +567,21 @@ export class LineChart {
}
const self = this;
- const rows = this.tooltip.select('tbody')
+ const table = d3.select(this.tooltip.content()).select('table');
+ const header = table.select('thead')
+ .selectAll('th')
+ .data(
+ tooltipColumns,
+ (column: vz_chart_helpers.TooltipColumn, _, __) => {
+ return column.title
+ });
+ const newHeaderNodes = header.enter()
+ .append('th')
+ .text(col => col.title)
+ .nodes();
+ header.exit().remove();
+
+ const rows = table.select('tbody')
.selectAll('tr')
.data(points, (pt: vz_chart_helpers.Point, _, __) => {
return pt.dataset.metadata().name;
@@ -571,45 +606,22 @@ export class LineChart {
.order();
rows.exit().remove();
- rows.enter().append('tr').each(function(point) {
- self.drawTooltipRow(this, tooltipColumns, point);
- });
-
- // Because a tooltip content update is a DOM _mutation_, after an animation
- // frame, we update the position which is another read and mutation.
- window.cancelAnimationFrame(this._tooltipPositionAnimationFrame);
- this._tooltipPositionAnimationFrame = window.requestAnimationFrame(() => {
- this.repositionTooltip();
- });
+ const newRowNodes = rows.enter()
+ .append('tr')
+ .each(function(point) {
+ self.drawTooltipRow(this, tooltipColumns, point);
+ })
+ .nodes();
+ const newNodes = [...newHeaderNodes, ...newRowNodes] as Element[];
+ this.tooltip.updateAndPosition(this.targetSVG.node(), newNodes);
}
private drawTooltipRow(
row: d3.BaseType,
tooltipColumns: vz_chart_helpers.TooltipColumn[],
point: vz_chart_helpers.Point) {
- const {smoothingEnabled, colorScale} = this;
const self = this;
- const swatchCol = {
- name: 'Swatch',
- evalType: TooltipColumnEvalType.DOM,
- evaluate(d: vz_chart_helpers.Point) {
- d3.select(this)
- .select('span')
- .style(
- 'background-color',
- () => colorScale.scale(d.dataset.metadata().name));
- },
- enter(d: vz_chart_helpers.Point) {
- d3.select(this)
- .append('span')
- .classed('swatch', true)
- .style(
- 'background-color',
- () => colorScale.scale(d.dataset.metadata().name));
- },
- };
- const columns = d3.select(row).selectAll('td')
- .data([swatchCol, ...tooltipColumns]);
+ const columns = d3.select(row).selectAll('td').data(tooltipColumns);
columns.each(function(col: TooltipColumn) {
// Skip column value update when the column is static.
@@ -637,30 +649,6 @@ export class LineChart {
}
}
- /**
- * Repositions the tooltip based on new width and height of the bounding box.
- * In order to update the position, it _read_ the DOM, then _mutate_ the DOM.
- */
- private repositionTooltip() {
- // compute left position
- let documentWidth = document.body.clientWidth;
- let node: any = this.tooltip.node();
- let parentRect = node.parentElement.getBoundingClientRect();
- let nodeRect = node.getBoundingClientRect();
- // prevent it from falling off the right side of the screen
- let left = documentWidth - parentRect.left - nodeRect.width - 60, top = 0;
-
- if (this.tooltipPosition === 'right') {
- left = Math.min(parentRect.width, left);
- } else { // 'bottom'
- left = Math.min(0, left);
- top = parentRect.height + vz_chart_helpers.TOOLTIP_Y_PIXEL_OFFSET;
- }
-
- this.tooltip.style('transform', `translate(${left}px, ${top}px)`);
- this.tooltip.style('opacity', 1);
- }
-
private findClosestPoint(
target: vz_chart_helpers.Point,
dataset: Plottable.Dataset): vz_chart_helpers.Point | null {
@@ -841,10 +829,6 @@ export class LineChart {
this.tooltipSortingMethod = method;
}
- public setTooltipPosition(position: string) {
- this.tooltipPosition = position;
- }
-
public renderTo(targetSVG: d3.Selection) {
this.targetSVG = targetSVG;
this.outer.renderTo(targetSVG);
diff --git a/tensorboard/components/vz_line_chart2/vz-line-chart2.html b/tensorboard/components/vz_line_chart2/vz-line-chart2.html
index 53e0adc07e4..fb46a5e0abf 100644
--- a/tensorboard/components/vz_line_chart2/vz-line-chart2.html
+++ b/tensorboard/components/vz_line_chart2/vz-line-chart2.html
@@ -20,6 +20,7 @@
+
-
+
+