@vitessce/scatterplot-embedding 4.0.4 → 4.0.5

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.
@@ -1,5 +1,5 @@
1
1
  import React, {
2
- useState, useEffect, useCallback, useMemo,
2
+ useState, useEffect, useCallback, useMemo, useDeferredValue,
3
3
  } from 'react';
4
4
  import { extent, quantileSorted } from 'd3-array';
5
5
  import { isEqual } from 'lodash-es';
@@ -28,8 +28,8 @@ import {
28
28
  useCoordinationScopes,
29
29
  } from '@vitessce/vit-s';
30
30
  import {
31
- setObsSelection, mergeObsSets, getCellSetPolygons, getCellColors,
32
- stratifyArrays,
31
+ setObsSelection, mergeObsSets, getCellSetPolygons, treeToColorIndicesArray,
32
+ colorIndicesFromCodes, getObsIndexMap, stratifyArrays,
33
33
  } from '@vitessce/sets-utils';
34
34
  import { pluralize as plur, commaNumber, aggregateFeatureArrays } from '@vitessce/utils';
35
35
  import {
@@ -187,7 +187,8 @@ export function EmbeddingScatterplotSubscriber(props) {
187
187
  );
188
188
  const cellsCount = obsEmbeddingIndex?.length || 0;
189
189
  const [
190
- { obsSets: cellSets, obsSetsMembership }, obsSetsStatus, obsSetsUrls, obsSetsError,
190
+ { obsSets: cellSets, obsSetsMembership, obsSetsColumns },
191
+ obsSetsStatus, obsSetsUrls, obsSetsError,
191
192
  ] = useObsSetsData(
192
193
  loaders, dataset, false,
193
194
  { setObsSetSelection: setCellSetSelection, setObsSetColor: setCellSetColor },
@@ -261,6 +262,7 @@ export function EmbeddingScatterplotSubscriber(props) {
261
262
  ]);
262
263
 
263
264
  const [dynamicCellRadius, setDynamicCellRadius] = useState(cellRadiusFixed);
265
+ const [isSelectionPending, setIsSelectionPending] = useState(false);
264
266
  const [dynamicCellOpacity, setDynamicCellOpacity] = useState(cellOpacityFixed);
265
267
 
266
268
  const [originalViewState, setOriginalViewState] = useState(null);
@@ -278,14 +280,38 @@ export function EmbeddingScatterplotSubscriber(props) {
278
280
  }, [additionalCellSets, cellSetColor, setCellColorEncoding,
279
281
  setAdditionalCellSets, setCellSetColor, setCellSetSelection]);
280
282
 
281
- const cellColors = useMemo(() => getCellColors({
282
- cellSets: mergedCellSets,
283
- cellSetSelection,
284
- cellSetColor,
285
- obsIndex: matrixObsIndex,
286
- theme,
287
- }), [mergedCellSets, theme,
288
- cellSetSelection, cellSetColor, matrixObsIndex]);
283
+ // A set selection or color change re-encodes every observation in the memos
284
+ // below, which at atlas scale takes longer than a frame. Deferring these values
285
+ // lets React commit the urgent update first — the sets manager checkbox that
286
+ // initiated the change paints immediately — and re-render this view afterwards.
287
+ // The urgent render sees the previous values, so the memos below keep their
288
+ // cached results and cost nothing in that first commit.
289
+ const deferredCellSetSelection = useDeferredValue(cellSetSelection);
290
+ const deferredCellSetColor = useDeferredValue(cellSetColor);
291
+ const deferredSampleSetSelection = useDeferredValue(sampleSetSelection);
292
+
293
+ // Positional rather than keyed by observation ID: at atlas scale an ID-keyed color
294
+ // Map costs one string hash lookup per point per render, plus a per-observation
295
+ // color array whenever the selected sets carry confidence scores.
296
+ const obsColorIndices = useMemo(() => (
297
+ // When the loader provides raw categorical codes over this same observation
298
+ // axis (checked by reference), the encoding is one typed-array pass with no
299
+ // tree walk. colorIndicesFromCodes returns null for selections it cannot
300
+ // resolve (e.g. user-defined lasso selections), falling back to the tree.
301
+ (obsSetsColumns && obsSetsColumns.obsIndex === obsEmbeddingIndex
302
+ && colorIndicesFromCodes({
303
+ columns: obsSetsColumns.columns,
304
+ obsIndex: obsEmbeddingIndex,
305
+ selectedNamePaths: deferredCellSetSelection,
306
+ cellSetColor: deferredCellSetColor,
307
+ theme,
308
+ }))
309
+ || treeToColorIndicesArray(
310
+ mergedCellSets, deferredCellSetSelection, deferredCellSetColor,
311
+ obsEmbeddingIndex, theme,
312
+ )
313
+ ), [mergedCellSets, deferredCellSetSelection, deferredCellSetColor,
314
+ obsEmbeddingIndex, theme, obsSetsColumns]);
289
315
 
