@vitessce/scatterplot-gating 3.7.1 → 3.8.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.
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-SxljTded.js";
2
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
2
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
3
3
  class DeflateDecoder extends BaseDecoder {
4
4
  decodeBlock(buffer) {
5
5
  return inflate_1(new Uint8Array(buffer)).buffer;
@@ -4,7 +4,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
4
4
  var _a2, _b;
5
5
  import * as React from "react";
6
6
  import React__default, { useLayoutEffect, useEffect, cloneElement, createElement, forwardRef, useState, useRef, useMemo, useImperativeHandle, useContext, createContext, isValidElement, Children, PureComponent, useCallback } from "react";
7
- import { usePlotOptionsStyles, OptionsContainer, CellColorEncodingOption, OptionSelect, useVitessceContainer, useComponentHover, useComponentViewInfo, useLoaders, useCoordinationScopes, useSetComponentHover, useSetComponentViewInfo, useCoordination, useDeckCanvasSize, useObsSetsData, useFeatureSelection, useObsFeatureMatrixIndices, useReady, useUrls, useUint8FeatureSelection, useExpressionValueGetter, TitleInfo } from "@vitessce/vit-s";
7
+ import { usePlotOptionsStyles, OptionsContainer, CellColorEncodingOption, OptionSelect, useVitessceContainer, useComponentHover, useComponentViewInfo, useLoaders, useCoordinationScopes, useSetComponentHover, useSetComponentViewInfo, useCoordination, useInitialCoordination, useDeckCanvasSize, useObsSetsData, useFeatureSelection, useObsFeatureMatrixIndices, useReady, useUrls, useUint8FeatureSelection, useExpressionValueGetter, TitleInfo } from "@vitessce/vit-s";
8
8
  import * as ReactDOM from "react-dom";
9
9
  import ReactDOM__default from "react-dom";
10
10
  function _mergeNamespaces(n2, m2) {
@@ -9492,6 +9492,7 @@ const ViewType$1 = {
9492
9492
  SCATTERPLOT: "scatterplot",
9493
9493
  SPATIAL: "spatial",
9494
9494
  SPATIAL_BETA: "spatialBeta",
9495
+ // SPATIAL_ACCELERATED: 'spatialAccelerated',
9495
9496
  HEATMAP: "heatmap",
9496
9497
  LAYER_CONTROLLER: "layerController",
9497
9498
  LAYER_CONTROLLER_BETA: "layerControllerBeta",
@@ -9785,6 +9786,8 @@ const CoordinationType$1 = {
9785
9786
  // Could be per-image-layer
9786
9787
  SPATIAL_TARGET_RESOLUTION: "spatialTargetResolution",
9787
9788
  // Per-spatial-layer
9789
+ SPATIAL_MAX_RESOLUTION: "spatialMaxResolution",
9790
+ // Per-image-channel (for spatial-accelerated)
9788
9791
  // For clipping plane sliders
9789
9792
  SPATIAL_SLICE_X: "spatialSliceX",
9790
9793
  SPATIAL_SLICE_Y: "spatialSliceY",
@@ -10333,6 +10336,7 @@ const COMPONENT_COORDINATION_TYPES = {
10333
10336
  CoordinationType$1.SPATIAL_RENDERING_MODE,
10334
10337
  CoordinationType$1.VOLUMETRIC_RENDERING_ALGORITHM,
10335
10338
  CoordinationType$1.SPATIAL_TARGET_RESOLUTION,
10339
+ CoordinationType$1.SPATIAL_MAX_RESOLUTION,
10336
10340
  CoordinationType$1.SPATIAL_SLICE_X,
10337
10341
  CoordinationType$1.SPATIAL_SLICE_Y,
10338
10342
  CoordinationType$1.SPATIAL_SLICE_Z,
@@ -10506,6 +10510,7 @@ const COMPONENT_COORDINATION_TYPES = {
10506
10510
  CoordinationType$1.SPATIAL_RENDERING_MODE,
10507
10511
  CoordinationType$1.VOLUMETRIC_RENDERING_ALGORITHM,
10508
10512
  CoordinationType$1.SPATIAL_TARGET_RESOLUTION,
10513
+ CoordinationType$1.SPATIAL_MAX_RESOLUTION,
10509
10514
  CoordinationType$1.SPATIAL_SLICE_X,
10510
10515
  CoordinationType$1.SPATIAL_SLICE_Y,
10511
10516
  CoordinationType$1.SPATIAL_SLICE_Z,
@@ -123124,22 +123129,22 @@ function addDecoder(cases, importFn) {
123124
123129
  }
123125
123130
  cases.forEach((c2) => registry$1.set(c2, importFn));
123126
123131
  }
123127
- addDecoder([void 0, 1], () => import("./raw-9dT3xwhD.js").then((m2) => m2.default));
123128
- addDecoder(5, () => import("./lzw-C40QIo-x.js").then((m2) => m2.default));
123132
+ addDecoder([void 0, 1], () => import("./raw-CPZ15kc6.js").then((m2) => m2.default));
123133
+ addDecoder(5, () => import("./lzw-UI7yv_i_.js").then((m2) => m2.default));
123129
123134
  addDecoder(6, () => {
123130
123135
  throw new Error("old style JPEG compression is not supported.");
123131
123136
  });
123132
- addDecoder(7, () => import("./jpeg-CdsXFHlP.js").then((m2) => m2.default));
123133
- addDecoder([8, 32946], () => import("./deflate-DBNG5DrN.js").then((m2) => m2.default));
123134
- addDecoder(32773, () => import("./packbits-DuTPSgqu.js").then((m2) => m2.default));
123137
+ addDecoder(7, () => import("./jpeg-DVC1608-.js").then((m2) => m2.default));
123138
+ addDecoder([8, 32946], () => import("./deflate-BKZQ1leu.js").then((m2) => m2.default));
123139
+ addDecoder(32773, () => import("./packbits-9luiYC71.js").then((m2) => m2.default));
123135
123140
  addDecoder(
123136
123141
  34887,
123137
- () => import("./lerc-BI9jJUb4.js").then(async (m2) => {
123142
+ () => import("./lerc-DNyzMqIs.js").then(async (m2) => {
123138
123143
  await m2.zstd.init();
123139
123144
  return m2;
123140
123145
  }).then((m2) => m2.default)
123141
123146
  );
123142
- addDecoder(50001, () => import("./webimage-DCRfoRZ7.js").then((m2) => m2.default));
123147
+ addDecoder(50001, () => import("./webimage-D6ZE7-4c.js").then((m2) => m2.default));
123143
123148
  function decodeRowAcc(row, stride) {
123144
123149
  let length2 = row.length - stride;
123145
123150
  let offset2 = 0;
@@ -158134,13 +158139,13 @@ function IconButton(props) {
158134
158139
  function ToolMenu(props) {
158135
158140
  const pointerIconAltText = "Pointer tool";
158136
158141
  const lassoIconAltText = "Select lasso";
158137
- const { setActiveTool, activeTool, visibleTools = { pan: true, selectLasso: true }, recenterOnClick = () => {
158142
+ const { setActiveTool, activeTool, visibleTools = { pan: true, selectLasso: true, recenter: true }, recenterOnClick = () => {
158138
158143
  } } = props;
158139
158144
  const { classes } = useStyles$2();
158140
158145
  const onRecenterButtonCLick = () => {
158141
158146
  recenterOnClick();
158142
158147
  };
158143
- return jsxRuntimeExports.jsxs("div", { className: classes.tool, children: [visibleTools.pan && jsxRuntimeExports.jsx(IconTool, { alt: pointerIconAltText, onClick: () => setActiveTool(null), isActive: activeTool === null, children: jsxRuntimeExports.jsx(SvgNearMe, {}) }), visibleTools.selectLasso ? jsxRuntimeExports.jsx(IconTool, { alt: lassoIconAltText, onClick: () => setActiveTool(distEs6Exports$2.SELECTION_TYPE.POLYGON), isActive: activeTool === distEs6Exports$2.SELECTION_TYPE.POLYGON, children: jsxRuntimeExports.jsx(SvgSelectionLasso, {}) }) : null, jsxRuntimeExports.jsx(IconButton, { alt: "click to recenter", onClick: () => onRecenterButtonCLick(), "aria-label": "Recenter scatterplot view", children: jsxRuntimeExports.jsx(CenterFocusStrong, {}) })] });
158148
+ return jsxRuntimeExports.jsxs("div", { className: classes.tool, children: [visibleTools.pan && jsxRuntimeExports.jsx(IconTool, { alt: pointerIconAltText, onClick: () => setActiveTool(null), isActive: activeTool === null, children: jsxRuntimeExports.jsx(SvgNearMe, {}) }), visibleTools.selectLasso ? jsxRuntimeExports.jsx(IconTool, { alt: lassoIconAltText, onClick: () => setActiveTool(distEs6Exports$2.SELECTION_TYPE.POLYGON), isActive: activeTool === distEs6Exports$2.SELECTION_TYPE.POLYGON, children: jsxRuntimeExports.jsx(SvgSelectionLasso, {}) }) : null, visibleTools.recenter ? jsxRuntimeExports.jsx(IconButton, { alt: "click to recenter", onClick: () => onRecenterButtonCLick(), "aria-label": "Recenter scatterplot view", children: jsxRuntimeExports.jsx(CenterFocusStrong, {}) }) : null] });
158144
158149
  }
158145
158150
  const getCursorWithTool = () => "crosshair";
158146
158151
  const getCursor = (interactionState) => interactionState.isDragging ? "grabbing" : "default";
@@ -158375,7 +158380,7 @@ class AbstractSpatialOrScatterplot extends PureComponent {
158375
158380
  */
158376
158381
  render() {
158377
158382
  var _a3, _b2, _c, _d;
158378
- const { deckRef, viewState, uuid, hideTools, orbitAxis } = this.props;
158383
+ const { deckRef, viewState, uuid, hideTools, hideRecenter, orbitAxis } = this.props;
158379
158384
  const { gl, tool } = this.state;
158380
158385
  const layers = this.getLayers();
158381
158386
  const use3d = this.use3d();
@@ -158384,7 +158389,8 @@ class AbstractSpatialOrScatterplot extends PureComponent {
158384
158389
  const useDevicePixels = !use3d && (((_b2 = (_a3 = this.obsSegmentationsData) == null ? void 0 : _a3.shape) == null ? void 0 : _b2[0]) < 1e5 || ((_d = (_c = this.obsLocationsData) == null ? void 0 : _c.shape) == null ? void 0 : _d[1]) < 1e5);
158385
158390
  return jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx(ToolMenu, { activeTool: tool, setActiveTool: this.onToolChange, visibleTools: {
158386
158391
  pan: showPanTool && !hideTools,
158387
- selectLasso: showCellSelectionTools && !hideTools
158392
+ selectLasso: showCellSelectionTools && !hideTools,
158393
+ recenter: !hideRecenter
158388
158394
  }, recenterOnClick: this.recenter }), jsxRuntimeExports.jsx(DeckGL, { id: `deckgl-overlay-${uuid}`, ref: deckRef, views: [
158389
158395
  use3d ? new OrbitView({ id: "orbit", controller: true, orbitAxis }) : new OrthographicView({
158390
158396
  id: "ortho"
@@ -159336,21 +159342,14 @@ function GatingScatterplotOptions(props) {
159336
159342
  transformOptions
159337
159343
  } = props;
159338
159344
  const gatingScatterplotOptionsId = $bdb11010cef70236$export$f680877a34711e37();
159345
+ const gatingScatterplotGeneXId = $bdb11010cef70236$export$f680877a34711e37();
159346
+ const gatingScatterplotGeneYId = $bdb11010cef70236$export$f680877a34711e37();
159339
159347
  const { classes } = usePlotOptionsStyles();
159340
- const handleGeneSelectChange = (event) => {
159341
- const { options } = event.target;
159342
- const newValues = [];
159343
- for (let i2 = 0, l2 = options.length; i2 < l2; i2 += 1) {
159344
- if (options[i2].selected) {
159345
- newValues.push(options[i2].value);
159346
- }
159347
- }
159348
- if (newValues.length === 1 && gatingFeatureSelectionX && !gatingFeatureSelectionY && newValues[0] !== gatingFeatureSelectionX) {
159349
- setGatingFeatureSelectionY(newValues[0]);
159350
- } else if (newValues.length <= 2) {
159351
- setGatingFeatureSelectionX(newValues[0]);
159352
- setGatingFeatureSelectionY(newValues[1]);
159353
- }
159348
+ const handleSelectionX = (event) => {
159349
+ setGatingFeatureSelectionX(event.target.value === "" ? null : event.target.value);
159350
+ };
159351
+ const handleSelectionY = (event) => {
159352
+ setGatingFeatureSelectionY(event.target.value === "" ? null : event.target.value);
159354
159353
  };
159355
159354
  const handleTransformChange = (event) => {
159356
159355
  setGatingFeatureValueTransform(event.target.value === "" ? null : event.target.value);
@@ -159369,20 +159368,40 @@ function GatingScatterplotOptions(props) {
159369
159368
  return /* @__PURE__ */ React__default.createElement(React__default.Fragment, null, /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.labelCell, variant: "head", scope: "row" }, /* @__PURE__ */ React__default.createElement(
159370
159369
  "label",
159371
159370
  {
159372
- htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`
159371
+ htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`
159372
+ },
159373
+ capitalize$2(featureType),
159374
+ " along X"
159375
+ )), /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.inputCell, variant: "body" }, /* @__PURE__ */ React__default.createElement(
159376
+ OptionSelect,
159377
+ {
159378
+ className: classes.select,
159379
+ value: gatingFeatureSelectionX ?? "",
159380
+ onChange: handleSelectionX,
159381
+ inputProps: {
159382
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`
159383
+ }
159384
+ },
159385
+ /* @__PURE__ */ React__default.createElement("option", { value: "" }, "None"),
159386
+ geneSelectOptions.map((name2) => /* @__PURE__ */ React__default.createElement("option", { key: name2, value: name2 }, name2))
159387
+ ))), /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.labelCell, variant: "head", scope: "row" }, /* @__PURE__ */ React__default.createElement(
159388
+ "label",
159389
+ {
159390
+ htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`
159373
159391
  },
159374
- capitalize$2(pluralize(featureType, geneSelectOptions == null ? void 0 : geneSelectOptions.length))
159392
+ capitalize$2(featureType),
159393
+ " along Y"
159375
159394
  )), /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.inputCell, variant: "body" }, /* @__PURE__ */ React__default.createElement(
159376
159395
  OptionSelect,
159377
159396
  {
159378
- multiple: true,
159379
159397
  className: classes.select,
159380
- value: [gatingFeatureSelectionX, gatingFeatureSelectionY].filter((v) => v),
159381
- onChange: handleGeneSelectChange,
159398
+ value: gatingFeatureSelectionY ?? "",
159399
+ onChange: handleSelectionY,
159382
159400
  inputProps: {
159383
- id: `scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`
159401
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`
159384
159402
  }
159385
159403
  },
159404
+ /* @__PURE__ */ React__default.createElement("option", { value: "" }, "None"),
159386
159405
  geneSelectOptions.map((name2) => /* @__PURE__ */ React__default.createElement("option", { key: name2, value: name2 }, name2))
159387
159406
  ))), /* @__PURE__ */ React__default.createElement(TableRow, null, /* @__PURE__ */ React__default.createElement(TableCell, { className: classes.labelCell, variant: "head", scope: "row" }, /* @__PURE__ */ React__default.createElement(
159388
159407
  "label",
@@ -159414,7 +159433,7 @@ function GatingScatterplotOptions(props) {
159414
159433
  type: "number",
159415
159434
  onChange: handleTransformCoefficientChange,
159416
159435
  value: gatingFeatureValueTransformCoefficient,
159417
- slotProps: { input: { shrink: true } },
159436
+ slotProps: { inputLabel: { shrink: true } },
159418
159437
  id: `scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}`
159419
159438
  }
159420
159439
  ))));
@@ -159494,6 +159513,14 @@ function GatingSubscriber(props) {
159494
159513
  COMPONENT_COORDINATION_TYPES[ViewType$1.GATING],
159495
159514
  coordinationScopes
159496
159515
  );
159516
+ const {
159517
+ embeddingZoom: initialZoom,
159518
+ embeddingTargetX: initialTargetX,
159519
+ embeddingTargetY: initialTargetY
159520
+ } = useInitialCoordination(
159521
+ COMPONENT_COORDINATION_TYPES[ViewType$1.GATING],
159522
+ coordinationScopes
159523
+ );
159497
159524
  const [width2, height2, deckRef] = useDeckCanvasSize();
159498
159525
  const title = useMemo(() => {
159499
159526
  if (titleOverride) {
@@ -159617,6 +159644,7 @@ function GatingSubscriber(props) {
159617
159644
  ]);
159618
159645
  const [dynamicCellRadius, setDynamicCellRadius] = useState(cellRadiusFixed);
159619
159646
  const [dynamicCellOpacity, setDynamicCellOpacity] = useState(cellOpacityFixed);
159647
+ const [originalViewState, setOriginalViewState] = useState(null);
159620
159648
  const mergedCellSets = useMemo(() => mergeObsSets(
159621
159649
  cellSets,
159622
159650
  additionalCellSets
@@ -159717,13 +159745,20 @@ function GatingSubscriber(props) {
159717
159745
  averageFillDensity
159718
159746
  );
159719
159747
  setDynamicCellOpacity(nextCellOpacityScale);
159720
- if (typeof targetX !== "number" || typeof targetY !== "number") {
159748
+ if (typeof initialTargetX !== "number" || typeof initialTargetY !== "number") {
159721
159749
  const newTargetX = xExtent[0] + xRange / 2;
159722
159750
  const newTargetY = yExtent[0] + yRange / 2;
159723
159751
  const newZoom = Math.log2(Math.min(width2 / xRange, height2 / yRange));
159724
- setTargetX(newTargetX);
159725
- setTargetY(-newTargetY);
159726
- setZoom(newZoom);
159752
+ const notYetInitialized = typeof targetX !== "number" || typeof targetY !== "number";
159753
+ const stillDefaultInitialized = targetX === newTargetX && targetY === -newTargetY;
159754
+ if (notYetInitialized || stillDefaultInitialized) {
159755
+ setTargetX(newTargetX);
159756
+ setTargetY(-newTargetY);
159757
+ setZoom(newZoom);
159758
+ }
159759
+ setOriginalViewState({ target: [newTargetX, -newTargetY, 0], zoom: newZoom });
159760
+ } else if (!originalViewState) {
159761
+ setOriginalViewState({ target: [initialTargetX, initialTargetY, 0], zoom: initialZoom });
159727
159762
  }
159728
159763
  }
159729
159764
  }, [
@@ -159735,6 +159770,8 @@ function GatingSubscriber(props) {
159735
159770
  width2,
159736
159771
  height2,
159737
159772
  zoom,
159773
+ initialTargetX,
159774
+ initialTargetY,
159738
159775
  averageFillDensity
159739
159776
  ]);
159740
159777
  const cellSelectionSet = useMemo(() => new Set(cellSelection), [cellSelection]);
@@ -159768,6 +159805,12 @@ function GatingSubscriber(props) {
159768
159805
  gatingFeatureSelectionY,
159769
159806
  obsType
159770
159807
  ]);
159808
+ const setViewState = ({ zoom: newZoom, target }) => {
159809
+ setZoom(newZoom);
159810
+ setTargetX(target[0]);
159811
+ setTargetY(target[1]);
159812
+ setTargetZ(target[2] || 0);
159813
+ };
159771
159814
  return /* @__PURE__ */ React__default.createElement(
159772
159815
  TitleInfo,
159773
159816
  {
@@ -159843,13 +159886,10 @@ function GatingSubscriber(props) {
159843
159886
  uuid,
159844
159887
  theme,
159845
159888
  hideTools: !(gatingFeatureSelectionX && gatingFeatureSelectionY),
159889
+ hideRecenter: !(gatingFeatureSelectionX && gatingFeatureSelectionY),
159846
159890
  viewState: { zoom, target: [targetX, targetY, targetZ] },
159847
- setViewState: ({ zoom: newZoom, target }) => {
159848
- setZoom(newZoom);
159849
- setTargetX(target[0]);
159850
- setTargetY(target[1]);
159851
- setTargetZ(target[2] || 0);
159852
- },
159891
+ setViewState,
159892
+ originalViewState,
159853
159893
  obsEmbeddingIndex: obsIndex,
159854
159894
  obsEmbedding: obsXY,
159855
159895
  cellFilter,
@@ -159873,7 +159913,8 @@ function GatingSubscriber(props) {
159873
159913
  },
159874
159914
  updateViewInfo: setComponentViewInfo,
159875
159915
  getExpressionValue,
159876
- getCellIsSelected
159916
+ getCellIsSelected,
159917
+ embeddingPointsVisible: true
159877
159918
  }
159878
159919
  ),
159879
159920
  !disableTooltip && /* @__PURE__ */ React__default.createElement(
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { G } from "./index-CQUIaH69.js";
1
+ import { G } from "./index-BtZNxZ-h.js";
2
2
  export {
3
3
  G as GatingSubscriber
4
4
  };
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
1
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
2
2
  const dctZigZag = new Int32Array([
3
3
  0,
4
4
  1,
@@ -1,5 +1,5 @@
1
1
  import { i as inflate_1 } from "./pako.esm-SxljTded.js";
2
- import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-CQUIaH69.js";
2
+ import { g as getDefaultExportFromCjs, B as BaseDecoder } from "./index-BtZNxZ-h.js";
3
3
  const LercParameters = {
4
4
  AddCompression: 1
5
5
  };
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
1
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
2
2
  const MIN_BITS = 9;
3
3
  const CLEAR_CODE = 256;
4
4
  const EOI_CODE = 257;
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
1
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
2
2
  class PackbitsDecoder extends BaseDecoder {
3
3
  decodeBlock(buffer) {
4
4
  const dataView = new DataView(buffer);
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
1
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
2
2
  class RawDecoder extends BaseDecoder {
3
3
  decodeBlock(buffer) {
4
4
  return buffer;
@@ -1,4 +1,4 @@
1
- import { B as BaseDecoder } from "./index-CQUIaH69.js";
1
+ import { B as BaseDecoder } from "./index-BtZNxZ-h.js";
2
2
  class WebImageDecoder extends BaseDecoder {
3
3
  constructor() {
4
4
  super();
@@ -1 +1 @@
1
- {"version":3,"file":"GatingScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/GatingScatterplotOptions.js"],"names":[],"mappings":"AAMA,0EAoIC"}
1
+ {"version":3,"file":"GatingScatterplotOptions.d.ts","sourceRoot":"","sources":["../src/GatingScatterplotOptions.js"],"names":[],"mappings":"AAMA,0EAwJC"}
@@ -1,32 +1,21 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from 'react';
3
3
  import { useId } from 'react-aria';
4
4
  import { TableCell, TableRow, TextField } from '@vitessce/styles';
5
5
  import { usePlotOptionsStyles, OptionSelect } from '@vitessce/vit-s';
6
- import { capitalize, pluralize as plur } from '@vitessce/utils';
6
+ import { capitalize } from '@vitessce/utils';
7
7
  export default function GatingScatterplotOptions(props) {
8
8
  const { featureType, gatingFeatureSelectionX, setGatingFeatureSelectionX, gatingFeatureSelectionY, setGatingFeatureSelectionY, gatingFeatureValueTransform, setGatingFeatureValueTransform, gatingFeatureValueTransformCoefficient, setGatingFeatureValueTransformCoefficient, geneSelectOptions, transformOptions, } = props;
9
9
  const gatingScatterplotOptionsId = useId();
10
+ const gatingScatterplotGeneXId = useId();
11
+ const gatingScatterplotGeneYId = useId();
10
12
  const { classes } = usePlotOptionsStyles();
11
13
  // Handlers for custom option field changes.
12
- const handleGeneSelectChange = (event) => {
13
- const { options } = event.target;
14
- const newValues = [];
15
- for (let i = 0, l = options.length; i < l; i += 1) {
16
- if (options[i].selected) {
17
- newValues.push(options[i].value);
18
- }
19
- }
20
- if (newValues.length === 1
21
- && gatingFeatureSelectionX
22
- && !gatingFeatureSelectionY
23
- && newValues[0] !== gatingFeatureSelectionX) {
24
- setGatingFeatureSelectionY(newValues[0]);
25
- }
26
- else if (newValues.length <= 2) {
27
- setGatingFeatureSelectionX(newValues[0]);
28
- setGatingFeatureSelectionY(newValues[1]);
29
- }
14
+ const handleSelectionX = (event) => {
15
+ setGatingFeatureSelectionX(event.target.value === '' ? null : event.target.value);
16
+ };
17
+ const handleSelectionY = (event) => {
18
+ setGatingFeatureSelectionY(event.target.value === '' ? null : event.target.value);
30
19
  };
31
20
  const handleTransformChange = (event) => {
32
21
  setGatingFeatureValueTransform(event.target.value === '' ? null : event.target.value);
@@ -45,9 +34,11 @@ export default function GatingScatterplotOptions(props) {
45
34
  }
46
35
  }
47
36
  };
48
- return (_jsxs(_Fragment, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`, children: capitalize(plur(featureType, geneSelectOptions?.length)) }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(OptionSelect, { multiple: true, className: classes.select, value: [gatingFeatureSelectionX, gatingFeatureSelectionY].filter(v => v), onChange: handleGeneSelectChange, inputProps: {
49
- id: `scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`,
50
- }, children: geneSelectOptions.map(name => (_jsx("option", { value: name, children: name }, name))) }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gating-transform-select-${gatingScatterplotOptionsId}`, children: "Transform" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(OptionSelect, { className: classes.select, value: gatingFeatureValueTransform === null ? '' : gatingFeatureValueTransform, onChange: handleTransformChange, inputProps: {
37
+ return (_jsxs(_Fragment, { children: [_jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsxs("label", { htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`, children: [capitalize(featureType), " along X"] }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsxs(OptionSelect, { className: classes.select, value: gatingFeatureSelectionX ?? '', onChange: handleSelectionX, inputProps: {
38
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`,
39
+ }, children: [_jsx("option", { value: "", children: "None" }), geneSelectOptions.map(name => (_jsx("option", { value: name, children: name }, name)))] }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsxs("label", { htmlFor: `scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`, children: [capitalize(featureType), " along Y"] }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsxs(OptionSelect, { className: classes.select, value: gatingFeatureSelectionY ?? '', onChange: handleSelectionY, inputProps: {
40
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`,
41
+ }, children: [_jsx("option", { value: "", children: "None" }), geneSelectOptions.map(name => (_jsx("option", { value: name, children: name }, name)))] }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gating-transform-select-${gatingScatterplotOptionsId}`, children: "Transform" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(OptionSelect, { className: classes.select, value: gatingFeatureValueTransform === null ? '' : gatingFeatureValueTransform, onChange: handleTransformChange, inputProps: {
51
42
  id: `scatterplot-gating-transform-select-${gatingScatterplotOptionsId}`,
52
- }, children: transformOptions.map(opt => (_jsx("option", { value: opt.value === null ? '' : opt.value, children: opt.name }, opt.name))) }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}`, children: "Transform Coefficient" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(TextField, { label: "Transform Coefficient", type: "number", onChange: handleTransformCoefficientChange, value: gatingFeatureValueTransformCoefficient, slotProps: { input: { shrink: true } }, id: `scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}` }) })] })] }));
43
+ }, children: transformOptions.map(opt => (_jsx("option", { value: opt.value === null ? '' : opt.value, children: opt.name }, opt.name))) }) })] }), _jsxs(TableRow, { children: [_jsx(TableCell, { className: classes.labelCell, variant: "head", scope: "row", children: _jsx("label", { htmlFor: `scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}`, children: "Transform Coefficient" }) }), _jsx(TableCell, { className: classes.inputCell, variant: "body", children: _jsx(TextField, { label: "Transform Coefficient", type: "number", onChange: handleTransformCoefficientChange, value: gatingFeatureValueTransformCoefficient, slotProps: { inputLabel: { shrink: true } }, id: `scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}` }) })] })] }));
53
44
  }
