Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
}
</style>
</template>
<script src="../vz-line-chart2/line-chart-exporter.js"></script>
<script>
(function() {

Expand Down Expand Up @@ -200,6 +201,11 @@
cancelAnimationFrame(this._redrawRaf);
},

exportAsSvgString() {
const exporter = this.$.chart.getExporter();
return exporter.exportAsString();
},

resetDomain() {
const chart = this.$.chart;
chart.resetDomain();
Expand Down
1 change: 1 addition & 0 deletions tensorboard/components/vz_line_chart2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ licenses(["notice"]) # Apache 2.0
tf_web_library(
name = "vz_line_chart2",
srcs = [
"line-chart-exporter.ts",
"line-chart.ts",
"panZoomDragLayer.html",
"panZoomDragLayer.ts",
Expand Down
147 changes: 147 additions & 0 deletions tensorboard/components/vz_line_chart2/line-chart-exporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/* 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_line_chart2 {

enum NodeName {
GROUP = 'G',
DIV = 'DIV',
SVG = 'SVG',
TEXT = 'TEXT',
}

export class PlottableExporter {
private root: Element;
private uniqueId: number = 0;

constructor(rootEl: Element) {
this.root = rootEl;
}

public exportAsString(): string {
const convertedNodes = this.convert(this.root);
if (!convertedNodes) return '';
const svg = this.createRootSvg();
svg.appendChild(convertedNodes);
return svg.outerHTML;
}

private createUniqueId(prefix: string): string {
return `${prefix}_${this.uniqueId++}`;
}

private getSize(): DOMRect | ClientRect {
return this.root.getBoundingClientRect();
}

private createRootSvg(): Element {
const svg = document.createElement('svg');
const rect = this.getSize();

// case on `viewBox` is sensitive.
svg.setAttributeNS('svg', 'viewBox', `0 0 ${rect.width} ${rect.height}`);
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
return svg;
}

private convert(node: Node): Node | null {
let newNode = null;
const nodeName = node.nodeName.toUpperCase();
if (node.nodeType == Node.ELEMENT_NODE &&
(nodeName == NodeName.DIV || nodeName == NodeName.SVG)) {
newNode = document.createElement(NodeName.GROUP);
const style = window.getComputedStyle(node as Element);
const left = parseInt(style.left, 10);
const top = parseInt(style.top, 10);
if (left || top) {
const clipId = this.createUniqueId('clip');
newNode.setAttribute('transform', `translate(${left}, ${top})`);
newNode.setAttribute('clip-path', `url(#${clipId})`);
const width = parseInt(style.width, 10);
const height = parseInt(style.height, 10);
const rect = document.createElement('rect');
rect.setAttribute('width', String(width));
rect.setAttribute('height', String(height));
const clipPath = document.createElementNS('svg', 'clipPath');
clipPath.id = clipId;
clipPath.appendChild(rect);
newNode.appendChild(clipPath);
}
} else {
newNode = node.cloneNode();
}
Array.from(node.childNodes)
.map(node => this.convert(node))
.filter(Boolean)
.forEach(el => newNode.appendChild(el));

// Remove empty grouping. They add too much noise.
const shouldOmit = (
newNode.nodeName.toUpperCase() == NodeName.GROUP &&
!newNode.hasChildNodes()
) || this.shouldOmitNode(node);

if (shouldOmit) return null;
return this.stripClass(this.transferStyle(node, newNode));
}

private stripClass(node: Node): Node {
if (node.nodeType == Node.ELEMENT_NODE) {
(node as Element).removeAttribute('class');
}
return node;
}

private transferStyle(origNode: Node, node: Node): Node {
if (node.nodeType != Node.ELEMENT_NODE) return node;
const el = node as HTMLElement;
const nodeName = node.nodeName.toUpperCase();
const style = window.getComputedStyle(origNode as HTMLElement);

if (nodeName == NodeName.TEXT) {
Object.assign(el.style, {
fontFamily: style.fontFamily,
fontSize: style.fontSize,
fontWeight: style.fontWeight,
});
}

if (nodeName != NodeName.GROUP) {
el.setAttribute('fill', style.fill);
el.setAttribute('stroke', style.stroke);
el.setAttribute('stroke-width', style.strokeWidth);
}

if (style.opacity != '1') el.setAttribute('opacity', style.opacity);

return node;
}

protected shouldOmitNode(node: Node): boolean {
return false;
}
}

export class LineChartExporter extends PlottableExporter {
shouldOmitNode(node: Node): boolean {
// Scatter plot is useful for tooltip. Tooltip is meaningless in the
// exported svg.
if (node.nodeType == Node.ELEMENT_NODE) {
return (node as Element).classList.contains('scatter-plot');
}
return false;
}
}

}
4 changes: 4 additions & 0 deletions tensorboard/components/vz_line_chart2/vz-line-chart2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ Polymer({
this._chart.setTooltipSortingMethod(this.tooltipSortingMethod);
},

getExporter() {
return new LineChartExporter(this.$.chartdiv);
},

});

} // namespace vz_line_chart2
2 changes: 2 additions & 0 deletions tensorboard/plugins/scalar/tf_scalar_dashboard/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ tf_web_library(
"@org_polymer_paper_icon_button",
"@org_polymer_paper_input",
"@org_polymer_paper_item",
"@org_polymer_paper_listbox",
"@org_polymer_paper_menu_button",
"@org_polymer_paper_menu",
"@org_polymer_paper_slider",
"@org_polymer_paper_styles",
Expand Down
35 changes: 33 additions & 2 deletions tensorboard/plugins/scalar/tf_scalar_dashboard/tf-scalar-card.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
limitations under the License.
-->

<link rel="import" href="../paper-item/paper-item.html">
<link rel="import" href="../paper-dropdown-menu/paper-dropdown-menu.html">
<link rel="import" href="../paper-icon-button/paper-icon-button.html">
<link rel="import" href="../paper-item/paper-item.html">
<link rel="import" href="../paper-listbox/paper-listbox.html">
<link rel="import" href="../paper-menu-button/paper-menu-button.html">
<link rel="import" href="../paper-menu/paper-menu.html">
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-backend/tf-backend.html">
Expand Down Expand Up @@ -75,6 +77,22 @@
on-tap="_resetDomain"
title="Fit domain to data"
></paper-icon-button>
<template is="dom-if" if="[[showDownloadLinks]]">
<paper-menu-button on-paper-dropdown-open="_updateDownloadLink">
<paper-icon-button
class="dropdown-trigger"
slot="dropdown-trigger"
icon="file-download"
></paper-icon-button>
<paper-listbox class="dropdown-content" slot="dropdown-content">
<paper-item>
<a id="svgLink" download="[[tag]].svg">
Download Current Graph as SVG
</a>
</paper-item>
</paper-listbox>
</paper-menu-button>
</template>
<span style="flex-grow: 1"></span>
<template is="dom-if" if="[[showDownloadLinks]]">
<div class="download-links">
Expand Down Expand Up @@ -148,8 +166,8 @@
}

.download-links a {
font-size: 10px;
align-self: center;
font-size: 10px;
margin: 2px;
}

Expand All @@ -162,6 +180,15 @@
font-size: 10px;
}
}

paper-menu-button {
padding: 0;
}
paper-item a {
color: inherit;
text-decoration: none;
white-space: nowrap;
}
</style>
</template>
<script>
Expand Down Expand Up @@ -274,6 +301,10 @@
chart.resetDomain();
}
},
_updateDownloadLink() {
const svgStr = this.$$('tf-line-chart-data-loader').exportAsSvgString();
this.$$('#svgLink').href = `data:image/svg+xml,${svgStr}`;
},
_csvUrl(tag, run) {
return tf_backend.addParams(
this.getDataLoadUrl({tag, run}),
Expand Down