290
316
  // cellSetPolygonCache is an array of tuples like [(key0, val0), (key1, val1), ...],
291
317
  // where the keys are cellSetSelection arrays.
@@ -294,28 +320,29 @@ export function EmbeddingScatterplotSubscriber(props) {
294
320
  const cacheGet = (cache, key) => cache.find(el => isEqual(el[0], key))?.[1];
295
321
  const cellSetPolygons = useMemo(() => {
296
322
  if ((cellSetLabelsVisible || cellSetPolygonsVisible)
297
- && !cacheHas(cellSetPolygonCache, cellSetSelection)
323
+ && !cacheHas(cellSetPolygonCache, deferredCellSetSelection)
298
324
  && mergedCellSets?.tree?.length
299
325
  && obsEmbedding
300
326
  && obsEmbeddingIndex
301
- && cellSetColor?.length) {
327
+ && deferredCellSetColor?.length) {
302
328
  const newCellSetPolygons = getCellSetPolygons({
303
329
  obsIndex: obsEmbeddingIndex,
304
330
  obsEmbedding,
305
331
  cellSets: mergedCellSets,
306
- cellSetSelection,
307
- cellSetColor,
332
+ cellSetSelection: deferredCellSetSelection,
333
+ cellSetColor: deferredCellSetColor,
308
334
  theme,
309
335
  });
310
- setCellSetPolygonCache(cache => [...cache, [cellSetSelection, newCellSetPolygons]]);
336
+ setCellSetPolygonCache(
337
+ cache => [...cache, [deferredCellSetSelection, newCellSetPolygons]],
338
+ );
311
339
  return newCellSetPolygons;
312
340
  }
313
- return cacheGet(cellSetPolygonCache, cellSetSelection) || [];
341
+ return cacheGet(cellSetPolygonCache, deferredCellSetSelection) || [];
314
342
  }, [cellSetPolygonsVisible, cellSetPolygonCache, cellSetLabelsVisible, theme,
315
- obsEmbeddingIndex, obsEmbedding, mergedCellSets, cellSetSelection, cellSetColor]);
316
-
343
+ obsEmbeddingIndex, obsEmbedding, mergedCellSets, deferredCellSetSelection,
344
+ deferredCellSetColor]);
317
345
 
318
- const cellSelection = useMemo(() => Array.from(cellColors.keys()), [cellColors]);
319
346
 