@@ -1 +1 @@
1
- {"version":3,"file":"GatingSubscriber.d.ts","sourceRoot":"","sources":["../src/GatingSubscriber.js"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;KAYK;AACL,wCAVK;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAES,cAAc,EAA7B,OAAO;IACS,mBAAmB;IAErB,kBAAkB,EAAhC,MAAM;CAEhB,eAobF"}
1
+ {"version":3,"file":"GatingSubscriber.d.ts","sourceRoot":"","sources":["../src/GatingSubscriber.js"],"names":[],"mappings":"AAqCA;;;;;;;;;;;;KAYK;AACL,wCAVK;IAAsB,IAAI,EAAlB,MAAM;IACQ,KAAK,EAAnB,MAAM;IACQ,kBAAkB,EAAhC,MAAM;IAES,cAAc,EAA7B,OAAO;IACS,mBAAmB;IAErB,kBAAkB,EAAhC,MAAM;CAEhB,eA6cF"}
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback, useMemo, } from 'react';
3
3
  import { extent } from 'd3-array';
4
4
  import { isEqual } from 'lodash-es';
5
5
  import { pluralize as plur, capitalize, commaNumber, getValueTransformFunction, VALUE_TRANSFORM_OPTIONS, } from '@vitessce/utils';
6
- import { TitleInfo, useDeckCanvasSize, useReady, useUrls, useUint8FeatureSelection, useExpressionValueGetter, useObsSetsData, useFeatureSelection, useObsFeatureMatrixIndices, useCoordination, useLoaders, useSetComponentHover, useSetComponentViewInfo, useCoordinationScopes, } from '@vitessce/vit-s';
6
+ import { TitleInfo, useDeckCanvasSize, useReady, useUrls, useUint8FeatureSelection, useExpressionValueGetter, useObsSetsData, useFeatureSelection, useObsFeatureMatrixIndices, useCoordination, useInitialCoordination, useLoaders, useSetComponentHover, useSetComponentViewInfo, useCoordinationScopes, } from '@vitessce/vit-s';
7
7
  import { getCellSetPolygons, mergeObsSets, setObsSelection, getCellColors, } from '@vitessce/sets-utils';
8
8
  import { Scatterplot, ScatterplotTooltipSubscriber, ScatterplotOptions, getPointSizeDevicePixels, getPointOpacity, EmptyMessage, } from '@vitessce/scatterplot';
9
9
  import { ViewType, COMPONENT_COORDINATION_TYPES, ViewHelpMapping } from '@vitessce/constants-internal';
@@ -31,6 +31,7 @@ export function GatingSubscriber(props) {
31
31
  const setComponentViewInfo = useSetComponentViewInfo(uuid);
32
32
  // Get "props" from the coordination space.
33
33
  const [{ dataset, obsType, featureType, featureValueType, embeddingZoom: zoom, embeddingTargetX: targetX, embeddingTargetY: targetY, embeddingTargetZ: targetZ, obsFilter: cellFilter, obsHighlight: cellHighlight, 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, featureSelection: gatingFeatureSelectionColor, featureValueTransform, featureValueTransformCoefficient, gatingFeatureSelectionX, gatingFeatureSelectionY, }, { 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, setFeatureValueTransform, setFeatureValueTransformCoefficient, setGatingFeatureSelectionX, setGatingFeatureSelectionY, }] = useCoordination(COMPONENT_COORDINATION_TYPES[ViewType.GATING], coordinationScopes);
34
+ const { embeddingZoom: initialZoom, embeddingTargetX: initialTargetX, embeddingTargetY: initialTargetY, } = useInitialCoordination(COMPONENT_COORDINATION_TYPES[ViewType.GATING], coordinationScopes);
34
35
  const [width, height, deckRef] = useDeckCanvasSize();
35
36
  const title = useMemo(() => {
36
37
  if (titleOverride) {
@@ -105,6 +106,7 @@ export function GatingSubscriber(props) {
105
106
  ]);
106
107
  const [dynamicCellRadius, setDynamicCellRadius] = useState(cellRadiusFixed);
107
108
  const [dynamicCellOpacity, setDynamicCellOpacity] = useState(cellOpacityFixed);
109
+ const [originalViewState, setOriginalViewState] = useState(null);
108
110
  const mergedCellSets = useMemo(() => mergeObsSets(cellSets, additionalCellSets), [cellSets, additionalCellSets]);
109
111
  const setCellSelectionProp = useCallback((v) => {
110
112
  setObsSelection(v, additionalCellSets, cellSetColor, setCellSetSelection, setAdditionalCellSets, setCellSetColor, setCellColorEncoding);
@@ -165,19 +167,29 @@ export function GatingSubscriber(props) {
165
167
  setDynamicCellRadius(pointSizeDevicePixels);
166
168
  const nextCellOpacityScale = getPointOpacity(zoom, xRange, yRange, width, height, numCells, averageFillDensity);
167
169
  setDynamicCellOpacity(nextCellOpacityScale);
168
- if (typeof targetX !== 'number' || typeof targetY !== 'number') {
170
+ if (typeof initialTargetX !== 'number' || typeof initialTargetY !== 'number') {
169
171
  const newTargetX = xExtent[0] + xRange / 2;
170
172
  const newTargetY = yExtent[0] + yRange / 2;
171
173
  const newZoom = Math.log2(Math.min(width / xRange, height / yRange));
172
- setTargetX(newTargetX);
173
- // Graphics rendering has the y-axis going south so we need to multiply by negative one.
174
- setTargetY(-newTargetY);
175
- setZoom(newZoom);
174
+ const notYetInitialized = (typeof targetX !== 'number' || typeof targetY !== 'number');
175
+ const stillDefaultInitialized = (targetX === newTargetX && targetY === -newTargetY);
176
+ if (notYetInitialized || stillDefaultInitialized) {
177
+ setTargetX(newTargetX);
178
+ // Graphics rendering has the y-axis going south so we need to multiply by negative one.
179
+ setTargetY(-newTargetY);
180
+ setZoom(newZoom);
181
+ }
182
+ setOriginalViewState({ target: [newTargetX, -newTargetY, 0], zoom: newZoom });
183
+ }
184
+ else if (!originalViewState) {
185
+ // originalViewState has not yet been set and
186
+ // the view config defined an initial viewState.
187
+ setOriginalViewState({ target: [initialTargetX, initialTargetY, 0], zoom: initialZoom });
176
188
  }
177
189
  }
178
190
  // eslint-disable-next-line react-hooks/exhaustive-deps
179
191
  }, [xRange, yRange, xExtent, yExtent, numCells,
180
- width, height, zoom, averageFillDensity]);
192
+ width, height, zoom, initialTargetX, initialTargetY, averageFillDensity]);
181
193
  const cellSelectionSet = useMemo(() => new Set(cellSelection), [cellSelection]);
182
194
  const getCellIsSelected = useCallback((object, { index }) => ((cellSelectionSet || new Set([])).has(obsIndex[index]) ? 1.0 : 0.0), [cellSelectionSet, obsIndex]);
183
195
  const cellRadius = (cellRadiusMode === 'manual' ? cellRadiusFixed : dynamicCellRadius);
@@ -204,17 +216,18 @@ export function GatingSubscriber(props) {
204
216
  }, [obsXY, obsIndex, featureValueTransform,
205
217
  gatingFeatureSelectionX, gatingFeatureSelectionY, obsType,
206
218
  ]);
219
+ const setViewState = ({ zoom: newZoom, target }) => {
220
+ setZoom(newZoom);
221
+ setTargetX(target[0]);
222
+ setTargetY(target[1]);
223
+ setTargetZ(target[2] || 0);
224
+ };
207
225
  return (_jsxs(TitleInfo, { title: title, info: `${commaNumber(cellsCount)} ${plur(obsType, cellsCount)}`, closeButtonVisible: closeButtonVisible, downloadButtonVisible: downloadButtonVisible, removeGridComponent: removeGridComponent, urls: urls, theme: theme, isReady: isReady, helpText: helpText, errors: errors, options: (_jsx(ScatterplotOptions, { observationsLabel: obsType, cellRadius: cellRadiusFixed, setCellRadius: setCellRadiusFixed, cellRadiusMode: cellRadiusMode, setCellRadiusMode: setCellRadiusMode, cellOpacity: cellOpacityFixed, setCellOpacity: setCellOpacityFixed, cellOpacityMode: cellOpacityMode, setCellOpacityMode: setCellOpacityMode, cellSetLabelsVisible: cellSetLabelsVisible, setCellSetLabelsVisible: setCellSetLabelsVisible, cellSetLabelSize: cellSetLabelSize, setCellSetLabelSize: setCellSetLabelSize, cellSetPolygonsVisible: cellSetPolygonsVisible, setCellSetPolygonsVisible: setCellSetPolygonsVisible, cellColorEncoding: cellColorEncoding, setCellColorEncoding: setCellColorEncoding, geneExpressionColormap: geneExpressionColormap, setGeneExpressionColormap: setGeneExpressionColormap, geneExpressionColormapRange: geneExpressionColormapRange, setGeneExpressionColormapRange: setGeneExpressionColormapRange, children: _jsx(GatingScatterplotOptions, { featureType: featureType, gatingFeatureSelectionX: gatingFeatureSelectionX, setGatingFeatureSelectionX: setGatingFeatureSelectionX, gatingFeatureSelectionY: gatingFeatureSelectionY, setGatingFeatureSelectionY: setGatingFeatureSelectionY, gatingFeatureValueTransform: featureValueTransform, setGatingFeatureValueTransform: (newValue) => {
208
226
  setFeatureValueTransform(newValue);
209
227
  setTargetX(null);
210
228
  setTargetY(null);
211
229
  setZoom(null);
212
- }, gatingFeatureValueTransformCoefficient: featureValueTransformCoefficient, setGatingFeatureValueTransformCoefficient: setFeatureValueTransformCoefficient, geneSelectOptions: featureIndex, transformOptions: VALUE_TRANSFORM_OPTIONS }) })), children: [_jsx(EmptyMessage, { visible: !(gatingFeatureSelectionX && gatingFeatureSelectionY), message: `Select two ${plur(featureType, 2)} in the plot settings.` }), _jsx(Scatterplot, { ref: deckRef, uuid: uuid, theme: theme, hideTools: !(gatingFeatureSelectionX && gatingFeatureSelectionY), viewState: { zoom, target: [targetX, targetY, targetZ] }, setViewState: ({ zoom: newZoom, target }) => {
213
- setZoom(newZoom);
214
- setTargetX(target[0]);
215
- setTargetY(target[1]);
216
- setTargetZ(target[2] || 0);
217
- }, obsEmbeddingIndex: obsIndex, obsEmbedding: obsXY, 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: () => {
230
+ }, gatingFeatureValueTransformCoefficient: featureValueTransformCoefficient, setGatingFeatureValueTransformCoefficient: setFeatureValueTransformCoefficient, geneSelectOptions: featureIndex, transformOptions: VALUE_TRANSFORM_OPTIONS }) })), children: [_jsx(EmptyMessage, { visible: !(gatingFeatureSelectionX && gatingFeatureSelectionY), message: `Select two ${plur(featureType, 2)} in the plot settings.` }), _jsx(Scatterplot, { ref: deckRef, uuid: uuid, theme: theme, hideTools: !(gatingFeatureSelectionX && gatingFeatureSelectionY), hideRecenter: !(gatingFeatureSelectionX && gatingFeatureSelectionY), viewState: { zoom, target: [targetX, targetY, targetZ] }, setViewState: setViewState, originalViewState: originalViewState, obsEmbeddingIndex: obsIndex, obsEmbedding: obsXY, 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: () => {
218
231
  setComponentHover(uuid);
219
- }, updateViewInfo: setComponentViewInfo, getExpressionValue: getExpressionValue, getCellIsSelected: getCellIsSelected }), !disableTooltip && (_jsx(ScatterplotTooltipSubscriber, { parentUuid: uuid, obsHighlight: cellHighlight, width: width, height: height, getObsInfo: getObsInfo }))] }));
232
+ }, updateViewInfo: setComponentViewInfo, getExpressionValue: getExpressionValue, getCellIsSelected: getCellIsSelected, embeddingPointsVisible: true }), !disableTooltip && (_jsx(ScatterplotTooltipSubscriber, { parentUuid: uuid, obsHighlight: cellHighlight, width: width, height: height, getObsInfo: getObsInfo }))] }));
220
233
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vitessce/scatterplot-gating",
3
- "version": "3.7.1",
3
+ "version": "3.8.1",
4
4
  "author": "HIDIVE Lab at HMS",
