From 7950af6a9e501848e271672b9e381bca2f9fed2a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 17:59:52 +0000 Subject: [PATCH 1/3] Normalize Polymer run selection IDs for NgRx sync Co-authored-by: Samuel --- .../webapp/runs/effects/runs_effects.ts | 89 +++++++++++++++++-- 1 file changed, 82 insertions(+), 7 deletions(-) diff --git a/tensorbored/webapp/runs/effects/runs_effects.ts b/tensorbored/webapp/runs/effects/runs_effects.ts index 045d8acbf0a..0641f3db66a 100644 --- a/tensorbored/webapp/runs/effects/runs_effects.ts +++ b/tensorbored/webapp/runs/effects/runs_effects.ts @@ -124,11 +124,16 @@ function stripExpPrefix(runId: string): string { return i >= 0 ? runId.substring(i + 1) : runId; } +function runIdHasExperimentPrefix(runId: string): boolean { + return runId.indexOf('/') > 0; +} + function toPolymerRunColorMap( runColorMap: Record ): Record { const byName: Record = {}; - for (const [runId, hex] of Object.entries(runColorMap)) { + for (const runId of Object.keys(runColorMap).sort()) { + const hex = runColorMap[runId]; const runName = stripExpPrefix(runId); const existing = byName[runName]; if (existing !== undefined && existing !== hex) { @@ -172,6 +177,45 @@ function filterRunColorMapToActiveRoute( return filtered; } +function normalizeSelectionForActiveRoute( + runSelection: Array<[string, boolean]>, + experimentIds: string[] | null, + runIdToExperimentId: Record +): Array<[string, boolean]> { + const normalized = new Map(); + const activeExperimentIds = experimentIds ? new Set(experimentIds) : null; + + for (const [storedRunId, selected] of runSelection) { + if (runIdHasExperimentPrefix(storedRunId)) { + normalized.set(storedRunId, selected); + continue; + } + + const bareName = storedRunId; + let matched = false; + if (activeExperimentIds) { + for (const [runId, experimentId] of Object.entries(runIdToExperimentId)) { + if (!activeExperimentIds.has(experimentId)) continue; + if (stripExpPrefix(runId) !== bareName) continue; + normalized.set(runId, selected); + matched = true; + } + } + + if (matched) continue; + + if (activeExperimentIds && activeExperimentIds.size === 1) { + const [experimentId] = Array.from(activeExperimentIds); + normalized.set(`${experimentId}/${bareName}`, selected); + continue; + } + + normalized.set(storedRunId, selected); + } + + return Array.from(normalized.entries()); +} + function storedSelectionEqualsMap( runSelection: Array<[string, boolean]>, map: Map @@ -321,11 +365,19 @@ export class RunsEffects { this.loadRunSelectionFromStorage$ = createEffect(() => { return this.actions$.pipe( ofType(navigated), - map(() => { + withLatestFrom( + this.store.select(getExperimentIdsFromRoute), + this.store.select(getRunIdToExperimentId) + ), + map(([, experimentIds, runIdToExperimentId]) => { const stored = safeParseStoredRunSelection( window.localStorage.getItem(RUN_SELECTION_STORAGE_KEY) ); - const runSelection = stored.runSelection; + const runSelection = normalizeSelectionForActiveRoute( + stored.runSelection, + experimentIds, + runIdToExperimentId + ); // If stored selection exists but ALL runs are set to false // (none visible), discard it so the default behaviour (all @@ -435,12 +487,35 @@ export class RunsEffects { ); return stored.runSelection; }), - withLatestFrom(this.store.select(getRunSelectionMap)), + withLatestFrom( + this.store.select(getRunSelectionMap), + this.store.select(getExperimentIdsFromRoute), + this.store.select(getRunIdToExperimentId) + ), + map( + ([ + runSelection, + selectionMap, + experimentIds, + runIdToExperimentId, + ]) => { + const normalizedRunSelection = normalizeSelectionForActiveRoute( + runSelection, + experimentIds, + runIdToExperimentId + ); + return {normalizedRunSelection, selectionMap}; + } + ), filter( - ([runSelection, selectionMap]) => - !storedSelectionEqualsMap(runSelection, selectionMap) + ({normalizedRunSelection, selectionMap}) => + !storedSelectionEqualsMap(normalizedRunSelection, selectionMap) ), - map(([runSelection]) => actions.runSelectionStateLoaded({runSelection})) + map(({normalizedRunSelection}) => + actions.runSelectionStateLoaded({ + runSelection: normalizedRunSelection, + }) + ) ); }); From 0ba8fb59a61818237ec918166beb1aa3379c17b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 18:04:21 +0000 Subject: [PATCH 2/3] Redraw scalar charts when hidden tabs resize Co-authored-by: Samuel --- .../tf-line-chart-data-loader.ts | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts b/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts index 3ab3f87593f..38570276909 100644 --- a/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts +++ b/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts @@ -55,7 +55,7 @@ const cascadingRedraw = _.throttle(function internalRedraw() { x._maybeRenderedInBadState = false; } window.cancelAnimationFrame(redrawRaf); - window.requestAnimationFrame(internalRedraw); + redrawRaf = window.requestAnimationFrame(internalRedraw); }, 100); // A component that fetches data from the TensorBoard server and renders it into @@ -140,6 +140,38 @@ class _TfLineChartDataLoader `; private _redrawRaf: number | null = null; + private _resizeObserver: ResizeObserver | null = null; + private _lastObservedWidth = -1; + private _lastObservedHeight = -1; + + override connectedCallback() { + super.connectedCallback(); + this._resizeObserver = new ResizeObserver((entries) => { + if (entries.length === 0) return; + const {width, height} = entries[0].contentRect; + const roundedWidth = Math.round(width); + const roundedHeight = Math.round(height); + if ( + roundedWidth === this._lastObservedWidth && + roundedHeight === this._lastObservedHeight + ) { + return; + } + this._lastObservedWidth = roundedWidth; + this._lastObservedHeight = roundedHeight; + + if (roundedWidth <= 0 || roundedHeight <= 0 || !this.active) { + this._maybeRenderedInBadState = true; + return; + } + + if (!redrawQueue.includes(this)) { + redrawQueue.push(this); + } + cascadingRedraw(); + }); + this._resizeObserver.observe(this); + } @property({ type: Boolean, @@ -231,6 +263,10 @@ class _TfLineChartDataLoader } override disconnectedCallback() { + if (this._resizeObserver !== null) { + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } super.disconnectedCallback(); if (this._redrawRaf !== null) cancelAnimationFrame(this._redrawRaf); } From cc29d64821a9d9603fc16e0d9284a9570c124964 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Feb 2026 20:32:34 +0000 Subject: [PATCH 3/3] Live-sync Polymer run colors on time-series updates Co-authored-by: Samuel --- .../components/tf_color_scale/colorScale.ts | 4 ++- .../tf-line-chart-data-loader.ts | 27 ++++++++++++++- .../tf_runs_selector/tf-runs-selector.ts | 22 +++++++++++- .../tf_audio_dashboard/tf-audio-loader.ts | 27 +++++++++++++-- .../tf_image_dashboard/tf-image-loader.ts | 26 ++++++++++++-- .../tf_pr_curve_dashboard/tf-pr-curve-card.ts | 30 ++++++++++++++-- .../tf-pr-curve-steps-selector.ts | 34 +++++++++++++++++-- .../text/tf_text_dashboard/tf-text-loader.ts | 28 +++++++++++++-- 8 files changed, 183 insertions(+), 15 deletions(-) diff --git a/tensorbored/components/tf_color_scale/colorScale.ts b/tensorbored/components/tf_color_scale/colorScale.ts index 242e9554a19..f4f94abfa1b 100644 --- a/tensorbored/components/tf_color_scale/colorScale.ts +++ b/tensorbored/components/tf_color_scale/colorScale.ts @@ -18,6 +18,8 @@ import {experimentsStore} from '../tf_backend/experimentsStore'; import {runsStore} from '../tf_backend/runsStore'; import {standard} from './palettes'; +export const RUN_COLOR_MAP_CHANGED_EVENT = 'tb-run-color-map-changed'; + /** * Read the run-name → hex-color map seeded on window by the NgRx * RunsEffects syncPolymerRunColorMap$ effect. Returns null before the @@ -77,7 +79,7 @@ function createAutoUpdateColorScale( } store.addListener(update); // Re-read colors when the NgRx store subscription updates them. - window.addEventListener('tb-run-color-map-changed', update); + window.addEventListener(RUN_COLOR_MAP_CHANGED_EVENT, update); update(); return (runName) => colorScale.getColor(runName); } diff --git a/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts b/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts index 38570276909..4f462239131 100644 --- a/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts +++ b/tensorbored/components/tf_line_chart_data_loader/tf-line-chart-data-loader.ts @@ -19,7 +19,10 @@ import * as Plottable from 'plottable'; import '../polymer/irons_and_papers'; import {LegacyElementMixin} from '../polymer/legacy_element_mixin'; import {RequestManager} from '../tf_backend/requestManager'; -import {runsColorScale} from '../tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../tf_color_scale/colorScale'; import {DataLoaderBehavior} from '../tf_dashboard_common/data-loader-behavior'; import { AxisScaleType, @@ -141,11 +144,26 @@ class _TfLineChartDataLoader private _redrawRaf: number | null = null; private _resizeObserver: ResizeObserver | null = null; + private _runColorMapChangedListener: (() => void) | null = null; private _lastObservedWidth = -1; private _lastObservedHeight = -1; override connectedCallback() { super.connectedCallback(); + this._runColorMapChangedListener = () => { + if (this.active) { + if (!redrawQueue.includes(this)) { + redrawQueue.push(this); + } + cascadingRedraw(); + } else { + this._maybeRenderedInBadState = true; + } + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); this._resizeObserver = new ResizeObserver((entries) => { if (entries.length === 0) return; const {width, height} = entries[0].contentRect; @@ -263,6 +281,13 @@ class _TfLineChartDataLoader } override disconnectedCallback() { + if (this._runColorMapChangedListener !== null) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } if (this._resizeObserver !== null) { this._resizeObserver.disconnect(); this._resizeObserver = null; diff --git a/tensorbored/components/tf_runs_selector/tf-runs-selector.ts b/tensorbored/components/tf_runs_selector/tf-runs-selector.ts index 58d0bc1eefe..1f4e0aadccb 100644 --- a/tensorbored/components/tf_runs_selector/tf-runs-selector.ts +++ b/tensorbored/components/tf_runs_selector/tf-runs-selector.ts @@ -20,7 +20,10 @@ import {LegacyElementMixin} from '../polymer/legacy_element_mixin'; import * as baseStore from '../tf_backend/baseStore'; import {environmentStore} from '../tf_backend/environmentStore'; import {runsStore} from '../tf_backend/runsStore'; -import {runsColorScale} from '../tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../tf_color_scale/colorScale'; import '../tf_dashboard_common/tf-multi-checkbox'; import '../tf_wbr_string/tf-wbr-string'; @@ -245,6 +248,7 @@ class TfRunsSelector extends LegacyElementMixin(PolymerElement) { _envStoreListener: baseStore.ListenKey; private _selectionChangedListener: (() => void) | null = null; + private _runColorMapChangedListener: (() => void) | null = null; private _syncingFromStorage = false; override attached() { @@ -264,6 +268,15 @@ class TfRunsSelector extends LegacyElementMixin(PolymerElement) { 'tb-run-selection-changed', this._selectionChangedListener ); + + this._runColorMapChangedListener = () => { + this.set('coloring', {getColor: runsColorScale}); + (this.$.multiCheckbox as any).synchronizeColors(); + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); } private _syncFromStorage() { @@ -282,6 +295,13 @@ class TfRunsSelector extends LegacyElementMixin(PolymerElement) { ); this._selectionChangedListener = null; } + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } } _toggleAll() { diff --git a/tensorbored/plugins/audio/tf_audio_dashboard/tf-audio-loader.ts b/tensorbored/plugins/audio/tf_audio_dashboard/tf-audio-loader.ts index 69bd4981ec0..82741f16500 100644 --- a/tensorbored/plugins/audio/tf_audio_dashboard/tf-audio-loader.ts +++ b/tensorbored/plugins/audio/tf_audio_dashboard/tf-audio-loader.ts @@ -23,7 +23,10 @@ import {getRouter} from '../../../components/tf_backend/router'; import '../../../components/tf_card_heading/tf-card-heading'; import '../../../components/tf_card_heading/tf-card-heading-style'; import {formatDate} from '../../../components/tf_card_heading/util'; -import {runsColorScale} from '../../../components/tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../../../components/tf_color_scale/colorScale'; import '../../../components/tf_dashboard_common/tensorboard-color'; import '../../../components/tf_markdown_view/tf-markdown-view'; @@ -171,8 +174,11 @@ class _TfAudioLoader _stepIndex: number; _attached: boolean = false; + @property({type: Number}) + _runColorVersion = 0; + private _runColorMapChangedListener: (() => void) | null = null; - @computed('run') + @computed('run', '_runColorVersion') get _runColor(): string { var run = this.run; return runsColorScale(run); @@ -216,10 +222,27 @@ class _TfAudioLoader } override attached() { + this._runColorMapChangedListener = () => { + this._runColorVersion += 1; + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); this._attached = true; this.reload(); } + override detached() { + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } + } + @observe('run', 'tag') _reloadOnRunTagChange() { this.reload(); diff --git a/tensorbored/plugins/image/tf_image_dashboard/tf-image-loader.ts b/tensorbored/plugins/image/tf_image_dashboard/tf-image-loader.ts index ffa8b0a913c..423a80cd3c5 100644 --- a/tensorbored/plugins/image/tf_image_dashboard/tf-image-loader.ts +++ b/tensorbored/plugins/image/tf_image_dashboard/tf-image-loader.ts @@ -23,7 +23,10 @@ import {getRouter} from '../../../components/tf_backend/router'; import '../../../components/tf_card_heading/tf-card-heading'; import '../../../components/tf_card_heading/tf-card-heading-style'; import {formatDate} from '../../../components/tf_card_heading/util'; -import {runsColorScale} from '../../../components/tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../../../components/tf_color_scale/colorScale'; import '../../../components/tf_dashboard_common/tensorboard-color'; @customElement('tf-image-loader') @@ -232,7 +235,10 @@ class TfImageLoader extends LegacyElementMixin(PolymerElement) { type: Boolean, }) _isImageLoading: boolean = false; - @computed('run') + @property({type: Number}) + _runColorVersion = 0; + private _runColorMapChangedListener: (() => void) | null = null; + @computed('run', '_runColorVersion') get _runColor(): string { var run = this.run; return runsColorScale(run); @@ -284,8 +290,24 @@ class TfImageLoader extends LegacyElementMixin(PolymerElement) { return this.actualSize ? 'true' : 'false'; } override attached() { + this._runColorMapChangedListener = () => { + this._runColorVersion += 1; + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); this.reload(); } + override detached() { + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } + } @observe('run', 'tag') reload() { if (!this.isAttached) { diff --git a/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-card.ts b/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-card.ts index 015230f4574..adad62418fb 100644 --- a/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-card.ts +++ b/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-card.ts @@ -23,7 +23,10 @@ import {RequestManager} from '../../../components/tf_backend/requestManager'; import {getRouter} from '../../../components/tf_backend/router'; import {addParams} from '../../../components/tf_backend/urlPathHelpers'; import '../../../components/tf_card_heading/tf-card-heading'; -import {runsColorScale} from '../../../components/tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../../../components/tf_color_scale/colorScale'; import {RequestDataCallback} from '../../../components/tf_dashboard_common/data-loader-behavior'; import '../../../components/tf_line_chart_data_loader/tf-line-chart-data-loader'; import * as vz_chart_helpers from '../../../components/vz_chart_helpers/vz-chart-helpers'; @@ -81,7 +84,7 @@ export class TfPrCurveCard extends PolymerElement {
[[run]] is at @@ -198,6 +201,9 @@ export class TfPrCurveCard extends PolymerElement { @property({type: Boolean}) _attached: boolean; + @property({type: Number}) + _runColorVersion = 0; + private _runColorMapChangedListener: (() => void) | null = null; @property({type: Object}) _xComponentsCreationMethod = () => { @@ -306,12 +312,19 @@ export class TfPrCurveCard extends PolymerElement { }; } - _computeRunColor(run) { + _computeRunColor(run, _) { return runsColorScale(run); } connectedCallback() { super.connectedCallback(); + this._runColorMapChangedListener = () => { + this._runColorVersion += 1; + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); // Defer reloading until after we're attached, because that ensures that // the requestManager has been set from above. (Polymer is tricky // sometimes) @@ -319,6 +332,17 @@ export class TfPrCurveCard extends PolymerElement { this.reload(); } + disconnectedCallback() { + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } + super.disconnectedCallback(); + } + _getChartDataLoader() { // tslint:disable-next-line:no-unnecessary-type-assertion return this.shadowRoot?.querySelector('tf-line-chart-data-loader') as any; // TfLineChartDataLoader diff --git a/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-steps-selector.ts b/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-steps-selector.ts index 797570cabd3..15cd2f2ab67 100644 --- a/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-steps-selector.ts +++ b/tensorbored/plugins/pr_curve/tf_pr_curve_dashboard/tf-pr-curve-steps-selector.ts @@ -17,7 +17,10 @@ import {computed, customElement, observe, property} from '@polymer/decorators'; import {html, PolymerElement} from '@polymer/polymer'; import * as _ from 'lodash'; import '../../../components/polymer/irons_and_papers'; -import {runsColorScale} from '../../../components/tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../../../components/tf_color_scale/colorScale'; @customElement('tf-pr-curve-steps-selector') // tslint:disable-next-line:no-unused-variable @@ -28,7 +31,7 @@ class TfPrCurveStepsSelector extends PolymerElement {
[[run]]
@@ -89,8 +92,33 @@ class TfPrCurveStepsSelector extends PolymerElement { @property({type: Object}) _runToStepIndex: object = {}; + @property({type: Number}) + _runColorVersion = 0; + private _runColorMapChangedListener: (() => void) | null = null; + + connectedCallback() { + super.connectedCallback(); + this._runColorMapChangedListener = () => { + this._runColorVersion += 1; + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + } + + disconnectedCallback() { + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } + super.disconnectedCallback(); + } - _computeColorForRun(run) { + _computeColorForRun(run, _) { return runsColorScale(run); } diff --git a/tensorbored/plugins/text/tf_text_dashboard/tf-text-loader.ts b/tensorbored/plugins/text/tf_text_dashboard/tf-text-loader.ts index c99011633d1..feecff14117 100644 --- a/tensorbored/plugins/text/tf_text_dashboard/tf-text-loader.ts +++ b/tensorbored/plugins/text/tf_text_dashboard/tf-text-loader.ts @@ -23,7 +23,10 @@ import {RequestManager} from '../../../components/tf_backend/requestManager'; import {getRouter} from '../../../components/tf_backend/router'; import {addParams} from '../../../components/tf_backend/urlPathHelpers'; import '../../../components/tf_card_heading/tf-card-heading'; -import {runsColorScale} from '../../../components/tf_color_scale/colorScale'; +import { + RUN_COLOR_MAP_CHANGED_EVENT, + runsColorScale, +} from '../../../components/tf_color_scale/colorScale'; import '../../../components/tf_dashboard_common/scrollbar-style'; import '../../../components/tf_markdown_view/tf-markdown-view'; @@ -115,7 +118,11 @@ class TfTextLoader extends LegacyElementMixin(PolymerElement) { @property({type: Object}) _canceller: Canceller = new Canceller(); - @computed('run') + @property({type: Number}) + _runColorVersion = 0; + private _runColorMapChangedListener: (() => void) | null = null; + + @computed('run', '_runColorVersion') get _runColor(): string { var run = this.run; return runsColorScale(run); @@ -130,9 +137,26 @@ class TfTextLoader extends LegacyElementMixin(PolymerElement) { } override attached() { + this._runColorMapChangedListener = () => { + this._runColorVersion += 1; + }; + window.addEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); this.reload(); } + override detached() { + if (this._runColorMapChangedListener) { + window.removeEventListener( + RUN_COLOR_MAP_CHANGED_EVENT, + this._runColorMapChangedListener + ); + this._runColorMapChangedListener = null; + } + } + reload() { if (!this.isAttached) { return;