diff --git a/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.html b/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.html index 455716992a..b1b71b151f 100644 --- a/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.html +++ b/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.html @@ -37,7 +37,7 @@
-
+
+
+ Nearest points in the original space:
{{metadataColumn}} labels (click to apply):Load data from your computer
@@ -383,6 +453,14 @@ Step 3: Host projector config
diff --git a/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.ts b/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.ts
index 814066eaeb..63bfc71201 100644
--- a/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.ts
+++ b/tensorboard/plugins/projector/vz_projector/vz-projector-data-panel.ts
@@ -27,7 +27,15 @@ export let DataPanelPolymer = PolymerElement({
selectedLabelOption:
{type: String, notify: true, observer: '_selectedLabelOptionChanged'},
normalizeData: Boolean,
- showForceCategoricalColorsCheckbox: Boolean
+ showForceCategoricalColorsCheckbox: Boolean,
+ metadataEditorInput: {type: String},
+ metadataEditorInputLabel: {type: String, value: 'Tag selection as'},
+ metadataEditorInputChange: {type: Object},
+ metadataEditorColumn: {type: String},
+ metadataEditorColumnChange: {type: Object},
+ metadataEditorButtonClicked: {type: Object},
+ metadataEditorButtonDisabled: {type: Boolean},
+ downloadMetadataClicked: {type: Boolean}
},
observers: [
'_generateUiForNewCheckpointForRun(selectedRun)',
@@ -44,6 +52,12 @@ export class DataPanel extends DataPanelPolymer {
private colorOptions: ColorOption[];
forceCategoricalColoring: boolean = false;
+ private metadataEditorInput: string;
+ private metadataEditorInputLabel: string;
+ private metadataEditorButtonDisabled: boolean;
+
+ private selectedPointIndices: number[];
+ private neighborsOfFirstPoint: knn.NearestEntry[];
private selectedTensor: string;
private selectedRun: string;
private dataProvider: DataProvider;
@@ -115,12 +129,39 @@ export class DataPanel extends DataPanelPolymer {
}
metadataChanged(
- spriteAndMetadata: SpriteAndMetadataInfo, metadataFile: string) {
+ spriteAndMetadata: SpriteAndMetadataInfo, metadataFile?: string) {
this.spriteAndMetadata = spriteAndMetadata;
- this.metadataFile = metadataFile;
+ if (metadataFile != null) {
+ this.metadataFile = metadataFile;
+ }
this.updateMetadataUI(this.spriteAndMetadata.stats, this.metadataFile);
- this.selectedColorOptionName = this.colorOptions[0].name;
+ if (this.selectedColorOptionName == null || this.colorOptions.filter(c =>
+ c.name === this.selectedColorOptionName).length === 0) {
+ this.selectedColorOptionName = this.colorOptions[0].name;
+ }
+
+ let labelIndex = -1;
+ this.metadataFields = spriteAndMetadata.stats.map((stats, i) => {
+ if (!stats.isNumeric && labelIndex === -1) {
+ labelIndex = i;
+ }
+ return stats.name;
+ });
+
+ if (this.metadataEditorColumn == null || this.metadataFields.filter(name =>
+ name === this.metadataEditorColumn).length === 0) {
+ // Make the default label the first non-numeric column.
+ this.metadataEditorColumn = this.metadataFields[Math.max(0, labelIndex)];
+ }
+ }
+
+ onProjectorSelectionChanged(
+ selectedPointIndices: number[],
+ neighborsOfFirstPoint: knn.NearestEntry[]) {
+ this.selectedPointIndices = selectedPointIndices;
+ this.neighborsOfFirstPoint = neighborsOfFirstPoint;
+ this.metadataEditorInputChange();
}
private addWordBreaks(longString: string): string {
@@ -145,7 +186,16 @@ export class DataPanel extends DataPanelPolymer {
}
return stats.name;
});
- this.selectedLabelOption = this.labelOptions[Math.max(0, labelIndex)];
+
+ if (this.selectedLabelOption == null || this.labelOptions.filter(name =>
+ name === this.selectedLabelOption).length === 0) {
+ this.selectedLabelOption = this.labelOptions[Math.max(0, labelIndex)];
+ }
+
+ if (this.metadataEditorColumn == null || this.labelOptions.filter(name =>
+ name === this.metadataEditorColumn).length === 0) {
+ this.metadataEditorColumn = this.labelOptions[Math.max(0, labelIndex)];
+ }
// Color by options.
const standardColorOption: ColorOption[] = [
@@ -207,6 +257,101 @@ export class DataPanel extends DataPanelPolymer {
this.colorOptions = standardColorOption.concat(metadataColorOption);
}
+ private metadataEditorContext(enabled: boolean) {
+ this.metadataEditorButtonDisabled = !enabled;
+ if (this.projector) {
+ this.projector.metadataEditorContext(enabled, this.metadataEditorColumn);
+ }
+ }
+
+ private metadataEditorInputChange() {
+ let col = this.metadataEditorColumn;
+ let value = this.metadataEditorInput;
+ let selectionSize = this.selectedPointIndices.length +
+ this.neighborsOfFirstPoint.length;
+ if (selectionSize > 0) {
+ if (value != null && value.trim() !== '') {
+ if (this.spriteAndMetadata.stats.filter(s => s.name===col)[0].isNumeric
+ && isNaN(+value)) {
+ this.metadataEditorInputLabel = `Label must be numeric`;
+ this.metadataEditorContext(false);
+ }
+ else {
+ let numMatches = this.projector.dataSet.points.filter(p =>
+ p.metadata[col].toString() === value.trim()).length;
+
+ if (numMatches === 0) {
+ this.metadataEditorInputLabel =
+ `Tag ${selectionSize} with new label`;
+ }
+ else {
+ this.metadataEditorInputLabel = `Tag ${selectionSize} points as`;
+ }
+ this.metadataEditorContext(true);
+ }
+ }
+ else {
+ this.metadataEditorInputLabel = 'Tag selection as';
+ this.metadataEditorContext(false);
+ }
+ }
+ else {
+ this.metadataEditorContext(false);
+
+ if (value != null && value.trim() !== '') {
+ this.metadataEditorInputLabel = 'Select points to tag';
+ }
+ else {
+ this.metadataEditorInputLabel = 'Tag selection as';
+ }
+ }
+ }
+
+ private metadataEditorInputKeydown(e) {
+ // Check if 'Enter' was pressed
+ if (e.keyCode === 13) {
+ this.metadataEditorButtonClicked();
+ }
+ e.stopPropagation();
+ }
+
+ private metadataEditorColumnChange() {
+ this.metadataEditorInputChange();
+ }
+
+ private metadataEditorButtonClicked() {
+ if (!this.metadataEditorButtonDisabled) {
+ let value = this.metadataEditorInput.trim();
+ let selectionSize = this.selectedPointIndices.length +
+ this.neighborsOfFirstPoint.length;
+ this.projector.metadataEdit(this.metadataEditorColumn, value);
+ this.projector.metadataEditorContext(true, this.metadataEditorColumn);
+ this.metadataEditorInputLabel = `${selectionSize} labeled as '${value}'`;
+ }
+ }
+
+ private downloadMetadataClicked() {
+ if (this.projector && this.projector.dataSet
+ && this.projector.dataSet.spriteAndMetadataInfo) {
+ let tsvFile = this.projector.dataSet.spriteAndMetadataInfo.stats.map(s =>
+ s.name).join('\t');
+
+ this.projector.dataSet.spriteAndMetadataInfo.pointsInfo.forEach(p => {
+ let vals = [];
+
+ for (const column in p) {
+ vals.push(p[column]);
+ }
+ tsvFile += '\n' + vals.join('\t');
+ });
+
+ const textBlob = new Blob([tsvFile], {type: 'text/plain'});
+ this.$.downloadMetadataLink.download = 'metadata-edited.tsv';
+ this.$.downloadMetadataLink.href = window.URL.createObjectURL(textBlob);
+ this.$.downloadMetadataLink.click();
+ }
+ }
+
setNormalizeData(normalizeData: boolean) {
this.normalizeData = normalizeData;
}
@@ -403,7 +548,7 @@ export class DataPanel extends DataPanelPolymer {
}
(this.$$('#demo-data-buttons-container') as HTMLElement).style.display =
- 'block';
+ 'flex';
// Fill out the projector config.
const projectorConfigTemplate =
@@ -492,6 +637,10 @@ export class DataPanel extends DataPanelPolymer {
this.runNames.length + ' runs';
}
+ _hasChoice(choices: any[]): boolean {
+ return choices.length > 0;
+ }
+
_hasChoices(choices: any[]): boolean {
return choices.length > 1;
}
diff --git a/tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.html b/tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.html
index a705fec1e5..89df3ca60d 100644
--- a/tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.html
+++ b/tensorboard/plugins/projector/vz_projector/vz-projector-inspector-panel.html
@@ -59,25 +59,25 @@
margin-right: 0;
}
-.nn {
+.nn, .metadata-info {
display: flex;
flex-direction: column;
}
-.nn > * {
+.nn > *, .metadata-info > * {
padding: 0 20px;
}
-.nn-list {
+.nn-list, .metadata-list {
overflow-y: auto;
}
-.nn-list .neighbor {
+.nn-list .neighbor, .metadata-list .metadata {
font-size: 12px;
margin-bottom: 8px;
}
-.nn-list .label-and-value {
+.nn-list .label-and-value, .metadata-list .label-and-value {
display: flex;
justify-content: space-between;
}
@@ -88,33 +88,33 @@
white-space: nowrap;
}
-.nn-list .value {
+.nn-list .value, .metadata-list .value {
color: #666;
float: right;
font-weight: 300;
margin-left: 8px;
}
-.nn-list .bar {
+.nn-list .bar, .metadata-list .bar {
position: relative;
border-top: 1px solid rgba(0, 0, 0, 0.15);
margin: 2px 0;
}
-.nn-list .bar .fill {
+.nn-list .bar .fill, .metadata-list .bar .fill {
position: absolute;
top: -1px;
border-top: 1px solid white;
}
-.nn-list .tick {
+.nn-list .tick, .metadata-list .tick {
position: absolute;
top: 0px;
height: 3px;
border-left: 1px solid rgba(0, 0, 0, 0.15);
}
-.nn-list .neighbor-link:hover {
+.nn-list .neighbor-link:hover, .metadata-list .metadata-link:hover {
cursor: pointer;
}
@@ -164,14 +164,12 @@
display: inline-block;
}
-#nn-slider {
- margin: 0 -12px 0 0px;
+.nn-slider {
--paper-slider-input: {
- width: 66px
+ width: 64px;
};
- --paper-input-container-input-webkit-spinner: {
- -webkit-appearance: none;
- margin: 0;
+ --paper-input-container-input: {
+ font-size: 14px;
};
}
@@ -227,7 +225,8 @@