5
5
  "homepage": "http://vitessce.io",
6
6
  "repository": {
@@ -19,16 +19,16 @@
19
19
  "d3-array": "^2.4.0",
20
20
  "lodash-es": "^4.17.21",
21
21
  "react-aria": "^3.28.0",
22
- "@vitessce/styles": "3.7.1",
23
- "@vitessce/constants-internal": "3.7.1",
24
- "@vitessce/scatterplot": "3.7.1",
25
- "@vitessce/sets-utils": "3.7.1",
26
- "@vitessce/vit-s": "3.7.1",
27
- "@vitessce/utils": "3.7.1"
22
+ "@vitessce/styles": "3.8.1",
23
+ "@vitessce/constants-internal": "3.8.1",
24
+ "@vitessce/scatterplot": "3.8.1",
25
+ "@vitessce/sets-utils": "3.8.1",
26
+ "@vitessce/utils": "3.8.1",
27
+ "@vitessce/vit-s": "3.8.1"
28
28
  },
29
29
  "devDependencies": {
30
- "react": "^18.0.0",
31
- "react-dom": "^18.0.0",
30
+ "react": "18.3.1",
31
+ "react-dom": "18.3.1",
32
32
  "vite": "^7.0.0",
33
33
  "vitest": "^3.1.4"
34
34
  },
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { useId } from 'react-aria';
3
3
  import { TableCell, TableRow, TextField } from '@vitessce/styles';
