@jax-data-science/sci-image-visualizer 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13397,6 +13397,10 @@ class VisualizerComponent {
13397
13397
  * the newer render's overlay, or applying its ROIs.
13398
13398
  */
13399
13399
  this.renderToken = 0;
13400
+ /** Set once the template, and with it the plot div, exists (see ngAfterViewInit). */
13401
+ this.viewReady = false;
13402
+ /** An image-less dataset whose draw waits for the view: see plotSpatialWithoutImage. */
13403
+ this.pendingSpatialDraw = null;
13400
13404
  this.zIndex = 0;
13401
13405
  this.maxIndex = 0;
13402
13406
  this.contextMenuItems = [];
@@ -14444,6 +14448,20 @@ class VisualizerComponent {
14444
14448
  requestAnimationFrame(() => this.renderIntensityInset());
14445
14449
  };
14446
14450
  window.addEventListener('resize', this.profileResizeListener);
14451
+ this.onViewReady();
14452
+ }
14453
+ /** The plot div exists now: run an image-less draw that arrived before it did. */
14454
+ onViewReady() {
14455
+ this.viewReady = true;
14456
+ const pending = this.pendingSpatialDraw;
14457
+ this.pendingSpatialDraw = null;
14458
+ // Only if it is still the dataset on offer and no image has arrived meanwhile. By id, not by
14459
+ // object: the port may re-emit the same dataset as a new object (a colour-column change does),
14460
+ // and then the current object is the one to draw.
14461
+ const current = this.spatialDataset;
14462
+ if (pending && current && current.id === pending.id && !this.imageInfo) {
14463
+ void this.plotSpatialWithoutImage(current);
14464
+ }
14447
14465
  }
14448
14466
  onProfilePanelDragStart(e) {
14449
14467
  this.profilePanelDragging = true;
@@ -14740,6 +14758,13 @@ class VisualizerComponent {
14740
14758
  * dataset, so regions drawn here are kept per dataset like any image's.
14741
14759
  */
14742
14760
  async plotSpatialWithoutImage(dataset) {
14761
+ if (!this.viewReady) {
14762
+ // A host that creates the visualizer AFTER publishing the dataset (jit-ui opening a file
14763
+ // from its tree) has it replayed into ngOnInit, before the template and its plot div
14764
+ // exist; drawing now finds no target. Draw once the view is ready (onViewReady).
14765
+ this.pendingSpatialDraw = dataset;
14766
+ return;
14767
+ }
14743
14768
  // The same generation as the image pipeline: a newer image or dataset supersedes this
14744
14769
  // draw, and a superseded draw neither reports nor releases the newer one's loading state.
14745
14770
  const token = ++this.renderToken;
@@ -29368,6 +29393,10 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29368
29393
  this.loadingBadge = null;
29369
29394
  this.tileLoading = [];
29370
29395
  this.observationsLoading = 0;
29396
+ /** Image tiles in flight, and the scene they belong to: a fetch that outlives {@link reset}
29397
+ * settles against the old generation and leaves the new scene's count alone. */
29398
+ this.imageTilesLoading = 0;
29399
+ this.imageTilesGeneration = 0;
29371
29400
  /** Monotonic load generation. Bumped by {@link reset} and {@link cancelLoading}; the frame-loading
29372
29401
  * loops (volume assembly, surface preload) capture it and bail when it changes, so a Cancel (or a
29373
29402
  * new plot) actually stops fetching frames instead of running to completion in the background. */
@@ -29686,6 +29715,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29686
29715
  return false;
29687
29716
  }
29688
29717
  this.reset();
29718
+ // This plot's scene. A newer plot resets into the next one while this one still awaits, and
29719
+ // a superseded plot must not go on to draw (or count image tiles) into the newer scene.
29720
+ const scene = this.imageTilesGeneration;
29689
29721
  this.host = host;
29690
29722
  this.plotDivId = plotDiv;
29691
29723
  this.currentPlotType = plotType;
@@ -29727,6 +29759,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29727
29759
  });
