@vitessce/scatterplot-embedding 3.5.9 → 3.5.11

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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D, E } from "./index-5909753b.js";
1
+ import { D, E } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-5909753b.js";
1
+ import { B as BaseDecoder } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-68f84e2a.js";
2
- import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-5909753b.js";
2
+ import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-ab73546f.js";
3
3
  import "react";
4
4
  import "@vitessce/vit-s";
5
5
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-5909753b.js";
1
+ import { B as BaseDecoder } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-5909753b.js";
1
+ import { B as BaseDecoder } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-5909753b.js";
1
+ import { B as BaseDecoder } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-5909753b.js";
1
+ import { B as BaseDecoder } from "./index-ab73546f.js";
2
2
  import "react";
3
3
  import "@vitessce/vit-s";
4
4
  import "react-dom";
@@ -1 +1 @@
1
- {"version":3,"file":"DualEmbeddingScatterplotSubscriber.d.ts","sourceRoot":"","sources":["../src/DualEmbeddingScatterplotSubscriber.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;GAeG;AACH,0DAVG;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAEU,mBAAmB;IAErB,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;CAEhB,eA4CA"}
1
+ {"version":3,"file":"DualEmbeddingScatterplotSubscriber.d.ts","sourceRoot":"","sources":["../src/DualEmbeddingScatterplotSubscriber.js"],"names":[],"mappings":"AAQA;;;;;;;;;;;;;;;GAeG;AACH,0DAVG;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAEU,mBAAmB;IAErB,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;CAEhB,eAmDA"}
@@ -23,11 +23,14 @@ export function DualEmbeddingScatterplotSubscriber(props) {
23
23
  const { uuid, coordinationScopes, } = props;
24
24
  // Get "props" from the coordination space.
25
25
  const [{ embeddingType, sampleSetSelection, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.DUAL_SCATTERPLOT], coordinationScopes);
26
+ const isCaseCtrl = Array.isArray(sampleSetSelection) && sampleSetSelection.length === 2;
26
27
  const caseSampleSetSelection = useMemo(() => (sampleSetSelection?.[0]
27
28
  ? [sampleSetSelection[0]]
28
29
  : null), [sampleSetSelection]);
29
30
  const ctrlSampleSetSelection = useMemo(() => (sampleSetSelection?.[1]
30
31
  ? [sampleSetSelection[1]]
31
32
  : null), [sampleSetSelection]);
32
- return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }, children: [_jsx("div", { style: { width: '50%', display: 'flex', flexDirection: 'column' }, children: _jsx(EmbeddingScatterplotSubscriber, { ...props, uuid: `${uuid}-case`, title: `Scatterplot (${embeddingType}), ${caseSampleSetSelection?.[0]?.at(-1)}`, sampleSetSelection: caseSampleSetSelection }) }), _jsx("div", { style: { width: '50%', display: 'flex', flexDirection: 'column' }, children: _jsx(EmbeddingScatterplotSubscriber, { ...props, uuid: `${uuid}-ctrl`, title: `Scatterplot (${embeddingType}), ${ctrlSampleSetSelection?.[0]?.at(-1)}`, sampleSetSelection: ctrlSampleSetSelection }) })] }));
33
+ return (_jsxs("div", { style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }, children: [_jsx("div", { style: { width: isCaseCtrl ? '50%' : '100%', display: 'flex', flexDirection: 'column' }, children: _jsx(EmbeddingScatterplotSubscriber, { ...props, uuid: `${uuid}-case`, title: (isCaseCtrl
34
+ ? `Scatterplot (${embeddingType}), ${caseSampleSetSelection?.[0]?.at(-1)}`
35
+ : null), sampleSetSelection: caseSampleSetSelection }) }), isCaseCtrl ? (_jsx("div", { style: { width: '50%', display: 'flex', flexDirection: 'column' }, children: _jsx(EmbeddingScatterplotSubscriber, { ...props, uuid: `${uuid}-ctrl`, title: `Scatterplot (${embeddingType}), ${ctrlSampleSetSelection?.[0]?.at(-1)}`, sampleSetSelection: ctrlSampleSetSelection }) })) : null] }));
33
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"EmbeddingScatterplotSubscriber.d.ts","sourceRoot":"","sources":["../src/EmbeddingScatterplotSubscriber.js"],"names":[],"mappings":"AA0CA;;;;;;;;;;;;GAYG;AACH,sDAVG;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAEU,mBAAmB;IAErB,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;CAEhB,eAwhBA"}
1
+ {"version":3,"file":"EmbeddingScatterplotSubscriber.d.ts","sourceRoot":"","sources":["../src/EmbeddingScatterplotSubscriber.js"],"names":[],"mappings":"AA4CA;;;;;;;;;;;;GAYG;AACH,sDAVG;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAEU,mBAAmB;IAErB,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;CAEhB,eAkkBA"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useState, useEffect, useCallback, useMemo, } from 'react';
3
3
  import { extent, quantileSorted } from 'd3-array';
4
4
  import { isEqual } from 'lodash-es';
