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 @@
DATA
@@ -262,28 +307,53 @@ - - Sphereize data - - - The data is normalized by shifting each point by the centroid and making - it unit norm. - - -

- + +

+ Load data from your computer - Load data + Load - + Publish your embedding visualization and data Publish -

+ + + Download the metadata with applied modifications + + Download + + + + + Label selected metadata + + Label + +

Load data from your computer

@@ -383,6 +453,14 @@

Step 3: Host projector config

Click outside to dismiss.
+ + Sphereize data + + + The data is normalized by shifting each point by the centroid and making + it unit norm. + +
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 @@ The number of neighbors (in the original space) to show when clicking on a point. - +
@@ -241,6 +240,21 @@

Nearest points in the original space:

+