29728
29760
  this.viewer = viewer;
29729
29761
  await viewer.ready;
29762
+ if (scene !== this.imageTilesGeneration)
29763
+ return false;
29730
29764
  if (isSpatialOmics3d(plotType)) {
29731
29765
  await this.mountSpatialOmics3d(viewer, host);
29732
29766
  }
@@ -29823,11 +29857,15 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29823
29857
  const v = this.viewer;
29824
29858
  if (!v)
29825
29859
  return;
29860
+ const scene = this.imageTilesGeneration;
29826
29861
  const desc = await this.ensureDescriptor();
29862
+ // Reset into a newer scene while the descriptor was in flight: this render is superseded.
29863
+ if (scene !== this.imageTilesGeneration)
29864
+ return;
29827
29865
  if (desc && desc.levels?.length) {
29828
29866
  if (token != null && token !== this.sliceReq)
29829
29867
  return;
29830
- await this.renderImageTiled(z, desc);
29868
+ await this.renderImageTiled(z, desc, scene);
29831
29869
  return;
29832
29870
  }
29833
29871
  return this.renderImageStitched(z, token);
@@ -29920,7 +29958,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29920
29958
  * coordinates so regions align. Same three modes as the stitch path. Per-channel layers use the
29921
29959
  * REAL pyramid levels (per-channel tiles only exist there); the composite uses all levels.
29922
29960
  */