320
347
  const [xRange, yRange, xExtent, yExtent, numCells] = useMemo(() => {
321
348
  if (obsEmbedding && obsEmbedding.data && obsEmbedding.shape) {
@@ -373,10 +400,13 @@ export function EmbeddingScatterplotSubscriber(props) {
373
400
  observationsLabel, obsLabelsTypes, obsLabelsData, obsSetsMembership,
374
401
  );
375
402
 
376
- const cellSelectionSet = useMemo(() => new Set(cellSelection), [cellSelection]);
403
+ // With no set selection every observation counts as selected, matching the
404
+ // behavior of the ID-keyed color map this replaced. Uses the deferred selection
405
+ // so that it stays consistent with obsColorIndices within each commit.
406
+ const allCellsSelected = !(deferredCellSetSelection && mergedCellSets);
377
407
  const getCellIsSelected = useCallback((object, { index }) => (
378
- (cellSelectionSet || new Set([])).has(obsEmbeddingIndex[index]) ? 1.0 : 0.0
379
- ), [cellSelectionSet, obsEmbeddingIndex]);
408
+ (allCellsSelected || obsColorIndices.colorIndices[index] !== 0) ? 1.0 : 0.0
409
+ ), [allCellsSelected, obsColorIndices]);
380
410
 
381
411
  const cellRadius = (cellRadiusMode === 'manual' ? cellRadiusFixed : dynamicCellRadius);
382
412
  const cellOpacity = (cellOpacityMode === 'manual' ? cellOpacityFixed : dynamicCellOpacity);
@@ -468,8 +498,11 @@ export function EmbeddingScatterplotSubscriber(props) {
468
498
  // useExpressionValueGetter hook and getter function.
469
499
  const [alignedEmbeddingIndex, alignedEmbeddingData] = useMemo(() => {
470
500
  // Sort the embedding data according to the matrix obsIndex.
471
- if (obsEmbedding?.data && obsEmbeddingIndex && matrixObsIndex) {
472
- const matrixIndexMap = new Map(matrixObsIndex.map((key, i) => ([key, i])));
501
+ if (obsEmbedding?.data && obsEmbeddingIndex && matrixObsIndex
502
+ && obsEmbeddingIndex !== matrixObsIndex) {
503
+ // (The same index array, as when both come from one data source,
504
+ // is already aligned and skips this copy.)
505
+ const matrixIndexMap = getObsIndexMap(matrixObsIndex);
473
506
  const toMatrixIndex = obsEmbeddingIndex.map(key => matrixIndexMap.get(key));
474
507
 
475
508
  const newEmbeddingIndex = new Array(obsEmbeddingIndex.length);
@@ -489,42 +522,32 @@ export function EmbeddingScatterplotSubscriber(props) {
489
522
  return [obsEmbeddingIndex, obsEmbedding];
490
523
  }, [matrixObsIndex, obsEmbeddingIndex, obsEmbedding]);
491
524
 
492
- const sampleIdToObsIdsMap = useMemo(() => {
493
- // sampleEdges maps obsId -> sampleId.
494
- // However when we stratify we want to map sampleId -> [obsId1, obsId2, ...].
495
- // Here we create this reverse mapping.
496
- if (sampleEdges) {
497
- const result = new Map();
498
- Array.from(sampleEdges.entries()).forEach(([obsId, sampleId]) => {
499
- if (!result.has(sampleId)) {
500
- result.set(sampleId, [obsId]);
501
- } else {
502
- result.get(sampleId).push(obsId);
503
- }
504
- });
505
- return result;
506
- }
507
- return null;
508
- }, [sampleEdges]);
509
-
510
525
  // Stratify multiple arrays: per-cellSet and per-sampleSet.
526
+ // The strata feed only the contour layers and, when the points are hidden, the
527
+ // observation count shown in the title — so with contours off and points on,
528
+ // this per-observation pass is skipped entirely rather than recomputed on
529
+ // every selection change.
530
+ const isStratifiedDataNeeded = embeddingContoursVisible || !embeddingPointsVisible;
511
531
  const [stratifiedData, stratifiedDataCount] = useMemo(() => {
512
- if (alignedEmbeddingData?.data) {
532
+ if (isStratifiedDataNeeded && alignedEmbeddingData?.data) {
513
533
  const [result, cellCountResult] = stratifyArrays(
514
- sampleEdges, sampleIdToObsIdsMap,
515
- sampleSets, sampleSetSelection,
516
- alignedEmbeddingIndex, mergedCellSets, cellSetSelection, {
534
+ sampleEdges,
535
+ sampleSets, deferredSampleSetSelection,
536
+ alignedEmbeddingIndex, mergedCellSets, deferredCellSetSelection, {
517
537
  obsEmbeddingX: alignedEmbeddingData.data[0],
518
538
  obsEmbeddingY: alignedEmbeddingData.data[1],
519
539
  ...(uint8ExpressionData?.[0] ? { featureValue: uint8ExpressionData } : {}),
520
540
  }, featureAggregationStrategyToUse,
541
+ // Raw codes let the strata come from typed arrays when the indices match.
542
+ { obsSetsColumns },
521
543
  );
522
544
  return [result, cellCountResult];
523
545
  }
524
546
  return [null, null];
525
- }, [alignedEmbeddingIndex, alignedEmbeddingData, uint8ExpressionData,
526
- sampleEdges, sampleIdToObsIdsMap, sampleSets, sampleSetSelection,
527
- cellSetSelection, mergedCellSets, featureAggregationStrategyToUse,
547
+ }, [isStratifiedDataNeeded, alignedEmbeddingIndex, alignedEmbeddingData,
548
+ uint8ExpressionData, sampleEdges, sampleSets, deferredSampleSetSelection,
549
+ deferredCellSetSelection, mergedCellSets, featureAggregationStrategyToUse,
550
+ obsSetsColumns,
528
551
  ]);
529
552
 
530
553
  const setViewState = ({ zoom: newZoom, target }) => {
@@ -548,7 +571,7 @@ export function EmbeddingScatterplotSubscriber(props) {
548
571
  removeGridComponent={removeGridComponent}
549
572
  urls={urls}
550
573
  theme={theme}
551
- isReady={isReady}
574
+ isReady={isReady && !isSelectionPending}
552
575
  helpText={helpText}
553
576
  errors={errors}
554
577
  options={(
@@ -595,6 +618,7 @@ export function EmbeddingScatterplotSubscriber(props) {
595
618
  <Scatterplot
596
619
  ref={deckRef}
597
620
  uuid={uuid}
621
+ onSelectionBusy={setIsSelectionPending}
598
622
  theme={theme}
599
623
  viewState={{ zoom, target: [targetX, targetY, targetZ] }}
600
624
  setViewState={setViewState}
@@ -602,9 +626,8 @@ export function EmbeddingScatterplotSubscriber(props) {
602
626
  obsEmbeddingIndex={obsEmbeddingIndex}
603
627
  obsEmbedding={obsEmbedding}
604
628
  cellFilter={cellFilter}
605
- cellSelection={cellSelection}
606
629
  cellHighlight={cellHighlight}
607
- cellColors={cellColors}
630
+ obsColorIndices={obsColorIndices}
608
631
  cellSetPolygons={cellSetPolygons}
609
632
  cellSetLabelSize={cellSetLabelSize}
610
633
  cellSetLabelsVisible={cellSetLabelsVisible}
@@ -624,12 +647,12 @@ export function EmbeddingScatterplotSubscriber(props) {
624
647
  getExpressionValue={getExpressionValue}
625
648
  getCellIsSelected={getCellIsSelected}
626
649
 
627
- obsSetSelection={cellSetSelection}
628
- sampleSetSelection={sampleSetSelection}
650
+ obsSetSelection={deferredCellSetSelection}
651
+ sampleSetSelection={deferredSampleSetSelection}
629
652
  // InternMap data structures where keys are
630
653
  // obsSet -> sampleSet -> arrayKey -> [].
631
654
  stratifiedData={stratifiedData}
632
- obsSetColor={cellSetColor}
655
+ obsSetColor={deferredCellSetColor}
633
656
  sampleSetColor={sampleSetColor}
634
657
  contourThresholds={contourThresholds}
635
658
  contourColorEncoding={contourColorEncoding}