@vitessce/neuroglancer 3.9.6 → 3.9.7

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.
@@ -57,6 +57,8 @@ const ROTATION_EPS = 1e-3;
57
57
  const TARGET_EPS = 0.5;
58
58
  const NG_ROT_COOLDOWN_MS = 120;
59
59
 
60
+ const GUIDE_URL = 'https://vitessce.io/docs/ng-guide/';
61
+
60
62
  const LAST_INTERACTION_SOURCE = {
61
63
  vitessce: 'vitessce',
62
64
  neuroglancer: 'neuroglancer',
@@ -281,13 +283,40 @@ export function NeuroglancerSubscriber(props) {
281
283
  segmentationChannelScopesByLayer?.[layerScope]?.forEach((channelScope) => {
282
284
  const { obsSets: layerSets, obsIndex: layerIndex } = obsSegmentationsSetsData
283
285
  ?.[layerScope]?.[channelScope] || {};
284
- if (layerSets && layerIndex) {
285
- const {
286
- obsSetColor,
287
- obsColorEncoding,
288
- obsSetSelection,
289
- additionalObsSets,
290
- } = segmentationChannelCoordination[0][layerScope][channelScope];
286
+ const {
287
+ obsSetColor,
288
+ obsColorEncoding,
289
+ obsSetSelection,
290
+ additionalObsSets,
291
+ spatialChannelColor,
292
+ } = segmentationChannelCoordination[0][layerScope][channelScope];
293
+
294
+ if (obsColorEncoding === 'spatialChannelColor') {
295
+ // All segments get the same static channel color
296
+ if (layerIndex && spatialChannelColor) {
297
+ const hex = rgbToHex(spatialChannelColor);
298
+ const ngCellColors = {};
299
+
300
+ if (obsSetSelection?.length > 0) {
301
+ // Only color the segments belonging to selected sets.
302
+ const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
303
+ const selectedIds = new Set();
304
+ obsSetSelection.forEach((setPath) => {
305
+ const rootNode = mergedCellSets?.tree?.find(n => n.name === setPath[0]);
306
+ const leafNode = setPath.length > 1
307
+ ? rootNode?.children?.find(n => n.name === setPath[1])
308
+ : rootNode;
309
+ leafNode?.set?.forEach(([id]) => selectedIds.add(String(id)));
310
+ });
311
+ layerIndex.forEach((id) => {
312
+ if (selectedIds.has(String(id))) {
313
+ ngCellColors[id] = hex;
314
+ }
315
+ });
316
+ }
317
+ result[layerScope][channelScope] = ngCellColors;
318
+ }
319
+ } else if (layerSets && layerIndex) {
291
320
  const mergedCellSets = mergeObsSets(layerSets, additionalObsSets);
292
321
  const cellColors = getCellColors({
293
322
  cellSets: mergedCellSets,
@@ -301,13 +330,6 @@ export function NeuroglancerSubscriber(props) {
301
330
  cellColors.forEach((color, i) => {
302
331
  ngCellColors[i] = rgbToHex(color);
303
332
  });
304
- /* // TODO: Is this necessary?
305
- const obsColorIndices = treeToCellSetColorIndicesBySetNames(
306
- mergedLayerSets,
307
- obsSetSelection,
308
- obsSetColor,
309
- );
310
- */
311
333
  result[layerScope][channelScope] = ngCellColors;
312
334
  }
313
335
  });
@@ -512,6 +534,8 @@ export function NeuroglancerSubscriber(props) {
512
534
  }, []);
513
535
 
514
536
  const onSegmentClick = useCallback((value) => {
537
+ // Note: this callback is no longer called by the child component.
538
+ // Reference: https://github.com/vitessce/vitessce/pull/2439
515
539
  if (value) {
516
540
  const id = String(value);
517
541
  const selectedCellIds = [id];
@@ -536,14 +560,16 @@ export function NeuroglancerSubscriber(props) {
536
560
  setCellColorEncoding, setCellSetColor, setCellSetSelection,
537
561
  ]);
538
562
 
539
- // Get the ultimate cellColorMapping to pass to NeuroglancerComp as a prop.
540
- // For now, we take the first layer and channel for cell colors.
541
- const cellColorMapping = useMemo(() => (segmentationColorMapping
542
- ?.[segmentationLayerScopes?.[0]]
543
- ?.[segmentationChannelScopesByLayer?.[segmentationLayerScopes?.[0]]?.[0]]
544
- ?? {}
545
- ), [segmentationColorMapping]);
563
+ // Get the ultimate cellColorMapping for each layer to pass to NeuroglancerComp as a prop.
546
564
 
565
+ const cellColorMappingByLayer = useMemo(() => {
566
+ const result = {};
567
+ segmentationLayerScopes?.forEach((layerScope) => {
568
+ const channelScope = segmentationChannelScopesByLayer?.[layerScope]?.[0];
569
+ result[layerScope] = segmentationColorMapping?.[layerScope]?.[channelScope] ?? {};
570
+ });
571
+ return result;
572
+ }, [segmentationColorMapping, segmentationLayerScopes, segmentationChannelScopesByLayer]);
547
573
 
548
574
  // TODO: try to simplify using useMemoCustomComparison?
549
575
  // This would allow us to refactor a lot of the checking-for-changes logic into a comparison function,
@@ -556,9 +582,6 @@ export function NeuroglancerSubscriber(props) {
556
582
  return current;
557
583
  }
558
584
 
559
- const nextSegments = Object.keys(cellColorMapping);
560
- const prevLayer = current?.layers?.[0] || {};
561
- const prevSegments = prevLayer.segments || [];
562
585
  const { projectionScale, projectionOrientation, position } = current;
563
586
 
564
587
  // Did Vitessce coords change vs the *previous* render?
@@ -695,20 +718,23 @@ export function NeuroglancerSubscriber(props) {
695
718
  lastInteractionSource.current = null;
696
719
  }
697
720
 
698
- const newLayer0 = {
699
- ...prevLayer,
700
- segments: nextSegments,
701
- segmentColors: cellColorMapping,
702
- };
703
-
721
+ const updatedLayers = current?.layers?.map((layer, idx) => {
722
+ const layerScope = segmentationLayerScopes?.[idx];
723
+ const layerColorMapping = cellColorMappingByLayer?.[layerScope] ?? {};
724
+ const layerSegments = Object.keys(layerColorMapping);
725
+ return {
726
+ ...layer,
727
+ segments: layerSegments,
728
+ segmentColors: layerColorMapping,
729
+ };
730
+ }) ?? [];
704
731
 
705
732
  const updated = {
706
733
  ...current,
707
734
  projectionScale: nextProjectionScale,
708
735
  projectionOrientation: nextOrientation,
709
736
  position: nextPosition,
710
- layers: prevSegments.length === 0 ? [newLayer0, ...(current?.layers?.slice(1)
711
- || [])] : current?.layers,
737
+ layers: updatedLayers,
712
738
  };
713
739
 
714
740
  latestViewerStateRef.current = updated;
@@ -724,7 +750,7 @@ export function NeuroglancerSubscriber(props) {
724
750
  };
725
751
 
726
752
  return updated;
727
- }, [cellColorMapping, spatialZoom, spatialRotationX, spatialRotationY,
753
+ }, [cellColorMappingByLayer, spatialZoom, spatialRotationX, spatialRotationY,
728
754
  spatialRotationZ, spatialTargetX, spatialTargetY, initalViewerState,
729
755
  latestViewerStateIteration]);
730
756
 
@@ -741,6 +767,7 @@ export function NeuroglancerSubscriber(props) {
741
767
  // console.log(derivedViewerState);
742
768
 
743
769
  return (
770
+
744
771
  <TitleInfo
745
772
  title={title}
746
773
  info={subtitle}
@@ -753,30 +780,32 @@ export function NeuroglancerSubscriber(props) {
753
780
  isReady={isReady}
754
781
  errors={errors}
755
782
  withPadding={false}
783
+ guideUrl={GUIDE_URL}
756
784
  >
757
- <div style={{ position: 'relative', width: '100%', height: '100%' }} ref={containerRef}>
758
- <div style={{ position: 'absolute', top: 0, right: 0, zIndex: 50 }}>
759
- <MultiLegend
760
- theme="dark"
761
- maxHeight={ngHeight}
762
- segmentationLayerScopes={segmentationLayerScopes}
763
- segmentationLayerCoordination={segmentationLayerCoordination}
764
- segmentationChannelScopesByLayer={segmentationChannelScopesByLayer}
765
- segmentationChannelCoordination={segmentationChannelCoordination}
766
- />
767
- </div>
785
+ {hasLayers ? (
786
+ <div style={{ position: 'relative', width: '100%', height: '100%' }} ref={containerRef}>
787
+ <div style={{ position: 'absolute', top: 0, right: 0, zIndex: 50 }}>
788
+ <MultiLegend
789
+ theme="dark"
790
+ maxHeight={ngHeight}
791
+ segmentationLayerScopes={segmentationLayerScopes}
792
+ segmentationLayerCoordination={segmentationLayerCoordination}
793
+ segmentationChannelScopesByLayer={segmentationChannelScopesByLayer}
794
+ segmentationChannelCoordination={segmentationChannelCoordination}
795
+ />
796
+ </div>
768
797
 
769
- {hasLayers ? (
770
798
  <NeuroglancerComp
771
799
  classes={classes}
772
800
  onSegmentClick={onSegmentClick}
773
801
  onSelectHoveredCoords={onSegmentHighlight}
774
802
  viewerState={derivedViewerState}
775
- cellColorMapping={cellColorMapping}
803
+ cellColorMapping={cellColorMappingByLayer}
776
804
  setViewerState={handleStateUpdate}
777
805
  />
778
- ) : null}
779
- </div>
806
+ </div>
807
+ ) : null}
780
808
  </TitleInfo>
809
+
781
810
  );
782
811
  }
@@ -427,7 +427,7 @@ export default class Neuroglancer extends React.Component {
427
427
  this.disposers = [];
428
428
  this.prevColorOverrides = new Set();
429
429
  this.overrideColorsById = Object.create(null);
430
- this.allKnownIds = new Set();
430
+ this.allKnownIdsByLayer = {};
431
431
  }
432
432
 
433
433
  minimalPoseSnapshot = () => {
@@ -476,30 +476,36 @@ export default class Neuroglancer extends React.Component {
476
476
  };
477
477
 
478
478
  /* To add colors to the segments, turning unselected to grey */
479
- applyColorsAndVisibility = (cellColorMapping) => {
479
+ applyColorsAndVisibility = (cellColorMappingByLayer) => {
480
480
  if (!this.viewer) return;
481
- // Track all ids we've ever seen so we can grey the ones
482
- // that drop out of the current selection.
483
- const selected = { ...(cellColorMapping || {}) }; // clone, don't mutate props
484
- for (const id of Object.keys(selected)) this.allKnownIds.add(id);
485
- // If empty on first call, seed from initial segmentColors (if present)
486
- if (this.allKnownIds.size === 0) {
487
- const init = this.props.viewerState?.layers?.[0]?.segmentColors || {};
488
- for (const id of Object.keys(init)) this.allKnownIds.add(id);
489
- }
490
-
491
- // Build a full color table: selected keep their hex, others grey
492
- const fullSegmentColors = {};
493
- for (const id of this.allKnownIds) {
494
- fullSegmentColors[id] = selected[id] || GREY_HEX;
495
- }
496
- // Patch layers with the new segmentColors (pose untouched)
481
+ // Build full color table per layer
497
482
  const baseLayers = (this.props.viewerState?.layers)
498
483
  ?? (this.viewer.state.toJSON().layers || []);
499
484
 
500
- const newLayers = baseLayers.map((layer, idx) => {
501
- // if only one layer, take that or check layer.type === 'segmentation'.
502
- if (idx === 0 || layer?.type === 'segmentation') {
485
+ const newLayers = baseLayers.map((layer) => {
486
+ // Match layerScope by checking if the NG layer name contains the scope key.
487
+ // NG layer names are of the form:
488
+ // "obsSegmentations-init_A_obsSegmentations_0-init_A_obsSegmentations_0"
489
+ const layerScope = Object.keys(cellColorMappingByLayer).find(scope => layer.name?.includes(scope));
490
+
491
+ const selected = { ...(cellColorMappingByLayer[layerScope] || {}) };
492
+
493
+ // Track all known IDs for this layer scope
494
+ if (!this.allKnownIdsByLayer) this.allKnownIdsByLayer = {};
495
+ if (!this.allKnownIdsByLayer[layerScope]) {
496
+ this.allKnownIdsByLayer[layerScope] = new Set();
497
+ }
498
+ for (const id of Object.keys(selected)) {
499
+ this.allKnownIdsByLayer[layerScope].add(id);
500
+ }
501
+
502
+ // Build a full color table: selected keep their hex, others grey
503
+ const fullSegmentColors = {};
504
+ for (const id of this.allKnownIdsByLayer[layerScope] || []) {
505
+ fullSegmentColors[id] = selected[id] || GREY_HEX;
506
+ }
507
+
508
+ if (layer.type === 'segmentation') {
503
509
  return { ...layer, segmentColors: fullSegmentColors };
504
510
  }
505
511
  return layer;
@@ -624,7 +630,7 @@ export default class Neuroglancer extends React.Component {
624
630
  }
625
631
 
626
632
  componentDidUpdate(prevProps, prevState) {
627
- const { viewerState, cellColorMapping } = this.props;
633
+ const { viewerState, cellColorMapping: cellColorMappingByLayer } = this.props;
628
634
  // The restoreState() call clears the 'selected' (hovered on) segment, which is needed
629
635
  // by Neuroglancer's code to toggle segment visibilty on a mouse click. To free the user
630
636
  // from having to move the mouse before clicking, save the selected segment and restore
@@ -702,8 +708,8 @@ export default class Neuroglancer extends React.Component {
702
708
  this.withoutEmitting(() => {
703
709
  const layers = Array.isArray(viewerState.layers) ? viewerState.layers : [];
704
710
  this.viewer.state.restoreState({ layers });
705
- if (cellColorMapping && Object.keys(cellColorMapping).length) {
706
- this.applyColorsAndVisibility(cellColorMapping);
711
+ if (cellColorMappingByLayer && Object.keys(cellColorMappingByLayer).length) {
712
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
707
713
  }
708
714
  });
709
715
  }
@@ -712,12 +718,13 @@ export default class Neuroglancer extends React.Component {
712
718
  // this was to avid NG randomly assigning colors to the segments by resetting them
713
719
  const prevSize = prevProps.cellColorMapping
714
720
  ? Object.keys(prevProps.cellColorMapping).length : 0;
715
- const currSize = cellColorMapping ? Object.keys(cellColorMapping).length : 0;
716
- const mappingRefChanged = prevProps.cellColorMapping !== cellColorMapping;
721
+ const currSize = cellColorMappingByLayer
722
+ ? Object.keys(cellColorMappingByLayer).length : 0;
723
+ const mappingRefChanged = prevProps.cellColorMapping !== this.props.cellColorMapping;
717
724
  if (!this.didLayersChange(prevVS, viewerState)
718
725
  && (mappingRefChanged || prevSize !== currSize)) {
719
726
  this.withoutEmitting(() => {
720
- this.applyColorsAndVisibility(cellColorMapping);
727
+ this.applyColorsAndVisibility(cellColorMappingByLayer);
721
728
  });
722
729
  }
723
730
 
@@ -100,6 +100,7 @@ export function customIsEqualForCellColors(prevDeps, nextDeps) {
100
100
  'obsColorEncoding',
101
101
  'obsSetSelection',
102
102
  'additionalObsSets',
103
+ 'spatialChannelColor',
103
104
  ])
104
105
  ) {
105
106
  forceUpdate = true;