5
+ import { circle } from '@turf/circle';
5
6
  import { TitleInfo, useReady, useUrls, useDeckCanvasSize, useUint8FeatureSelection, useExpressionValueGetter, useGetObsInfo, useObsEmbeddingData, useObsSetsData, useFeatureSelection, useObsFeatureMatrixIndices, useFeatureLabelsData, useMultiObsLabels, useSampleSetsData, useSampleEdgesData, useCoordination, useLoaders, useSetComponentHover, useSetComponentViewInfo, useInitialCoordination, useExpandedFeatureLabelsMap, } from '@vitessce/vit-s';
6
7
  import { setObsSelection, mergeObsSets, getCellSetPolygons, getCellColors, stratifyArrays, } from '@vitessce/sets-utils';
7
8
  import { pluralize as plur, commaNumber } from '@vitessce/utils';
@@ -9,6 +10,7 @@ import { Scatterplot, ScatterplotTooltipSubscriber, ScatterplotOptions, getPoint
9
10
  import { Legend } from '@vitessce/legend';
10
11
  import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
11
12
  import { DEFAULT_CONTOUR_PERCENTILES } from './constants.js';
13
+ const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
12
14
  /**
13
15
  * A subscriber component for the scatterplot.
14
16
  * @param {object} props
@@ -32,11 +34,13 @@ export function EmbeddingScatterplotSubscriber(props) {
32
34
  const setComponentHover = useSetComponentHover();
33
35
  const setComponentViewInfo = useSetComponentViewInfo(uuid);
34
36
  // Get "props" from the coordination space.
35
- const [{ dataset, obsType, featureType, featureValueType, sampleType, embeddingZoom: zoom, embeddingTargetX: targetX, embeddingTargetY: targetY, embeddingTargetZ: targetZ, embeddingType: mapping, obsFilter: cellFilter, obsHighlight: cellHighlight, featureSelection: geneSelection, obsSetSelection: cellSetSelection, obsSetColor: cellSetColor, obsColorEncoding: cellColorEncoding, additionalObsSets: additionalCellSets, embeddingObsSetPolygonsVisible: cellSetPolygonsVisible, embeddingObsSetLabelsVisible: cellSetLabelsVisible, embeddingObsSetLabelSize: cellSetLabelSize, embeddingObsRadius: cellRadiusFixed, embeddingObsRadiusMode: cellRadiusMode, embeddingObsOpacity: cellOpacityFixed, embeddingObsOpacityMode: cellOpacityMode, featureValueColormap: geneExpressionColormap, featureValueColormapRange: geneExpressionColormapRange, tooltipsVisible, sampleSetSelection: sampleSetSelectionFromCoordination, sampleSetColor, embeddingPointsVisible, embeddingContoursVisible, embeddingContoursFilled, embeddingContourPercentiles: contourPercentiles, contourColorEncoding, contourColor, }, { setEmbeddingZoom: setZoom, setEmbeddingTargetX: setTargetX, setEmbeddingTargetY: setTargetY, setEmbeddingTargetZ: setTargetZ, setObsFilter: setCellFilter, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setAdditionalObsSets: setAdditionalCellSets, setEmbeddingObsSetPolygonsVisible: setCellSetPolygonsVisible, setEmbeddingObsSetLabelsVisible: setCellSetLabelsVisible, setEmbeddingObsSetLabelSize: setCellSetLabelSize, setEmbeddingObsRadius: setCellRadiusFixed, setEmbeddingObsRadiusMode: setCellRadiusMode, setEmbeddingObsOpacity: setCellOpacityFixed, setEmbeddingObsOpacityMode: setCellOpacityMode, setFeatureValueColormap: setGeneExpressionColormap, setFeatureValueColormapRange: setGeneExpressionColormapRange, setTooltipsVisible, setEmbeddingPointsVisible, setEmbeddingContoursVisible, setEmbeddingContoursFilled, setEmbeddingContourPercentiles: setContourPercentiles, setContourColorEncoding, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.SCATTERPLOT], coordinationScopes);
37
+ const [{ dataset, obsType, featureType, featureValueType, sampleType, embeddingZoom: zoom, embeddingTargetX: targetX, embeddingTargetY: targetY, embeddingTargetZ: targetZ, embeddingType: mapping, obsFilter: cellFilter, obsHighlight: cellHighlight, featureSelection: geneSelection, obsSetSelection: cellSetSelection, obsSetColor: cellSetColor, obsColorEncoding: cellColorEncoding, additionalObsSets: additionalCellSets, embeddingObsSetPolygonsVisible: cellSetPolygonsVisible, embeddingObsSetLabelsVisible: cellSetLabelsVisible, embeddingObsSetLabelSize: cellSetLabelSize, embeddingObsRadius: cellRadiusFixed, embeddingObsRadiusMode: cellRadiusMode, embeddingObsOpacity: cellOpacityFixed, embeddingObsOpacityMode: cellOpacityMode, featureValueColormap: geneExpressionColormap, featureValueColormapRange: geneExpressionColormapRange, tooltipsVisible, sampleSetSelection: sampleSetSelectionFromCoordination, sampleSetColor, embeddingPointsVisible, embeddingContoursVisible, embeddingContoursFilled, embeddingContourPercentiles: contourPercentiles, contourColorEncoding, contourColor, featureAggregationStrategy, }, { setEmbeddingZoom: setZoom, setEmbeddingTargetX: setTargetX, setEmbeddingTargetY: setTargetY, setEmbeddingTargetZ: setTargetZ, setObsFilter: setCellFilter, setObsSetSelection: setCellSetSelection, setObsHighlight: setCellHighlight, setObsSetColor: setCellSetColor, setObsColorEncoding: setCellColorEncoding, setAdditionalObsSets: setAdditionalCellSets, setEmbeddingObsSetPolygonsVisible: setCellSetPolygonsVisible, setEmbeddingObsSetLabelsVisible: setCellSetLabelsVisible, setEmbeddingObsSetLabelSize: setCellSetLabelSize, setEmbeddingObsRadius: setCellRadiusFixed, setEmbeddingObsRadiusMode: setCellRadiusMode, setEmbeddingObsOpacity: setCellOpacityFixed, setEmbeddingObsOpacityMode: setCellOpacityMode, setFeatureValueColormap: setGeneExpressionColormap, setFeatureValueColormapRange: setGeneExpressionColormapRange, setTooltipsVisible, setEmbeddingPointsVisible, setEmbeddingContoursVisible, setEmbeddingContoursFilled, setEmbeddingContourPercentiles: setContourPercentiles, setContourColorEncoding, setFeatureAggregationStrategy, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.SCATTERPLOT], coordinationScopes);
36
38
  const { embeddingZoom: initialZoom, embeddingTargetX: initialTargetX, embeddingTargetY: initialTargetY, } = useInitialCoordination(COMPONENT_COORDINATION_TYPES[ViewType.SCATTERPLOT], coordinationScopes);
37
39
  const observationsLabel = observationsLabelOverride || obsType;
38
40
  const sampleSetSelection = (sampleSetSelectionFromProps
39
41
  || sampleSetSelectionFromCoordination);
42
+ const featureAggregationStrategyToUse = featureAggregationStrategy
43
+ ?? DEFAULT_FEATURE_AGGREGATION_STRATEGY;
40
44
  const [width, height, deckRef] = useDeckCanvasSize();
41
45
  const title = titleOverride || `Scatterplot (${mapping})`;
42
46
  const [obsLabelsTypes, obsLabelsData] = useMultiObsLabels(coordinationScopes, obsType, loaders, dataset);
@@ -128,7 +132,7 @@ export function EmbeddingScatterplotSubscriber(props) {
128
132
  // compute the cell radius scale based on the
129
133
  // extents of the cell coordinates on the x/y axes.
130
134
  useEffect(() => {
131
- if (xRange && yRange) {
135
+ if (xRange && yRange && width && height) {
132
136
  const pointSizeDevicePixels = getPointSizeDevicePixels(window.devicePixelRatio, zoom, xRange, yRange, width, height);
133
137
  setDynamicCellRadius(pointSizeDevicePixels);
134
138
  const nextCellOpacityScale = getPointOpacity(zoom, xRange, yRange, width, height, numCells, averageFillDensity);
@@ -189,8 +193,34 @@ export function EmbeddingScatterplotSubscriber(props) {
189
193
  .map(t => Math.max(t, 1.0));
190
194
  return thresholds;
191
195
  }
192
- return null;
196
+ return [1, 10, 100];
193
197
  }, [contourPercentiles, sortedWeights]);
198
+ // Construct a circle polygon using Turf's circle function,
199
+ // which surrounds all points in the scatterplot,
200
+ // which we can use to position text labels along.
201
+ const circleInfo = useMemo(() => {
202
+ if (!originalViewState || !width || !height) {
203
+ return null;
204
+ }
205
+ const center = [
206
+ originalViewState.target[0],
207
+ originalViewState.target[1],
208
+ ];
209
+ const scaleFactor = (2 ** originalViewState.zoom);
210
+ if (!(typeof scaleFactor === 'number' && typeof center[0] === 'number' && typeof center[1] === 'number') || Number.isNaN(scaleFactor)) {
211
+ return null;
212
+ }
213
+ const radius = Math.min(width, height) / 2 / scaleFactor;
214
+ const numPoints = 96;
215
+ const options = { steps: numPoints, units: 'degrees' };
216
+ const circlePolygon = circle(center, radius, options);
217
+ return {
218
+ center,
219
+ radius,
220
+ polygon: circlePolygon,
221
+ steps: numPoints,
222
+ };
223
+ }, [originalViewState, width, height]);
194
224
  // It is possible for the embedding index+data to be out of order
195
225
  // with respect to the matrix index+data. Here, we align the embedding
196
226
  // data so that the rows are ordered the same as the matrix rows.
@@ -239,20 +269,19 @@ export function EmbeddingScatterplotSubscriber(props) {
239
269
  return null;
240
270
  }, [sampleEdges]);
241
271
  // Stratify multiple arrays: per-cellSet and per-sampleSet.
242
- const stratifiedData = useMemo(() => {
272
+ const [stratifiedData, stratifiedDataCount] = useMemo(() => {
243
273
  if (alignedEmbeddingData?.data) {
244
- const result = stratifyArrays(sampleEdges, sampleIdToObsIdsMap, sampleSets, sampleSetSelection, alignedEmbeddingIndex, mergedCellSets, cellSetSelection, {
274
+ const [result, cellCountResult] = stratifyArrays(sampleEdges, sampleIdToObsIdsMap, sampleSets, sampleSetSelection, alignedEmbeddingIndex, mergedCellSets, cellSetSelection, {
245
275
  obsEmbeddingX: alignedEmbeddingData.data[0],
246
276
  obsEmbeddingY: alignedEmbeddingData.data[1],
247
- // TODO: aggregate and transform expression data if needed prior to passing here
248
- ...(uint8ExpressionData?.[0] ? { featureValue: uint8ExpressionData?.[0] } : {}),
249
- });
250
- return result;
277
+ ...(uint8ExpressionData?.[0] ? { featureValue: uint8ExpressionData } : {}),
278
+ }, featureAggregationStrategyToUse);
279
+ return [result, cellCountResult];
251
280
  }
252
- return null;
281
+ return [null, null];
253
282
  }, [alignedEmbeddingIndex, alignedEmbeddingData, uint8ExpressionData,
254
283
  sampleEdges, sampleIdToObsIdsMap, sampleSets, sampleSetSelection,
255
- cellSetSelection, mergedCellSets,
284
+ cellSetSelection, mergedCellSets, featureAggregationStrategyToUse,
256
285
  ]);
257
286
  const setViewState = ({ zoom: newZoom, target }) => {
258
287
  setZoom(newZoom);
@@ -260,12 +289,16 @@ export function EmbeddingScatterplotSubscriber(props) {
260
289
  setTargetY(target[1]);
261
290
  setTargetZ(target[2] || 0);
262
291
  };
263
- return (_jsxs(TitleInfo, { title: title, info: `${commaNumber(cellsCount)} ${plur(observationsLabel, cellsCount)}`, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, urls: urls, theme: theme, isReady: isReady, helpText: helpText, options: (_jsx(ScatterplotOptions, { observationsLabel: observationsLabel, cellRadius: cellRadiusFixed, setCellRadius: setCellRadiusFixed, cellRadiusMode: cellRadiusMode, setCellRadiusMode: setCellRadiusMode, cellOpacity: cellOpacityFixed, setCellOpacity: setCellOpacityFixed, cellOpacityMode: cellOpacityMode, setCellOpacityMode: setCellOpacityMode, cellSetLabelsVisible: cellSetLabelsVisible, setCellSetLabelsVisible: setCellSetLabelsVisible, tooltipsVisible: tooltipsVisible, setTooltipsVisible: setTooltipsVisible, cellSetLabelSize: cellSetLabelSize, setCellSetLabelSize: setCellSetLabelSize, cellSetPolygonsVisible: cellSetPolygonsVisible, setCellSetPolygonsVisible: setCellSetPolygonsVisible, cellColorEncoding: cellColorEncoding, setCellColorEncoding: setCellColorEncoding, geneExpressionColormap: geneExpressionColormap, setGeneExpressionColormap: setGeneExpressionColormap, geneExpressionColormapRange: geneExpressionColormapRange, setGeneExpressionColormapRange: setGeneExpressionColormapRange, embeddingPointsVisible: embeddingPointsVisible, setEmbeddingPointsVisible: setEmbeddingPointsVisible, embeddingContoursVisible: embeddingContoursVisible, setEmbeddingContoursVisible: setEmbeddingContoursVisible, embeddingContoursFilled: embeddingContoursFilled, setEmbeddingContoursFilled: setEmbeddingContoursFilled, contourPercentiles: contourPercentiles, setContourPercentiles: setContourPercentiles, defaultContourPercentiles: DEFAULT_CONTOUR_PERCENTILES, contourColorEncoding: contourColorEncoding, setContourColorEncoding: setContourColorEncoding })), children: [_jsx(Scatterplot, { ref: deckRef, uuid: uuid, theme: theme, viewState: { zoom, target: [targetX, targetY, targetZ] }, setViewState: setViewState, originalViewState: originalViewState, obsEmbeddingIndex: obsEmbeddingIndex, obsEmbedding: obsEmbedding, cellFilter: cellFilter, cellSelection: cellSelection, cellHighlight: cellHighlight, cellColors: cellColors, cellSetPolygons: cellSetPolygons, cellSetLabelSize: cellSetLabelSize, cellSetLabelsVisible: cellSetLabelsVisible, cellSetPolygonsVisible: cellSetPolygonsVisible, setCellFilter: setCellFilter, setCellSelection: setCellSelectionProp, setCellHighlight: setCellHighlight, cellRadius: cellRadius, cellOpacity: cellOpacity, cellColorEncoding: cellColorEncoding, geneExpressionColormap: geneExpressionColormap, geneExpressionColormapRange: geneExpressionColormapRange, setComponentHover: () => {
292
+ // TODO: Update this once the rendered points reflects the selection/filtering.
293
+ const cellCountToUse = embeddingPointsVisible
294
+ ? cellsCount
295
+ : (stratifiedDataCount ?? cellsCount);
296
+ return (_jsxs(TitleInfo, { title: title, info: `${commaNumber(cellCountToUse)} ${plur(observationsLabel, cellCountToUse)}`, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, urls: urls, theme: theme, isReady: isReady, helpText: helpText, options: (_jsx(ScatterplotOptions, { observationsLabel: observationsLabel, cellRadius: cellRadiusFixed, setCellRadius: setCellRadiusFixed, cellRadiusMode: cellRadiusMode, setCellRadiusMode: setCellRadiusMode, cellOpacity: cellOpacityFixed, setCellOpacity: setCellOpacityFixed, cellOpacityMode: cellOpacityMode, setCellOpacityMode: setCellOpacityMode, cellSetLabelsVisible: cellSetLabelsVisible, setCellSetLabelsVisible: setCellSetLabelsVisible, tooltipsVisible: tooltipsVisible, setTooltipsVisible: setTooltipsVisible, cellSetLabelSize: cellSetLabelSize, setCellSetLabelSize: setCellSetLabelSize, cellSetPolygonsVisible: cellSetPolygonsVisible, setCellSetPolygonsVisible: setCellSetPolygonsVisible, cellColorEncoding: cellColorEncoding, setCellColorEncoding: setCellColorEncoding, geneExpressionColormap: geneExpressionColormap, setGeneExpressionColormap: setGeneExpressionColormap, geneExpressionColormapRange: geneExpressionColormapRange, setGeneExpressionColormapRange: setGeneExpressionColormapRange, embeddingPointsVisible: embeddingPointsVisible, setEmbeddingPointsVisible: setEmbeddingPointsVisible, embeddingContoursVisible: embeddingContoursVisible, setEmbeddingContoursVisible: setEmbeddingContoursVisible, embeddingContoursFilled: embeddingContoursFilled, setEmbeddingContoursFilled: setEmbeddingContoursFilled, contourPercentiles: contourPercentiles, setContourPercentiles: setContourPercentiles, defaultContourPercentiles: DEFAULT_CONTOUR_PERCENTILES, contourColorEncoding: contourColorEncoding, setContourColorEncoding: setContourColorEncoding, featureAggregationStrategy: featureAggregationStrategy, setFeatureAggregationStrategy: setFeatureAggregationStrategy })), children: [_jsx(Scatterplot, { ref: deckRef, uuid: uuid, theme: theme, viewState: { zoom, target: [targetX, targetY, targetZ] }, setViewState: setViewState, originalViewState: originalViewState, obsEmbeddingIndex: obsEmbeddingIndex, obsEmbedding: obsEmbedding, cellFilter: cellFilter, cellSelection: cellSelection, cellHighlight: cellHighlight, cellColors: cellColors, cellSetPolygons: cellSetPolygons, cellSetLabelSize: cellSetLabelSize, cellSetLabelsVisible: cellSetLabelsVisible, cellSetPolygonsVisible: cellSetPolygonsVisible, setCellFilter: setCellFilter, setCellSelection: setCellSelectionProp, setCellHighlight: setCellHighlight, cellRadius: cellRadius, cellOpacity: cellOpacity, cellColorEncoding: cellColorEncoding, geneExpressionColormap: geneExpressionColormap, geneExpressionColormapRange: geneExpressionColormapRange, setComponentHover: () => {
264
297
  setComponentHover(uuid);
265
298
  }, updateViewInfo: setComponentViewInfo, getExpressionValue: getExpressionValue, getCellIsSelected: getCellIsSelected, obsSetSelection: cellSetSelection, sampleSetSelection: sampleSetSelection,
266
299
  // InternMap data structures where keys are
267
300
  // obsSet -> sampleSet -> arrayKey -> [].
268
- stratifiedData: stratifiedData, obsSetColor: cellSetColor, sampleSetColor: sampleSetColor, contourThresholds: contourThresholds, contourColorEncoding: contourColorEncoding, contourColor: contourColor, contoursFilled: embeddingContoursFilled, embeddingPointsVisible: embeddingPointsVisible, embeddingContoursVisible: embeddingContoursVisible }), tooltipsVisible && (_jsx(ScatterplotTooltipSubscriber, { parentUuid: uuid, obsHighlight: cellHighlight, width: width, height: height, getObsInfo: getObsInfo, featureType: featureType, featureLabelsMap: featureLabelsMap })), _jsx(Legend, { visible: true, theme: theme, featureType: featureType, featureValueType: featureValueType, obsColorEncoding: cellColorEncoding, featureSelection: geneSelection, featureLabelsMap: featureLabelsMap, featureValueColormap: geneExpressionColormap, featureValueColormapRange: geneExpressionColormapRange, obsSetSelection: cellSetSelection, extent: expressionExtents?.[0], missing: expressionMissing?.[0],
301
+ stratifiedData: stratifiedData, obsSetColor: cellSetColor, sampleSetColor: sampleSetColor, contourThresholds: contourThresholds, contourColorEncoding: contourColorEncoding, contourColor: contourColor, contoursFilled: embeddingContoursFilled, embeddingPointsVisible: embeddingPointsVisible, embeddingContoursVisible: embeddingContoursVisible, circleInfo: circleInfo, featureSelection: geneSelection }), tooltipsVisible && width && height ? (_jsx(ScatterplotTooltipSubscriber, { parentUuid: uuid, obsHighlight: cellHighlight, width: width, height: height, getObsInfo: getObsInfo, featureType: featureType, featureLabelsMap: featureLabelsMap })) : null, _jsx(Legend, { visible: true, theme: theme, featureType: featureType, featureValueType: featureValueType, obsColorEncoding: cellColorEncoding, featureSelection: geneSelection, featureLabelsMap: featureLabelsMap, featureValueColormap: geneExpressionColormap, featureValueColormapRange: geneExpressionColormapRange, obsSetSelection: cellSetSelection, extent: expressionExtents, missing: expressionMissing,
269
302
  // Contour percentile legend
270
- pointsVisible: embeddingPointsVisible, contoursVisible: embeddingContoursVisible, contoursFilled: embeddingContoursFilled, contourPercentiles: contourPercentiles || DEFAULT_CONTOUR_PERCENTILES, contourThresholds: contourThresholds })] }));
303
+ pointsVisible: embeddingPointsVisible, contoursVisible: embeddingContoursVisible, contoursFilled: embeddingContoursFilled, contourPercentiles: contourPercentiles || DEFAULT_CONTOUR_PERCENTILES, contourThresholds: contourThresholds, featureAggregationStrategy: featureAggregationStrategyToUse })] }));
271
304
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/scatterplot-embedding",
3
- "version": "3.5.9",
3
+ "version": "3.5.11",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -20,12 +20,13 @@
20
20
  "d3-array": "^2.4.0",
21
21
  "lodash-es": "^4.17.21",
22
22
  "react-aria": "^3.28.0",
23
- "@vitessce/constants-internal": "3.5.9",
24
- "@vitessce/legend": "3.5.9",
25
- "@vitessce/scatterplot": "3.5.9",
26
- "@vitessce/sets-utils": "3.5.9",
27
- "@vitessce/utils": "3.5.9",
28
- "@vitessce/vit-s": "3.5.9"
23
+ "@turf/circle": "^7.2.0",
24
+ "@vitessce/constants-internal": "3.5.11",
25
+ "@vitessce/legend": "3.5.11",
26
+ "@vitessce/scatterplot": "3.5.11",
27
+ "@vitessce/sets-utils": "3.5.11",
28
+ "@vitessce/utils": "3.5.11",
29
+ "@vitessce/vit-s": "3.5.11"
29
30
  },
30
31
  "devDependencies": {
31
32
  "react": "^18.0.0",
@@ -34,6 +34,8 @@ export function DualEmbeddingScatterplotSubscriber(props) {
34
34
  sampleSetSelection,
35
35
  }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.DUAL_SCATTERPLOT], coordinationScopes);
36
36
 
37
+ const isCaseCtrl = Array.isArray(sampleSetSelection) && sampleSetSelection.length === 2;
38
+
37
39
  const caseSampleSetSelection = useMemo(() => (
38
40
  sampleSetSelection?.[0]
39
41
  ? [sampleSetSelection[0]]
@@ -47,22 +49,27 @@ export function DualEmbeddingScatterplotSubscriber(props) {
47
49
 
48
50
  return (
49
51
  <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'row' }}>
50
- <div style={{ width: '50%', display: 'flex', flexDirection: 'column' }}>
52
+ <div style={{ width: isCaseCtrl ? '50%' : '100%', display: 'flex', flexDirection: 'column' }}>
51
53
  <EmbeddingScatterplotSubscriber
52
54
  {...props}
53
55
  uuid={`${uuid}-case`}
54
- title={`Scatterplot (${embeddingType}), ${caseSampleSetSelection?.[0]?.at(-1)}`}
56
+ title={(isCaseCtrl
57
+ ? `Scatterplot (${embeddingType}), ${caseSampleSetSelection?.[0]?.at(-1)}`
58
+ : null
59
+ )}
55
60
  sampleSetSelection={caseSampleSetSelection}
56
61
  />
57
62
  </div>
58
- <div style={{ width: '50%', display: 'flex', flexDirection: 'column' }}>
59
- <EmbeddingScatterplotSubscriber
60
- {...props}
61
- uuid={`${uuid}-ctrl`}
62
- title={`Scatterplot (${embeddingType}), ${ctrlSampleSetSelection?.[0]?.at(-1)}`}
63
- sampleSetSelection={ctrlSampleSetSelection}
64
- />
65
- </div>
63
+ {isCaseCtrl ? (
64
+ <div style={{ width: '50%', display: 'flex', flexDirection: 'column' }}>
65
+ <EmbeddingScatterplotSubscriber
66
+ {...props}
67
+ uuid={`${uuid}-ctrl`}
68
+ title={`Scatterplot (${embeddingType}), ${ctrlSampleSetSelection?.[0]?.at(-1)}`}
69
+ sampleSetSelection={ctrlSampleSetSelection}
70
+ />
71
+ </div>
72
+ ) : null}
66
73
  </div>
67
74
  );
68
75
  }
@@ -3,6 +3,7 @@ import React, {
3
3
  } from 'react';
4
4
  import { extent, quantileSorted } from 'd3-array';
5
5
  import { isEqual } from 'lodash-es';
6
+ import { circle } from '@turf/circle';
6
7
  import {
7
8
  TitleInfo,
8
9
  useReady, useUrls,
@@ -39,6 +40,7 @@ import { Legend } from '@vitessce/legend';
39
40
  import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
40
41
  import { DEFAULT_CONTOUR_PERCENTILES } from './constants.js';
41
42
 
43
+ const DEFAULT_FEATURE_AGGREGATION_STRATEGY = 'first';
42
44
 
43
45
  /**
44
46
  * A subscriber component for the scatterplot.
@@ -112,6 +114,7 @@ export function EmbeddingScatterplotSubscriber(props) {
112
114
  embeddingContourPercentiles: contourPercentiles,
113
115
  contourColorEncoding,
114
116
  contourColor,
117
+ featureAggregationStrategy,
115
118
  }, {
116
119
  setEmbeddingZoom: setZoom,
117
120
  setEmbeddingTargetX: setTargetX,
@@ -138,6 +141,7 @@ export function EmbeddingScatterplotSubscriber(props) {
138
141
  setEmbeddingContoursFilled,
139
142
  setEmbeddingContourPercentiles: setContourPercentiles,
140
143
  setContourColorEncoding,
144
+ setFeatureAggregationStrategy,
141
145
  }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.SCATTERPLOT], coordinationScopes);
142
146
 
143
147
  const {
@@ -154,6 +158,9 @@ export function EmbeddingScatterplotSubscriber(props) {
154
158
  || sampleSetSelectionFromCoordination
155
159
  );
156
160
 
161
+ const featureAggregationStrategyToUse = featureAggregationStrategy
162
+ ?? DEFAULT_FEATURE_AGGREGATION_STRATEGY;
163
+
157
164
  const [width, height, deckRef] = useDeckCanvasSize();
158
165
 
159
166
  const title = titleOverride || `Scatterplot (${mapping})`;
@@ -298,7 +305,7 @@ export function EmbeddingScatterplotSubscriber(props) {
298
305
  // compute the cell radius scale based on the
299
306
  // extents of the cell coordinates on the x/y axes.
300
307
  useEffect(() => {
301
- if (xRange && yRange) {
308
+ if (xRange && yRange && width && height) {
302
309
  const pointSizeDevicePixels = getPointSizeDevicePixels(
303
310
  window.devicePixelRatio, zoom, xRange, yRange, width, height,
304
311
  );
@@ -379,9 +386,36 @@ export function EmbeddingScatterplotSubscriber(props) {
379
386
  .map(t => Math.max(t, 1.0));
380
387
  return thresholds;
381
388
  }
382
- return null;
389
+ return [1, 10, 100];
383
390
  }, [contourPercentiles, sortedWeights]);
384
391
 
392
+ // Construct a circle polygon using Turf's circle function,
393
+ // which surrounds all points in the scatterplot,
394
+ // which we can use to position text labels along.
395
+ const circleInfo = useMemo(() => {
396
+ if (!originalViewState || !width || !height) {
397
+ return null;
398
+ }
399
+ const center = [
400
+ originalViewState.target[0],
401
+ originalViewState.target[1],
402
+ ];
403
+ const scaleFactor = (2 ** originalViewState.zoom);
404
+ if (!(typeof scaleFactor === 'number' && typeof center[0] === 'number' && typeof center[1] === 'number') || Number.isNaN(scaleFactor)) {
405
+ return null;
406
+ }
407
+ const radius = Math.min(width, height) / 2 / scaleFactor;
408
+ const numPoints = 96;
409
+ const options = { steps: numPoints, units: 'degrees' };
410
+ const circlePolygon = circle(center, radius, options);
411
+ return {
412
+ center,
413
+ radius,
414
+ polygon: circlePolygon,
415
+ steps: numPoints,
416
+ };
417
+ }, [originalViewState, width, height]);
418
+
385
419
  // It is possible for the embedding index+data to be out of order
386
420
  // with respect to the matrix index+data. Here, we align the embedding
387
421
  // data so that the rows are ordered the same as the matrix rows.
@@ -432,24 +466,23 @@ export function EmbeddingScatterplotSubscriber(props) {
432
466
  }, [sampleEdges]);
433
467
 
434
468
  // Stratify multiple arrays: per-cellSet and per-sampleSet.
435
- const stratifiedData = useMemo(() => {
469
+ const [stratifiedData, stratifiedDataCount] = useMemo(() => {
436
470
  if (alignedEmbeddingData?.data) {
437
- const result = stratifyArrays(
471
+ const [result, cellCountResult] = stratifyArrays(
438
472
  sampleEdges, sampleIdToObsIdsMap,
439
473
  sampleSets, sampleSetSelection,
440
474
  alignedEmbeddingIndex, mergedCellSets, cellSetSelection, {
441
475
  obsEmbeddingX: alignedEmbeddingData.data[0],
442
476
  obsEmbeddingY: alignedEmbeddingData.data[1],
443
- // TODO: aggregate and transform expression data if needed prior to passing here
444
- ...(uint8ExpressionData?.[0] ? { featureValue: uint8ExpressionData?.[0] } : {}),
445
- },
477
+ ...(uint8ExpressionData?.[0] ? { featureValue: uint8ExpressionData } : {}),
478
+ }, featureAggregationStrategyToUse,
446
479
  );
447
- return result;
480
+ return [result, cellCountResult];
448
481
  }
449
- return null;
482
+ return [null, null];
450
483
  }, [alignedEmbeddingIndex, alignedEmbeddingData, uint8ExpressionData,
451
484
  sampleEdges, sampleIdToObsIdsMap, sampleSets, sampleSetSelection,
452
- cellSetSelection, mergedCellSets,
485
+ cellSetSelection, mergedCellSets, featureAggregationStrategyToUse,
453
486
  ]);
454
487
 
455
488
  const setViewState = ({ zoom: newZoom, target }) => {
@@ -459,10 +492,15 @@ export function EmbeddingScatterplotSubscriber(props) {
459
492
  setTargetZ(target[2] || 0);
460
493
  };
461
494
 
495
+ // TODO: Update this once the rendered points reflects the selection/filtering.
496
+ const cellCountToUse = embeddingPointsVisible
497
+ ? cellsCount
498
+ : (stratifiedDataCount ?? cellsCount);
499
+
462
500
  return (
463
501
  <TitleInfo
464
502
  title={title}
465
- info={`${commaNumber(cellsCount)} ${plur(observationsLabel, cellsCount)}`}
503
+ info={`${commaNumber(cellCountToUse)} ${plur(observationsLabel, cellCountToUse)}`}
466
504
  closeButtonVisible={closeButtonVisible}
467
505
  downloadButtonVisible={downloadButtonVisible}
468
506
  removeGridComponent={removeGridComponent}
@@ -506,6 +544,8 @@ export function EmbeddingScatterplotSubscriber(props) {
506
544
  defaultContourPercentiles={DEFAULT_CONTOUR_PERCENTILES}
507
545
  contourColorEncoding={contourColorEncoding}
508
546
  setContourColorEncoding={setContourColorEncoding}
547
+ featureAggregationStrategy={featureAggregationStrategy}
548
+ setFeatureAggregationStrategy={setFeatureAggregationStrategy}
509
549
  />
510
550
  )}
511
551
  >
@@ -554,18 +594,21 @@ export function EmbeddingScatterplotSubscriber(props) {
554
594
  contoursFilled={embeddingContoursFilled}
555
595
  embeddingPointsVisible={embeddingPointsVisible}
556
596
  embeddingContoursVisible={embeddingContoursVisible}
597
+
598
+ circleInfo={circleInfo}
599
+ featureSelection={geneSelection}
557
600
  />
558
- {tooltipsVisible && (
559
- <ScatterplotTooltipSubscriber
560
- parentUuid={uuid}
561
- obsHighlight={cellHighlight}
562
- width={width}
563
- height={height}
564
- getObsInfo={getObsInfo}
565
- featureType={featureType}
566
- featureLabelsMap={featureLabelsMap}
567
- />
568
- )}
601
+ {tooltipsVisible && width && height ? (
602
+ <ScatterplotTooltipSubscriber
603
+ parentUuid={uuid}
604
+ obsHighlight={cellHighlight}
605
+ width={width}
606
+ height={height}
607
+ getObsInfo={getObsInfo}
608
+ featureType={featureType}
609
+ featureLabelsMap={featureLabelsMap}
610
+ />
611
+ ) : null}
569
612
  <Legend
570
613
  visible
571
614
  theme={theme}
@@ -577,14 +620,15 @@ export function EmbeddingScatterplotSubscriber(props) {
577
620
  featureValueColormap={geneExpressionColormap}
578
621
  featureValueColormapRange={geneExpressionColormapRange}
579
622
  obsSetSelection={cellSetSelection}
580
- extent={expressionExtents?.[0]}
581
- missing={expressionMissing?.[0]}
623
+ extent={expressionExtents}
624
+ missing={expressionMissing}
582
625
  // Contour percentile legend
583
626
  pointsVisible={embeddingPointsVisible}
584
627
  contoursVisible={embeddingContoursVisible}
585
628
  contoursFilled={embeddingContoursFilled}
586
629
  contourPercentiles={contourPercentiles || DEFAULT_CONTOUR_PERCENTILES}
587
630
  contourThresholds={contourThresholds}
631
+ featureAggregationStrategy={featureAggregationStrategyToUse}
588
632
  />
589
633
  </TitleInfo>
590
634
  );