@vitessce/scatterplot-embedding 4.0.10 → 4.1.1

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.
@@ -15,6 +15,7 @@ import {
15
15
  useObsSetsData,
16
16
  useAnnotationStoryData,
17
17
  useAnnotationFrameCoordination,
18
+ useAnnotationEditingForView,
18
19
  useFeatureSelection,
19
20
  useObsFeatureMatrixIndices,
20
21
  useFeatureLabelsData,
@@ -33,14 +34,19 @@ import {
33
34
  setObsSelection, mergeObsSets, getCellSetPolygons, treeToColorIndicesArray,
34
35
  colorIndicesFromCodes, getObsIndexMap, stratifyArrays,
35
36
  } from '@vitessce/sets-utils';
36
- import { pluralize as plur, commaNumber, aggregateFeatureArrays } from '@vitessce/utils';
37
+ import {
38
+ pluralize as plur, commaNumber, aggregateFeatureArrays, getAnnotationFrameCoordinationValues,
39
+ } from '@vitessce/utils';
37
40
  import {
38
41
  Scatterplot, ScatterplotTooltipSubscriber, ScatterplotOptions,
39
42
  getPointSizeDevicePixels,
40
43
  getPointOpacity,
44
+ useAnnotationFrameTransition,
41
45
  } from '@vitessce/scatterplot';
42
46
  import { Legend } from '@vitessce/legend';
43
- import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
47
+ import {
48
+ ViewType, CoordinationType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping,
49
+ } from '@vitessce/constants-internal';
44
50
  import { DEFAULT_CONTOUR_PERCENTILES } from './constants.js';
45
51
 
46
52
  const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
@@ -48,8 +54,11 @@ const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
48
54
  /**
49
55
  * A subscriber component for the scatterplot.
50
56
  * @param {object} props
51
- * @param {number} props.uuid The unique identifier for this component, also used as its
52
- * view uid for looking up its coordination mapping via `useViewMapping`.
57
+ * @param {number} props.uuid The unique identifier for this component.
58
+ * @param {string} props.viewUid The uid of the corresponding view in the view config layout,
59
+ * used for looking up its coordination mapping via `useViewMapping`.
60
+ * Optional, defaults to `uuid`. Differs from `uuid` when rendered by a parent
61
+ * (e.g., the dual scatterplot) that renders multiple child scatterplots for one view.
53
62
  * @param {string} props.theme The current theme name.
54
63
  * @param {function} props.removeGridComponent The callback function to pass to TitleInfo,
55
64
  * to call when the component has been removed from the grid.
@@ -60,6 +69,7 @@ const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
60
69
  export function EmbeddingScatterplotSubscriber(props) {
61
70
  const {
62
71
  uuid,
72
+ viewUid: viewUidFromProps,
63
73
  closeButtonVisible,
64
74
  downloadButtonVisible,
65
75
  removeGridComponent,
@@ -73,8 +83,11 @@ export function EmbeddingScatterplotSubscriber(props) {
73
83
  sampleSetSelection: sampleSetSelectionFromProps,
74
84
  // Circle scale factor:
75
85
  circleScaleFactor = 0.8,
86
+ // Passed down from the ancestor <Vitessce/> or <VitS/> component.
87
+ areAnnotationsEditable,
76
88
  } = props;
77
89
 
90
+ const viewUid = viewUidFromProps ?? uuid;
78
91
  const loaders = useLoaders();
79
92
  // useViewMapping looks up this view's coordination mapping directly from the
80
93
  // view config (by uuid), after accounting for meta-coordination, so there is
@@ -84,14 +97,14 @@ export function EmbeddingScatterplotSubscriber(props) {
84
97
  const [
85
98
  // eslint-disable-next-line no-unused-vars
86
99
  coordinationScopes, _coordinationScopesBy, coordinationValues,
87
- ] = useViewMapping(uuid);
100
+ ] = useViewMapping(viewUid);
88
101
  const setComponentHover = useSetComponentHover();
89
102
  const setComponentViewInfo = useSetComponentViewInfo(uuid);
90
103
 
91
104
  // Merge the coordination values that the current annotation frame defines for this view
92
105
  // into the coordination space, whenever the frame changes. The story itself is read-only.
93
106
  // TODO: handle the rendering of alternative views/layouts (in vit-s?)
94
- useAnnotationFrameCoordination(uuid, coordinationScopes, coordinationValues);
107
+ useAnnotationFrameCoordination(viewUid, coordinationScopes, coordinationValues);
95
108
 
96
109
  // Get "props" from the coordination space.
97
110
  const [{
@@ -122,6 +135,7 @@ export function EmbeddingScatterplotSubscriber(props) {
122
135
  featureValueColormap: geneExpressionColormap,
123
136
  featureValueColormapRange: geneExpressionColormapRange,
124
137
  tooltipsVisible,
138
+ tooltipObsSetsMode,
125
139
  sampleSetSelection: sampleSetSelectionFromCoordination,
126
140
  sampleSetColor,
127
141
  embeddingPointsVisible,
@@ -137,6 +151,7 @@ export function EmbeddingScatterplotSubscriber(props) {
137
151
  annotationSemanticZoom,
138
152
  annotationTransitionDuration,
139
153
  annotationEditable,
154
+ annotationFrameIndex,
140
155
  }, {
141
156
  setEmbeddingZoom: setZoom,
142
157
  setEmbeddingTargetX: setTargetX,
@@ -167,7 +182,14 @@ export function EmbeddingScatterplotSubscriber(props) {
167
182
  setAnnotationStory,
168
183
  }] = useCoordination(
169
184
  COMPONENT_COORDINATION_TYPES[ViewType.SCATTERPLOT], coordinationScopes,
170
- coordinationValues, uuid,
185
+ coordinationValues, viewUid,
186
+ );
187
+
188
+ // Props for drawing annotation shapes in this view (while authoring the story
189
+ // via the annotation controller), and for highlighting the selected shape.
190
+ const annotationEditingProps = useAnnotationEditingForView(
191
+ viewUid,
192
+ Boolean(areAnnotationsEditable && annotationEditable && typeof annotationFrameIndex === 'number'),
171
193
  );
172
194
 
173
195
  const {
@@ -329,6 +351,10 @@ export function EmbeddingScatterplotSubscriber(props) {
329
351
  // cached results and cost nothing in that first commit.
330
352
  const deferredCellSetSelection = useDeferredValue(cellSetSelection);
331
353
  const deferredCellSetColor = useDeferredValue(cellSetColor);
354
+ // A lasso selection made while coloring by feature values also switches the
355
+ // encoding to set selection. Defer it with the selection and colors, so the
356
+ // points do not first show the previous selection's colors for every cell.
357
+ const deferredCellColorEncoding = useDeferredValue(cellColorEncoding);
332
358
  const deferredSampleSetSelection = useDeferredValue(sampleSetSelection);
333
359
 
334
360
  // Positional rather than keyed by observation ID: at atlas scale an ID-keyed color
@@ -439,6 +465,7 @@ export function EmbeddingScatterplotSubscriber(props) {
439
465
 
440
466
  const getObsInfo = useGetObsInfo(
441
467
  observationsLabel, obsLabelsTypes, obsLabelsData, obsSetsMembership,
468
+ cellSetSelection, tooltipObsSetsMode,
442
469
  );
443
470
 
444
471
  // With no set selection every observation counts as selected, matching the
@@ -591,6 +618,20 @@ export function EmbeddingScatterplotSubscriber(props) {
591
618
  obsSetsColumns,
592
619
  ]);
593
620
 
621
+ // Animate the 2D zoom/target when they change due to the current annotation frame.
622
+ const annotationFrameCoordinationValues = getAnnotationFrameCoordinationValues(
623
+ annotationStory, annotationFrameIndex, viewUid,
624
+ );
625
+ const annotationTransitionProps = useAnnotationFrameTransition(
626
+ { zoom, targetX, targetY },
627
+ {
628
+ zoom: annotationFrameCoordinationValues?.[CoordinationType.EMBEDDING_ZOOM],
629
+ targetX: annotationFrameCoordinationValues?.[CoordinationType.EMBEDDING_TARGET_X],
630
+ targetY: annotationFrameCoordinationValues?.[CoordinationType.EMBEDDING_TARGET_Y],
631
+ },
632
+ annotationTransitionDuration,
633
+ );
634
+
594
635
  const setViewState = ({ zoom: newZoom, target }) => {
595
636
  setZoom(newZoom);
596
637
  setTargetX(target[0]);
@@ -661,7 +702,7 @@ export function EmbeddingScatterplotSubscriber(props) {
661
702
  uuid={uuid}
662
703
  onSelectionBusy={setIsSelectionPending}
663
704
  theme={theme}
664
- viewState={{ zoom, target: [targetX, targetY, targetZ] }}
705
+ viewState={{ zoom, target: [targetX, targetY, targetZ], ...annotationTransitionProps }}
665
706
  setViewState={setViewState}
666
707
  originalViewState={originalViewState}
667
708
  obsEmbeddingIndex={obsEmbeddingIndex}
@@ -678,7 +719,7 @@ export function EmbeddingScatterplotSubscriber(props) {
678
719
  setCellHighlight={setCellHighlight}
679
720
  cellRadius={cellRadius}
680
721
  cellOpacity={cellOpacity}
681
- cellColorEncoding={cellColorEncoding}
722
+ cellColorEncoding={deferredCellColorEncoding}
682
723
  geneExpressionColormap={geneExpressionColormap}
683
724
  geneExpressionColormapRange={geneExpressionColormapRange}
684
725
  setComponentHover={() => {
@@ -711,6 +752,7 @@ export function EmbeddingScatterplotSubscriber(props) {
711
752
  annotationSemanticZoom={annotationSemanticZoom}
712
753
  annotationTransitionDuration={annotationTransitionDuration}
713
754
  annotationEditable={annotationEditable}
755
+ {...annotationEditingProps}
714
756
  />
715
757
  {tooltipsVisible && width && height ? (
716
758
  <ScatterplotTooltipSubscriber
@@ -728,7 +770,7 @@ export function EmbeddingScatterplotSubscriber(props) {
728
770
  theme={theme}
729
771
  featureType={featureType}
730
772
  featureValueType={featureValueType}
731
- obsColorEncoding={cellColorEncoding}
773
+ obsColorEncoding={deferredCellColorEncoding}
732
774
  featureSelection={geneSelection}
733
775
  featureLabelsMap={featureLabelsMap}
734
776
  featureValueColormap={geneExpressionColormap}
@@ -0,0 +1,105 @@
1
+ import React from 'react';
2
+ import { it, expect, vi } from 'vitest';
3
+ import { render, act } from '@testing-library/react';
4
+ import { Scatterplot } from '@vitessce/scatterplot';
5
+ import { EmbeddingScatterplotSubscriber } from './EmbeddingScatterplotSubscriber.js';
6
+
7
+ const state = vi.hoisted(() => ({
8
+ values: {}, setters: {}, index: [], embedding: null, sets: null,
9
+ }));
10
+ vi.mock('@vitessce/vit-s', () => ({
11
+ TitleInfo: ({ children }) => children,
12
+ useReady: () => true,
13
+ useUrls: () => [],
14
+ useDeckCanvasSize: () => [0, 0, null],
15
+ useUint8FeatureSelection: () => ({}),
16
+ useExpressionValueGetter: () => () => 0,
17
+ useGetObsInfo: () => () => ({}),
18
+ useObsEmbeddingData: () => [{ obsIndex: state.index, obsEmbedding: state.embedding }, 'success', [], null],
19
+ useObsSetsData: () => [{ obsSets: state.sets }, 'success', [], null],
20
+ useAnnotationStoryData: () => [{}, 'success', [], null],
21
+ useAnnotationFrameCoordination: () => {},
22
+ useAnnotationEditingForView: () => ({}),
23
+ useFeatureSelection: () => [null, [], 'success', []],
24
+ useObsFeatureMatrixIndices: () => [{ obsIndex: state.index }, 'success', [], null],
25
+ useFeatureLabelsData: () => [{}, 'success', [], null],
26
+ useMultiObsLabels: () => [[], [], [], [], []],
27
+ useSampleSetsData: () => [{}, 'success', [], null],
28
+ useSampleEdgesData: () => [{}, 'success', [], null],
29
+ useCoordination: () => [state.values, state.setters],
30
+ useLoaders: () => ({}),
31
+ useSetComponentHover: () => () => {},
32
+ useSetComponentViewInfo: () => () => {},
33
+ useInitialCoordination: () => ({ embeddingZoom: 0, embeddingTargetX: 0, embeddingTargetY: 0 }),
34
+ useExpandedFeatureLabelsMap: () => [null, 'success'],
35
+ useViewMapping: () => [{}, {}, {}],
36
+ }));
37
+ vi.mock('@vitessce/scatterplot', () => ({
38
+ Scatterplot: vi.fn(() => null),
39
+ ScatterplotTooltipSubscriber: () => null,
40
+ ScatterplotOptions: () => null,
41
+ getPointSizeDevicePixels: () => 1,
42
+ getPointOpacity: () => 1,
43
+ useAnnotationFrameTransition: () => ({}),
44
+ }));
45
+ vi.mock('@vitessce/legend', () => ({ Legend: () => null }));
46
+
47
+ it('switches from feature value coloring to a lasso selection without showing the previous selection', () => {
48
+ state.index = Array.from({ length: 1000 }, (_, i) => `cell-${i}`);
49
+ state.embedding = {
50
+ shape: [2, state.index.length],
51
+ data: [
52
+ Float32Array.from(state.index, (_, i) => i % 32),
53
+ Float32Array.from(state.index, (_, i) => Math.floor(i / 32)),
54
+ ],
55
+ };
56
+ state.sets = {
57
+ version: '0.1.3',
58
+ tree: [{
59
+ name: 'Types',
60
+ children: [{ name: 'All', set: state.index.map(id => [id, null]) }],
61
+ }],
62
+ };
63
+ // Coloring by feature values, with a previous set selection still stored.
64
+ state.values = {
65
+ dataset: 'A',
66
+ obsType: 'cell',
67
+ embeddingType: 'UMAP',
68
+ embeddingZoom: 0,
69
+ embeddingTargetX: 0,
70
+ embeddingTargetY: 0,
71
+ embeddingPointsVisible: true,
72
+ embeddingContoursVisible: false,
73
+ obsSetSelection: [['Types', 'All']],
74
+ additionalObsSets: null,
75
+ obsSetColor: [{ path: ['Types', 'All'], color: [0, 0, 255] }],
76
+ obsColorEncoding: 'geneSelection',
77
+ };
78
+ state.setters = Object.fromEntries([
79
+ 'AdditionalObsSets', 'ObsSetSelection', 'ObsSetColor', 'ObsColorEncoding',
80
+ ].map(name => [`set${name}`, (value) => {
81
+ state.values = { ...state.values, [name[0].toLowerCase() + name.slice(1)]: value };
82
+ }]));
83
+ const props = { uuid: 'umap', theme: 'dark' };
84
+ const { rerender } = render(<EmbeddingScatterplotSubscriber {...props} />);
85
+ const initialColorIndices = Scatterplot.mock.lastCall[0].obsColorIndices;
86
+ const currentSelect = Scatterplot.mock.lastCall[0].setCellSelection;
87
+ Scatterplot.mockClear();
88
+
89
+ act(() => {
90
+ // The same callback the selection layer calls after its hit test.
91
+ currentSelect(state.index.slice(0, 600));
92
+ rerender(<EmbeddingScatterplotSubscriber {...props} />);
93
+ });
94
+
95
+ const lastPlot = Scatterplot.mock.lastCall[0];
96
+ expect(lastPlot.cellColorEncoding).toEqual('cellSetSelection');
97
+ expect(lastPlot.obsColorIndices).not.toBe(initialColorIndices);
98
+ // Set selection coloring is only shown together with the new colors. Showing
99
+ // it with the previous colors would color every cell by the previous selection.
100
+ Scatterplot.mock.calls.forEach(([plot]) => {
101
+ if (plot.cellColorEncoding === 'cellSetSelection') {
102
+ expect(plot.obsSetSelection).toEqual(state.values.obsSetSelection);
103
+ }
104
+ });
105
+ });