in tensorboard/plugins/image/tf_image_dashboard/tf-image-loader.ts [31:363]
class TfImageLoader extends LegacyElementMixin(PolymerElement) {
static readonly template = html`
<tf-card-heading
tag="[[tag]]"
run="[[run]]"
display-name="[[tagMetadata.displayName]]"
description="[[tagMetadata.description]]"
color="[[_runColor]]"
>
<template is="dom-if" if="[[_hasMultipleSamples]]">
<div>sample: [[_sampleText]] of [[ofSamples]]</div>
</template>
<template is="dom-if" if="[[_hasAtLeastOneStep]]">
<div class="heading-row">
<div class="heading-label">
step
<span style="font-weight: bold"
>[[_toLocaleString(_stepValue)]]</span
>
</div>
<div class="heading-label heading-right datetime">
<template is="dom-if" if="[[_currentWallTime]]">
[[_currentWallTime]]
</template>
</div>
<div class="label right">
<paper-spinner-lite active hidden$="[[!_isImageLoading]]">
</paper-spinner-lite>
</div>
</div>
</template>
<template is="dom-if" if="[[_hasMultipleSteps]]">
<div>
<paper-slider
id="steps"
immediate-value="{{_stepIndex}}"
max="[[_maxStepIndex]]"
max-markers="[[_maxStepIndex]]"
snaps
step="1"
value="{{_stepIndex}}"
></paper-slider>
</div>
</template>
</tf-card-heading>
<!-- Semantically a button but <img> inside a <button> disallows user to do
an interesting operation like "Copy Image" in non-Chromium browsers. -->
<a
id="main-image-container"
role="button"
aria-label="Toggle actual size"
aria-expanded$="[[_getAriaExpanded(actualSize)]]"
on-tap="_handleTap"
></a>
<style include="tf-card-heading-style">
/** Make button a div. */
button {
width: 100%;
display: block;
background: none;
border: 0;
padding: 0;
}
/** Firefox: Get rid of dotted line inside button. */
button::-moz-focus-inner {
border: 0;
padding: 0;
}
/** Firefox: Simulate Chrome's outer glow on button when focused. */
button:-moz-focusring {
outline: none;
box-shadow: 0px 0px 1px 2px Highlight;
}
:host {
display: block;
width: 350px;
height: auto;
position: relative;
margin: 0 15px 40px 0;
overflow-x: auto;
}
/** When actual size shown is on, use the actual image width. */
:host([actual-size]) {
max-width: 100%;
width: auto;
}
:host([actual-size]) #main-image-container {
max-height: none;
width: auto;
}
:host([actual-size]) #main-image-container img {
width: auto;
}
paper-spinner-lite {
width: 14px;
height: 14px;
vertical-align: text-bottom;
--paper-spinner-color: var(--tb-orange-strong);
}
#steps {
height: 15px;
margin: 0 0 0 -15px;
/*
* 31 comes from adding a padding of 15px from both sides of the
* paper-slider, subtracting 1px so that the slider width aligns
* with the image (the last slider marker takes up 1px), and
* adding 2px to account for a border of 1px on both sides of
* the image. 30 - 1 + 2.
*/
width: calc(100% + 31px);
--paper-slider-active-color: var(--tb-orange-strong);
--paper-slider-knob-color: var(--tb-orange-strong);
--paper-slider-knob-start-border-color: var(--tb-orange-strong);
--paper-slider-knob-start-color: var(--tb-orange-strong);
--paper-slider-markers-color: var(--tb-orange-strong);
--paper-slider-pin-color: var(--tb-orange-strong);
--paper-slider-pin-start-color: var(--tb-orange-strong);
}
#main-image-container {
max-height: 1024px;
overflow: auto;
}
#main-image-container img {
cursor: pointer;
display: block;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
width: 100%;
height: auto;
}
paper-icon-button {
color: #2196f3;
border-radius: 100%;
width: 32px;
height: 32px;
padding: 4px;
}
paper-icon-button[selected] {
background: var(--tb-ui-light-accent);
}
[hidden] {
display: none;
}
</style>
`;
@property({type: String})
run: string;
@property({type: String})
tag: string;
@property({type: Number})
sample: number;
@property({type: Number})
ofSamples: number;
@property({type: Object})
tagMetadata: object;
@property({
type: Boolean,
reflectToAttribute: true,
})
actualSize: boolean = false;
@property({
type: Number,
})
brightnessAdjustment: number = 0.5;
@property({
type: Number,
})
contrastPercentage: number = 0;
@property({type: Object})
requestManager: RequestManager;
@property({
type: Object,
})
_metadataCanceller = new Canceller();
@property({
type: Object,
})
_imageCanceller = new Canceller();
@property({
type: Array,
notify: true,
})
_steps: unknown[] = [];
@property({
type: Number,
notify: true,
})
_stepIndex: number;
@property({
type: Boolean,
})
_isImageLoading: boolean = false;
@computed('run')
get _runColor(): string {
var run = this.run;
return runsColorScale(run);
}
@computed('_steps')
get _hasAtLeastOneStep(): boolean {
var steps = this._steps;
return !!steps && steps.length > 0;
}
@computed('_steps')
get _hasMultipleSteps(): boolean {
var steps = this._steps;
return !!steps && steps.length > 1;
}
@computed('_steps', '_stepIndex')
get _currentStep(): object {
var steps = this._steps as any;
var stepIndex = this._stepIndex;
return steps[stepIndex] || null;
}
@computed('_currentStep')
get _stepValue(): number {
var currentStep = this._currentStep;
if (!currentStep) return 0;
return (currentStep as any).step;
}
@computed('_currentStep')
get _currentWallTime(): string {
var currentStep = this._currentStep;
if (!currentStep) return '';
return formatDate((currentStep as any).wall_time);
}
@computed('_steps')
get _maxStepIndex(): number {
var steps = this._steps;
return steps.length - 1;
}
@computed('sample')
get _sampleText(): string {
var sample = this.sample;
return `${sample + 1}`;
}
@computed('ofSamples')
get _hasMultipleSamples(): boolean {
var ofSamples = this.ofSamples;
return ofSamples > 1;
}
_getAriaExpanded() {
return this.actualSize ? 'true' : 'false';
}
override attached() {
this.reload();
}
@observe('run', 'tag')
reload() {
if (!this.isAttached) {
return;
}
this._metadataCanceller.cancelAll();
const router = getRouter();
const url = addParams(router.pluginRoute('images', '/images'), {
tag: this.tag,
run: this.run,
sample: this.sample as any,
});
const updateSteps = this._metadataCanceller.cancellable((result) => {
if (result.cancelled) {
return;
}
const data = result.value as any;
const steps = data.map(this._createStepDatum.bind(this));
this.set('_steps', steps);
this.set('_stepIndex', steps.length - 1);
});
this.requestManager.request(url).then(updateSteps);
}
_createStepDatum(imageMetadata) {
let url = getRouter().pluginRoute('images', '/individualImage');
// Include wall_time just to disambiguate the URL and force
// the browser to reload the image when the URL changes. The
// backend doesn't care about the value.
url = addParams(url, {ts: imageMetadata.wall_time});
url += '&' + imageMetadata.query;
return {
// The wall time within the metadata is in seconds. The Date
// constructor accepts a time in milliseconds, so we multiply by 1000.
wall_time: new Date(imageMetadata.wall_time * 1000),
step: imageMetadata.step,
url,
};
}
@observe('_currentStep', 'brightnessAdjustment', 'contrastPercentage')
_updateImageUrl() {
var currentStep = this._currentStep;
var brightnessAdjustment = this.brightnessAdjustment;
var contrastPercentage = this.contrastPercentage;
// We manually change the image URL (instead of binding to the
// image's src attribute) because we would like to manage what
// happens when the image starts and stops loading.
if (!currentStep) return;
const img = new Image();
this._imageCanceller.cancelAll();
img.onload = img.onerror = this._imageCanceller
.cancellable((result) => {
if (result.cancelled) {
return;
}
const mainImageContainer = this.$$('#main-image-container');
mainImageContainer.textContent = '';
(PolymerDom.dom(mainImageContainer) as any).appendChild(img);
this.set('_isImageLoading', false);
})
.bind(this);
img.style.filter = `contrast(${contrastPercentage}%) `;
img.style.filter += `brightness(${brightnessAdjustment})`;
// Load the new image.
this.set('_isImageLoading', true);
img.src = (currentStep as any).url;
}
_handleTap(e) {
this.set('actualSize', !this.actualSize);
}
_toLocaleString(number) {
// Shows commas (or locale-appropriate punctuation) for large numbers.
return number.toLocaleString();
}
}