4
4
  import { usePlotOptionsStyles, OptionSelect } from '@vitessce/vit-s';
5
- import { capitalize, pluralize as plur } from '@vitessce/utils';
5
+ import { capitalize } from '@vitessce/utils';
6
6
 
7
7
  export default function GatingScatterplotOptions(props) {
8
8
  const {
@@ -20,28 +20,18 @@ export default function GatingScatterplotOptions(props) {
20
20
  } = props;
21
21
 
22
22
  const gatingScatterplotOptionsId = useId();
23
+ const gatingScatterplotGeneXId = useId();
24
+ const gatingScatterplotGeneYId = useId();
23
25
 
24
26
  const { classes } = usePlotOptionsStyles();
25
27
 
26
28
  // Handlers for custom option field changes.
27
- const handleGeneSelectChange = (event) => {
28
- const { options } = event.target;
29
- const newValues = [];
30
- for (let i = 0, l = options.length; i < l; i += 1) {
31
- if (options[i].selected) {
32
- newValues.push(options[i].value);
33
- }
34
- }
29
+ const handleSelectionX = (event) => {
30
+ setGatingFeatureSelectionX(event.target.value === '' ? null : event.target.value);
31
+ };
35
32
 
36
- if (newValues.length === 1
37
- && gatingFeatureSelectionX
38
- && !gatingFeatureSelectionY
39
- && newValues[0] !== gatingFeatureSelectionX) {
40
- setGatingFeatureSelectionY(newValues[0]);
41
- } else if (newValues.length <= 2) {
42
- setGatingFeatureSelectionX(newValues[0]);
43
- setGatingFeatureSelectionY(newValues[1]);
44
- }
33
+ const handleSelectionY = (event) => {
34
+ setGatingFeatureSelectionY(event.target.value === '' ? null : event.target.value);
45
35
  };
46
36
 
47
37
  const handleTransformChange = (event) => {
@@ -67,21 +57,51 @@ export default function GatingScatterplotOptions(props) {
67
57
  <TableRow>
68
58
  <TableCell className={classes.labelCell} variant="head" scope="row">
69
59
  <label
70
- htmlFor={`scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`}
60
+ htmlFor={`scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`}
61
+ >
62
+ {capitalize(featureType)} along X
63
+ </label>
64
+ </TableCell>
65
+ <TableCell className={classes.inputCell} variant="body">
66
+ <OptionSelect
67
+ className={classes.select}
68
+ value={gatingFeatureSelectionX ?? ''}
69
+ onChange={handleSelectionX}
70
+ inputProps={{
71
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneXId}`,
72
+ }}
73
+ >
74
+ <option value="">
75
+ None
76
+ </option>
77
+ {geneSelectOptions.map(name => (
78
+ <option key={name} value={name}>
79
+ {name}
80
+ </option>
81
+ ))}
82
+ </OptionSelect>
83
+ </TableCell>
84
+ </TableRow>
85
+ <TableRow>
86
+ <TableCell className={classes.labelCell} variant="head" scope="row">
87
+ <label
88
+ htmlFor={`scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`}
71
89
  >
72
- {capitalize(plur(featureType, geneSelectOptions?.length))}
90
+ {capitalize(featureType)} along Y
73
91
  </label>
74
92
  </TableCell>
75
93
  <TableCell className={classes.inputCell} variant="body">
76
94
  <OptionSelect
77
- multiple
78
95
  className={classes.select}
79
- value={[gatingFeatureSelectionX, gatingFeatureSelectionY].filter(v => v)}
80
- onChange={handleGeneSelectChange}
96
+ value={gatingFeatureSelectionY ?? ''}
97
+ onChange={handleSelectionY}
81
98
  inputProps={{
82
- id: `scatterplot-gating-gene-select-${gatingScatterplotOptionsId}`,
99
+ id: `scatterplot-gating-gene-select-${gatingScatterplotGeneYId}`,
83
100
  }}
84
101
  >
102
+ <option value="">
103
+ None
104
+ </option>
85
105
  {geneSelectOptions.map(name => (
86
106
  <option key={name} value={name}>
87
107
  {name}
@@ -129,7 +149,7 @@ export default function GatingScatterplotOptions(props) {
129
149
  type="number"
130
150
  onChange={handleTransformCoefficientChange}
131
151
  value={gatingFeatureValueTransformCoefficient}
132
- slotProps={{ input: { shrink: true } }}
152
+ slotProps={{ inputLabel: { shrink: true } }}
133
153
  id={`scatterplot-gating-transform-coefficient-${gatingScatterplotOptionsId}`}
134
154
  />
135
155
  </TableCell>
@@ -17,6 +17,7 @@ import {
17
17
  useFeatureSelection,
18
18
  useObsFeatureMatrixIndices,
19
19
  useCoordination,
20
+ useInitialCoordination,
20
21
  useLoaders,
21
22
  useSetComponentHover,
22
23
  useSetComponentViewInfo,
@@ -126,6 +127,14 @@ export function GatingSubscriber(props) {
126
127
  coordinationScopes,
127
128
  );
128
129
 
130
+ const {
131
+ embeddingZoom: initialZoom,
132
+ embeddingTargetX: initialTargetX,
133
+ embeddingTargetY: initialTargetY,
134
+ } = useInitialCoordination(
135
+ COMPONENT_COORDINATION_TYPES[ViewType.GATING], coordinationScopes,
136
+ );
137
+
129
138
  const [width, height, deckRef] = useDeckCanvasSize();
130
139
 
131
140
  const title = useMemo(() => {
@@ -244,6 +253,8 @@ export function GatingSubscriber(props) {
244
253
  const [dynamicCellRadius, setDynamicCellRadius] = useState(cellRadiusFixed);
245
254
  const [dynamicCellOpacity, setDynamicCellOpacity] = useState(cellOpacityFixed);
246
255
 
256
+ const [originalViewState, setOriginalViewState] = useState(null);
257
+
247
258
  const mergedCellSets = useMemo(() => mergeObsSets(
248
259
  cellSets, additionalCellSets,
249
260
  ), [cellSets, additionalCellSets]);
@@ -323,19 +334,28 @@ export function GatingSubscriber(props) {
323
334
  );
324
335
  setDynamicCellOpacity(nextCellOpacityScale);
325
336
 
326
- if (typeof targetX !== 'number' || typeof targetY !== 'number') {
337
+ if (typeof initialTargetX !== 'number' || typeof initialTargetY !== 'number') {
327
338
  const newTargetX = xExtent[0] + xRange / 2;
328
339
  const newTargetY = yExtent[0] + yRange / 2;
329
340
  const newZoom = Math.log2(Math.min(width / xRange, height / yRange));
330
- setTargetX(newTargetX);
331
- // Graphics rendering has the y-axis going south so we need to multiply by negative one.
332
- setTargetY(-newTargetY);
333
- setZoom(newZoom);
341
+ const notYetInitialized = (typeof targetX !== 'number' || typeof targetY !== 'number');
342
+ const stillDefaultInitialized = (targetX === newTargetX && targetY === -newTargetY);
343
+ if (notYetInitialized || stillDefaultInitialized) {
344
+ setTargetX(newTargetX);
345
+ // Graphics rendering has the y-axis going south so we need to multiply by negative one.
346
+ setTargetY(-newTargetY);
347
+ setZoom(newZoom);
348
+ }
349
+ setOriginalViewState({ target: [newTargetX, -newTargetY, 0], zoom: newZoom });
350
+ } else if (!originalViewState) {
351
+ // originalViewState has not yet been set and
352
+ // the view config defined an initial viewState.
353
+ setOriginalViewState({ target: [initialTargetX, initialTargetY, 0], zoom: initialZoom });
334
354
  }
335
355
  }
336
356
  // eslint-disable-next-line react-hooks/exhaustive-deps
337
357
  }, [xRange, yRange, xExtent, yExtent, numCells,
338
- width, height, zoom, averageFillDensity]);
358
+ width, height, zoom, initialTargetX, initialTargetY, averageFillDensity]);
339
359
 
340
360
  const cellSelectionSet = useMemo(() => new Set(cellSelection), [cellSelection]);
341
361
  const getCellIsSelected = useCallback((object, { index }) => (
@@ -372,6 +392,13 @@ export function GatingSubscriber(props) {
372
392
  gatingFeatureSelectionX, gatingFeatureSelectionY, obsType,
373
393
  ]);
374
394
 
395
+ const setViewState = ({ zoom: newZoom, target }) => {
396
+ setZoom(newZoom);
397
+ setTargetX(target[0]);
398
+ setTargetY(target[1]);
399
+ setTargetZ(target[2] || 0);
400
+ };
401
+
375
402
  return (
376
403
  <TitleInfo
377
404
  title={title}
@@ -438,13 +465,10 @@ export function GatingSubscriber(props) {
438
465
  uuid={uuid}
439
466
  theme={theme}
440
467
  hideTools={!(gatingFeatureSelectionX && gatingFeatureSelectionY)}
468
+ hideRecenter={!(gatingFeatureSelectionX && gatingFeatureSelectionY)}
441
469
  viewState={{ zoom, target: [targetX, targetY, targetZ] }}
442
- setViewState={({ zoom: newZoom, target }) => {
443
- setZoom(newZoom);
444
- setTargetX(target[0]);
445
- setTargetY(target[1]);
446
- setTargetZ(target[2] || 0);
447
- }}
470
+ setViewState={setViewState}
471
+ originalViewState={originalViewState}
448
472
  obsEmbeddingIndex={obsIndex}
449
473
  obsEmbedding={obsXY}
450
474
  cellFilter={cellFilter}
@@ -470,6 +494,8 @@ export function GatingSubscriber(props) {
470
494
  getExpressionValue={getExpressionValue}
471
495
  getCellIsSelected={getCellIsSelected}
472
496
 
497
+ embeddingPointsVisible
498
+
473
499
  />
474
500
  {!disableTooltip && (
475
501
  <ScatterplotTooltipSubscriber