29923
- async renderImageTiled(z, desc) {
29961
+ async renderImageTiled(z, desc, scene) {
29924
29962
  const v = this.viewer;
29925
29963
  if (!v)
29926
29964
  return;
@@ -29937,7 +29975,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29937
29975
  const st = states.find((s) => s.index === c);
29938
29976
  const color = st?.color ?? desc.channelInfo?.[c]?.color ?? tintFor(c);
29939
29977
  views.push({
29940
- source: this.buildTiledSource(desc, c, 1),
29978
+ source: this.buildTiledSource(desc, c, 1, scene),
29941
29979
  tint: color,
29942
29980
  name: st?.name ?? `ch${c}`,
29943
29981
  contrastLimits: [st?.min ?? 0, st?.max ?? 255],
@@ -29953,7 +29991,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29953
29991
  const st = states[0];
29954
29992
  this.channelView.render('grayscale', [
29955
29993
  {
29956
- source: this.buildTiledSource(desc, undefined, 1),
29994
+ source: this.buildTiledSource(desc, undefined, 1, scene),
29957
29995
  colormap: this.grayscaleColormap(),
29958
29996
  contrastLimits: [st?.min ?? 0, st?.max ?? 255],
29959
29997
  gamma: st?.gamma ?? 1,
@@ -29963,7 +30001,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29963
30001
  }
29964
30002
  else {
29965
30003
  this.imageMode = 'rgb';
29966
- this.channelView.render('rgb', [{ source: this.buildTiledSource(desc, undefined, 4) }], {
30004
+ this.channelView.render('rgb', [{ source: this.buildTiledSource(desc, undefined, 4, scene) }], {
29967
30005
  interpolation,
29968
30006
  });
29969
30007
  }
@@ -29974,8 +30012,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29974
30012
  await this.refreshHistogramSamples(z, desc);
29975
30013
  }
29976
30014
  /** Build a pyramidal TiledSource backed by the server `/tile` endpoint. `channel` selects a band
29977
- * (grayscale luminance, real levels only); omit it for the composite (RGBA, all levels). */
29978
- buildTiledSource(desc, channel, channels) {
30015
+ * (grayscale luminance, real levels only); omit it for the composite (RGBA, all levels).
30016
+ * `scene` is the generation of the render that asked for it, taken before that render's awaits. */
30017
+ buildTiledSource(desc, channel, channels, scene) {
29979
30018
  const infoB64 = this.tiles.getSelectedInfoB64() ?? '';
29980
30019
  // Per-channel tiles exist only at REAL Bio-Formats levels; the composite exists at all levels.
29981
30020
  const usable = channel == null
@@ -29985,6 +30024,9 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29985
30024
  const tileSize = desc.tileSize || TILE_SIZE;
29986
30025
  const ch = channel == null ? '' : `&channel=${channel}`;
29987
30026
  const api = this.api;
30027
+ // The scene this source draws: its tiles count on the badge only while that scene is current,
30028
+ // so a request the disposed source still issues after a reset never shows on the new one.
30029
+ const generation = scene;
29988
30030
  return {
29989
30031
  kind: 'tiled',
29990
30032
  width: desc.width,
@@ -29998,17 +30040,31 @@ class NapariVisualizerService extends BaseStoreVisualizer {
29998
30040
  fetchTile: async (key) => {
29999
30041
  const res = usable[key.level]?.res ?? key.level;
30000
30042
  const url = `${api}tile?info=${infoB64}&res=${res}&col=${key.col}&row=${key.row}&z=${key.z}&tileSize=${tileSize}${ch}`;
30001
- const headers = await this.tiles
30002
- .getAuthHeaders()
30003
- .catch(() => ({}));
30004
- const resp = await fetch(url, { headers });
30005
- if (!resp.ok) {
30006
- throw new Error(`[napari-js] tile ${key.level}/${key.col}/${key.row} → ${resp.status}`);
30043
+ // "Image reloading…" on the loading badge while any tile of the image is in flight.
30044
+ const counted = generation === this.imageTilesGeneration;
30045
+ if (counted) {
30046
+ this.imageTilesLoading++;
30047
+ this.showLoading();
30048
+ }
30049
+ try {
30050
+ const headers = await this.tiles
30051
+ .getAuthHeaders()
30052
+ .catch(() => ({}));
30053
+ const resp = await fetch(url, { headers });
30054
+ if (!resp.ok) {
30055
+ throw new Error(`[napari-js] tile ${key.level}/${key.col}/${key.row} → ${resp.status}`);
30056
+ }
30057
+ const bmp = await createImageBitmap(await resp.blob());
30058
+ if (channels === 4)
30059
+ return { width: bmp.width, height: bmp.height, data: bmp };
30060
+ return this.bitmapToLuminance(bmp);
30061
+ }
30062
+ finally {
30063
+ if (counted && generation === this.imageTilesGeneration) {
30064
+ this.imageTilesLoading--;
30065
+ this.showLoading();
30066
+ }
30007
30067
  }
30008
- const bmp = await createImageBitmap(await resp.blob());
30009
- if (channels === 4)
30010
- return { width: bmp.width, height: bmp.height, data: bmp };
30011
- return this.bitmapToLuminance(bmp);
30012
30068
  },
30013
30069
  };
30014
30070
  }
@@ -30170,7 +30226,11 @@ class NapariVisualizerService extends BaseStoreVisualizer {
30170
30226
  if (!this.host)
30171
30227
  return;
30172
30228
  this.loadingBadge ??= new NapariLoadingBadge(this.host);
30173
- this.loadingBadge.set([...(this.observationsLoading > 0 ? ['Observations'] : []), ...this.tileLoading]);
30229
+ this.loadingBadge.set([
30230
+ ...(this.imageTilesLoading > 0 ? ['Image'] : []),
30231
+ ...(this.observationsLoading > 0 ? ['Observations'] : []),
30232
+ ...this.tileLoading,
30233
+ ]);
30174
30234
  }
30175
30235
  installNavigator(z) {
30176
30236
  this.navigator?.destroy();
@@ -32719,6 +32779,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
32719
32779
  this.loadingBadge = null;
32720
32780
  this.tileLoading = [];
32721
32781
  this.observationsLoading = 0;
32782
+ this.imageTilesLoading = 0;
32783
+ this.imageTilesGeneration++;
32722
32784
  this.regionOverlay?.destroy();
32723
32785
  this.regionOverlay = null;
32724
32786
  this.axesLabels?.destroy();