plexora 0.0.1__py3-none-any.whl
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.
- plexora/__init__.py +129 -0
- plexora/__main__.py +15 -0
- plexora/_url.py +105 -0
- plexora/api/__init__.py +55 -0
- plexora/api/dataset.py +398 -0
- plexora/api/http.py +19 -0
- plexora/api/plugin.py +541 -0
- plexora/api/store.py +221 -0
- plexora/cli.py +777 -0
- plexora/client/dist/354_bundle.js +2 -0
- plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
- plexora/client/dist/418_bundle.js +2 -0
- plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
- plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
- plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
- plexora/client/dist/770_bundle.js +1 -0
- plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
- plexora/client/dist/vendor_bundle.js +2 -0
- plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
- plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
- plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
- plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
- plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
- plexora/client/src/css/import.css +474 -0
- plexora/client/src/css/main.css +903 -0
- plexora/client/src/css/openProject.css +363 -0
- plexora/client/src/css/quickView.css +166 -0
- plexora/client/src/css/tokens.css +51 -0
- plexora/client/src/css/viewer.css +1655 -0
- plexora/client/src/img/apple-touch-icon.png +0 -0
- plexora/client/src/img/favicon.ico +0 -0
- plexora/client/src/img/logo.ai +8892 -33
- plexora/client/src/img/logo.svg +1 -0
- plexora/client/src/img/logo_with_text.ai +9011 -34
- plexora/client/src/img/logo_with_text.svg +1 -0
- plexora/client/src/js/main.js +704 -0
- plexora/client/src/js/pluginRegistry.js +108 -0
- plexora/client/src/js/services/appStatus.js +286 -0
- plexora/client/src/js/services/browsePicker.js +73 -0
- plexora/client/src/js/services/dataLayer.js +400 -0
- plexora/client/src/js/services/datasetContext.js +143 -0
- plexora/client/src/js/services/glRenderer.js +143 -0
- plexora/client/src/js/services/importFormValidation.js +257 -0
- plexora/client/src/js/services/numericData.js +100 -0
- plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
- plexora/client/src/js/services/simpleEventHandler.js +25 -0
- plexora/client/src/js/vendor.js +29 -0
- plexora/client/src/js/views/channelList.js +663 -0
- plexora/client/src/js/views/colorSwatchPicker.js +202 -0
- plexora/client/src/js/views/columnClassifier.js +146 -0
- plexora/client/src/js/views/coordinateField.js +133 -0
- plexora/client/src/js/views/dataSourceField.js +276 -0
- plexora/client/src/js/views/imageViewer.js +3306 -0
- plexora/client/src/js/views/miniMap.js +787 -0
- plexora/client/src/js/views/navbarControls.js +178 -0
- plexora/client/src/js/views/openProjectPage.js +239 -0
- plexora/client/src/js/views/projectEdit.js +335 -0
- plexora/client/src/js/views/quickViewLanding.js +136 -0
- plexora/client/src/js/views/rainbow.js +326 -0
- plexora/client/src/js/views/requirementsModal.js +572 -0
- plexora/client/src/js/views/rgbImageViewer.js +115 -0
- plexora/client/src/js/views/searchableSelect.js +411 -0
- plexora/client/src/js/views/segmentationProgress.js +122 -0
- plexora/client/src/js/views/toolLoader.js +747 -0
- plexora/client/src/js/views/viewerControls.js +689 -0
- plexora/client/src/js/views/viewerManager.js +434 -0
- plexora/client/src/js/views/viewerSidebar.js +1117 -0
- plexora/client/src/js/workers/tileDecoder.js +130 -0
- plexora/client/src/shaders/frag.glsl +528 -0
- plexora/client/src/shaders/vert.glsl +12 -0
- plexora/client/templates/base.html +212 -0
- plexora/client/templates/index.html +238 -0
- plexora/client/templates/open_project.html +96 -0
- plexora/client/templates/project_columns.html +86 -0
- plexora/client/templates/project_edit.html +196 -0
- plexora/client/templates/upload.html +118 -0
- plexora/connect.py +528 -0
- plexora/datasource.py +782 -0
- plexora/jupyter.py +406 -0
- plexora/notebook_env.py +232 -0
- plexora/paths.py +486 -0
- plexora/plugins/__init__.py +12 -0
- plexora/plugins/cell_explorer/__init__.py +100 -0
- plexora/plugins/cell_explorer/server/__init__.py +11 -0
- plexora/plugins/cell_explorer/server/routes.py +167 -0
- plexora/plugins/cell_explorer/server/state.py +281 -0
- plexora/plugins/cell_explorer/server/values.py +145 -0
- plexora/plugins/cell_explorer/server/variables.py +367 -0
- plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
- plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
- plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
- plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
- plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
- plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
- plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
- plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
- plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
- plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
- plexora/plugins/figure_builder/__init__.py +125 -0
- plexora/plugins/figure_builder/server/__init__.py +7 -0
- plexora/plugins/figure_builder/server/compose.py +365 -0
- plexora/plugins/figure_builder/server/export.py +784 -0
- plexora/plugins/figure_builder/server/export_jobs.py +148 -0
- plexora/plugins/figure_builder/server/operations.py +556 -0
- plexora/plugins/figure_builder/server/pixels.py +142 -0
- plexora/plugins/figure_builder/server/provenance.py +174 -0
- plexora/plugins/figure_builder/server/render.py +278 -0
- plexora/plugins/figure_builder/server/repository.py +701 -0
- plexora/plugins/figure_builder/server/routes.py +594 -0
- plexora/plugins/figure_builder/server/schema.py +1023 -0
- plexora/plugins/figure_builder/server/sources.py +120 -0
- plexora/plugins/figure_builder/server/textmetrics.py +155 -0
- plexora/plugins/figure_builder/static/figureActions.js +363 -0
- plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
- plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
- plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
- plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
- plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
- plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
- plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
- plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
- plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
- plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
- plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
- plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
- plexora/plugins/figure_builder/static/figureRichText.js +634 -0
- plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
- plexora/plugins/figure_builder/static/figureSchema.js +218 -0
- plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
- plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
- plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
- plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
- plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
- plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
- plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
- plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
- plexora/plugins/gating/__init__.py +76 -0
- plexora/plugins/gating/server/__init__.py +0 -0
- plexora/plugins/gating/server/anndata_gates.py +367 -0
- plexora/plugins/gating/server/database.py +16 -0
- plexora/plugins/gating/server/model.py +320 -0
- plexora/plugins/gating/server/routes.py +235 -0
- plexora/plugins/gating/static/csvGatingList.js +947 -0
- plexora/plugins/gating/static/gating.css +289 -0
- plexora/plugins/gating/static/gatingApi.js +221 -0
- plexora/plugins/gating/static/gatingSidebarController.js +513 -0
- plexora/plugins/gating/templates/gating/legacy.html +26 -0
- plexora/plugins/gating/templates/gating/panel.html +69 -0
- plexora/plugins/roi/__init__.py +95 -0
- plexora/plugins/roi/server/__init__.py +8 -0
- plexora/plugins/roi/server/adapters.py +655 -0
- plexora/plugins/roi/server/geojson.py +267 -0
- plexora/plugins/roi/server/geometry.py +181 -0
- plexora/plugins/roi/server/mapping.py +169 -0
- plexora/plugins/roi/server/operations.py +293 -0
- plexora/plugins/roi/server/repository.py +215 -0
- plexora/plugins/roi/server/routes.py +409 -0
- plexora/plugins/roi/server/schema.py +323 -0
- plexora/plugins/roi/static/roi.css +583 -0
- plexora/plugins/roi/static/roiApi.js +177 -0
- plexora/plugins/roi/static/roiFigureBridge.js +107 -0
- plexora/plugins/roi/static/roiGeometry.js +350 -0
- plexora/plugins/roi/static/roiRenderer.js +278 -0
- plexora/plugins/roi/static/roiSidebarController.js +1038 -0
- plexora/plugins/roi/static/roiState.js +518 -0
- plexora/plugins/roi/static/roiTools.js +993 -0
- plexora/plugins/roi/templates/roi/panel.html +240 -0
- plexora/proxy.py +57 -0
- plexora/server/models/adapters/__init__.py +94 -0
- plexora/server/models/adapters/anndata_adapter.py +388 -0
- plexora/server/models/adapters/base.py +93 -0
- plexora/server/models/adapters/classify.py +192 -0
- plexora/server/models/adapters/csv_adapter.py +105 -0
- plexora/server/models/adapters/inspection.py +265 -0
- plexora/server/models/adapters/spatialdata_adapter.py +248 -0
- plexora/server/models/centroid_tiles.py +305 -0
- plexora/server/models/data_model.py +1659 -0
- plexora/server/models/database_model.py +169 -0
- plexora/server/models/project.py +1270 -0
- plexora/server/plugins.py +199 -0
- plexora/server/routes/browse_routes.py +41 -0
- plexora/server/routes/data_routes.py +294 -0
- plexora/server/routes/import_routes.py +603 -0
- plexora/server/routes/page_routes.py +139 -0
- plexora/server/routes/project_routes.py +438 -0
- plexora/server/routes/quick_view_routes.py +62 -0
- plexora/server/routes/system_routes.py +39 -0
- plexora/server/routes/tool_routes.py +418 -0
- plexora/server/utils/addHEColumns.py +11 -0
- plexora/server/utils/fast_png.py +97 -0
- plexora/server/utils/fullConversion.py +50 -0
- plexora/server/utils/native_dialog.py +168 -0
- plexora/server/utils/pre_normalization.py +47 -0
- plexora/server/utils/segmentation_pyramid.py +598 -0
- plexora/server/utils/smallestenclosingcircle.py +126 -0
- plexora/server/utils/tiffsurgeon.py +372 -0
- plexora/server_cli.py +48 -0
- plexora/test.py +0 -0
- plexora-0.0.1.dist-info/METADATA +341 -0
- plexora-0.0.1.dist-info/RECORD +248 -0
- plexora-0.0.1.dist-info/WHEEL +5 -0
- plexora-0.0.1.dist-info/entry_points.txt +12 -0
- plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
- plexora-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class GatingSidebarController - gating module's sidebar extension: gate-marker
|
|
3
|
+
* selection, threshold slider/distribution plot, and "Save Gates to AnnData". Composed
|
|
4
|
+
* against the core ViewerSidebar's public API rather than being part of it -- see
|
|
5
|
+
* setGateMarker() below for the one confirmed cross-wire (mirroring the active gate
|
|
6
|
+
* marker into channel slot 1 via sidebar.setSlotMarker()), which stays an explicit,
|
|
7
|
+
* opportunistic call from the gating side into a stable core API, not the reverse.
|
|
8
|
+
*
|
|
9
|
+
* Registered with the core sidebar via ViewerSidebar#registerModule() -- see
|
|
10
|
+
* pluginRegistry.js's `createSidebarController(ctx)` hook and its call site in main.js.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Datasource kinds whose gates can be written back to the source file, i.e.
|
|
14
|
+
// those backed by an AnnData group: an .h5ad, or the one table a SpatialData
|
|
15
|
+
// import selected out of its .zarr store. Must stay in step with the same
|
|
16
|
+
// check in the gating plugin's routes.py. CSV has nowhere to write to.
|
|
17
|
+
const SAVEABLE_SOURCE_TYPES = ["anndata", "spatialdata"];
|
|
18
|
+
|
|
19
|
+
class GatingSidebarController {
|
|
20
|
+
constructor(ctx) {
|
|
21
|
+
this.ctx = ctx;
|
|
22
|
+
this.sidebar = ctx.sidebar;
|
|
23
|
+
this.gatingList = ctx.moduleInstance;
|
|
24
|
+
this.dataLayer = ctx.dataLayer;
|
|
25
|
+
this.api = new GatingApi(ctx);
|
|
26
|
+
this.eventHandler = ctx.eventHandler;
|
|
27
|
+
this.config = ctx.config;
|
|
28
|
+
this.gateMarker = null;
|
|
29
|
+
this.gateSlider = null;
|
|
30
|
+
this.gateMarkerSelect = null;
|
|
31
|
+
this.gateMarkerChangeTimer = null;
|
|
32
|
+
this._saveGatingTimer = null;
|
|
33
|
+
this._gatingSaveChain = null;
|
|
34
|
+
this.gateDistributionScale = null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Called once from ViewerSidebar#init(), before the saved-state restore below.
|
|
38
|
+
setup() {
|
|
39
|
+
this.populateGateSelect();
|
|
40
|
+
this.bindSaveToAnndata();
|
|
41
|
+
|
|
42
|
+
// Thresholding is opened via the Tools menu (?tool=gating, see
|
|
43
|
+
// tool_routes.py / toolLoader.js). Closing just hides the panel --
|
|
44
|
+
// toolLoader.js owns the show/hide bookkeeping (and knows whether this
|
|
45
|
+
// was a lazy client-side open or a direct/bookmarked server-rendered
|
|
46
|
+
// one); this controller and its DOM stay alive so reopening later in
|
|
47
|
+
// the same session is instant, no re-fetch/re-init.
|
|
48
|
+
document.getElementById("gate_marker_close")?.addEventListener("click", () => {
|
|
49
|
+
window.PlexoraToolLoader?.hideToolPanel("gating");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const gateAuto = document.getElementById("gate_auto_button");
|
|
53
|
+
gateAuto.addEventListener("click", async () => {
|
|
54
|
+
gateAuto.disabled = true;
|
|
55
|
+
gateAuto.classList.add("auto-loading");
|
|
56
|
+
try {
|
|
57
|
+
await this.autoGate();
|
|
58
|
+
} finally {
|
|
59
|
+
gateAuto.disabled = false;
|
|
60
|
+
gateAuto.classList.remove("auto-loading");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Registered through the plugin's cleanup list so deactivating the
|
|
65
|
+
// plugin detaches it; this was previously a permanent window listener.
|
|
66
|
+
const onResize = () => this.redrawGateSlider();
|
|
67
|
+
window.addEventListener("resize", onResize);
|
|
68
|
+
this.ctx?.onCleanup?.(() => window.removeEventListener("resize", onResize));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Called by toolLoader.js right after unhiding the panel (both on first lazy
|
|
72
|
+
// load and on every reopen). The slider/distribution plot measure their own
|
|
73
|
+
// width via getBoundingClientRect() (redrawGateSlider/drawGateDistribution
|
|
74
|
+
// below), which returns 0 while the panel is display:none -- redraw now that
|
|
75
|
+
// it's actually visible so they don't render collapsed to zero width.
|
|
76
|
+
onShow() {
|
|
77
|
+
this.redrawGateSlider();
|
|
78
|
+
this.drawGateDistribution();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ViewerSidebar#init() restore-flow hooks (see registerModule()'s doc comment).
|
|
82
|
+
fetchSaved() {
|
|
83
|
+
return this.api.getSavedGatingList();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async applyOrDefault(savedGating) {
|
|
87
|
+
if (savedGating && savedGating.length) {
|
|
88
|
+
this.applySavedGating(savedGating);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// No gates saved in Plexora's own DB yet -- for an AnnData-backed
|
|
92
|
+
// datasource, check whether adata.uns[table_name] already has gates
|
|
93
|
+
// from outside Plexora (e.g. set on the source file before import)
|
|
94
|
+
// before falling back to a blank default marker. Wrapped: unlike
|
|
95
|
+
// the old fully-synchronous version, a rejected fetch here (network
|
|
96
|
+
// hiccup, stale-cached dataLayer.js missing the method, unexpected
|
|
97
|
+
// response shape) must still fall through to the default marker
|
|
98
|
+
// below -- not leave the sidebar with nothing selected at all.
|
|
99
|
+
if (SAVEABLE_SOURCE_TYPES.includes(this.ctx.dataset?.table?.sourceKind)) {
|
|
100
|
+
try {
|
|
101
|
+
const gates = await this.api.getGatesFromAnndata();
|
|
102
|
+
if (gates && Object.keys(gates).length) {
|
|
103
|
+
this.applyAnndataGates(gates);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Error loading gates from AnnData", error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
this.setGateMarker(this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0], { enableSlot: false });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Seeds gating_channels with previously-saved AnnData gates (lower bound
|
|
114
|
+
// only -- see anndata_gates.load_gates_from_anndata) so the marker
|
|
115
|
+
// dropdown's gated indicator and slider reflect prior work done outside
|
|
116
|
+
// Plexora as soon as the tool opens. Each channel's own current max
|
|
117
|
+
// stands in for the missing upper bound. Marker selection still defers
|
|
118
|
+
// to the normal default -- if that default happens to be a gated
|
|
119
|
+
// channel its slider already reflects the loaded value; other gated
|
|
120
|
+
// channels show via the dropdown's gated-indicator dot until picked.
|
|
121
|
+
applyAnndataGates(gates) {
|
|
122
|
+
for (const [channel, lowerBound] of Object.entries(gates)) {
|
|
123
|
+
const range = this.getGateRange(channel);
|
|
124
|
+
this.gatingList.gating_channels[channel] = [lowerBound, range[1]];
|
|
125
|
+
}
|
|
126
|
+
this.setGateMarker(this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0], { enableSlot: false });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
persistIfNeeded(hadSaved) {
|
|
130
|
+
if (!hadSaved) this.persistGatingList();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Wires the "Save Gates to AnnData" button/panel (adata.uns[table_name],
|
|
134
|
+
// lower gate bound only, one column per image -- see anndata_gates.py).
|
|
135
|
+
// Only meaningful for AnnData-backed datasources. The gates payload is
|
|
136
|
+
// computed fresh from getCustomGatedChannels() at click time -- not from
|
|
137
|
+
// the persisted GatingList row -- so every marker the user has actually
|
|
138
|
+
// gated is included, not just whichever one is currently on screen.
|
|
139
|
+
bindSaveToAnndata() {
|
|
140
|
+
const button = document.getElementById("save_gates_anndata_button");
|
|
141
|
+
const panel = document.getElementById("gating_save_anndata_panel");
|
|
142
|
+
const confirmButton = document.getElementById("save_anndata_confirm");
|
|
143
|
+
const cancelButton = document.getElementById("save_anndata_cancel");
|
|
144
|
+
const exitButton = document.getElementById("save_anndata_exit");
|
|
145
|
+
const tableNameInput = document.getElementById("save_anndata_table_name");
|
|
146
|
+
const status = document.getElementById("save_anndata_status");
|
|
147
|
+
|
|
148
|
+
if (SAVEABLE_SOURCE_TYPES.includes(this.ctx.dataset?.table?.sourceKind)) {
|
|
149
|
+
button.hidden = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One way in, several ways out. The panel opens over the bottom of a
|
|
153
|
+
// scrolling sidebar, so its dismiss control has to be visible from the
|
|
154
|
+
// moment it opens rather than below the fold with the action row -- and
|
|
155
|
+
// once a save has succeeded "Cancel" reads like it would undo it.
|
|
156
|
+
const closePanel = () => {
|
|
157
|
+
panel.hidden = true;
|
|
158
|
+
cancelButton.textContent = "Cancel";
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
button.addEventListener("click", () => {
|
|
162
|
+
status.textContent = "";
|
|
163
|
+
status.className = "";
|
|
164
|
+
cancelButton.textContent = "Cancel";
|
|
165
|
+
panel.hidden = !panel.hidden;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
cancelButton.addEventListener("click", closePanel);
|
|
169
|
+
exitButton.addEventListener("click", closePanel);
|
|
170
|
+
|
|
171
|
+
panel.addEventListener("keydown", (event) => {
|
|
172
|
+
if (event.key === "Escape") {
|
|
173
|
+
event.stopPropagation();
|
|
174
|
+
closePanel();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
confirmButton.addEventListener("click", async () => {
|
|
179
|
+
confirmButton.disabled = true;
|
|
180
|
+
status.className = "";
|
|
181
|
+
status.textContent = "Saving...";
|
|
182
|
+
try {
|
|
183
|
+
// The server derives gates from the persisted GatingList row
|
|
184
|
+
// (the DB), not from anything sent here -- flush the current
|
|
185
|
+
// in-memory state first so a gate set moments ago (still
|
|
186
|
+
// inside the 400ms autosave debounce) isn't missed.
|
|
187
|
+
await this.api.saveGatingList(this.gatingList.gating_channels, this.gatingList.selections);
|
|
188
|
+
const result = await this.saveWithImageId(tableNameInput.value.trim() || "gates");
|
|
189
|
+
status.className = "success";
|
|
190
|
+
status.textContent = `Saved column "${result.image_id}" (${result.n_active_gates} markers).`;
|
|
191
|
+
// Nothing left to cancel -- the write already happened.
|
|
192
|
+
cancelButton.textContent = "Close";
|
|
193
|
+
} catch (error) {
|
|
194
|
+
status.className = "error";
|
|
195
|
+
status.textContent = error.message || "Failed to save gates to AnnData";
|
|
196
|
+
} finally {
|
|
197
|
+
confirmButton.disabled = false;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Save, asking the host for an image-id column if the project has not
|
|
204
|
+
* recorded one.
|
|
205
|
+
*
|
|
206
|
+
* This plugin declares `role:image_id` as an optional requirement, so core
|
|
207
|
+
* owns collecting and storing it -- one column name, asked once, and every
|
|
208
|
+
* other plugin then finds it already answered. The panel used to carry its
|
|
209
|
+
* own free-text box defaulting to the literal "imageid", which asked the
|
|
210
|
+
* user for something the host already had a place to keep.
|
|
211
|
+
*/
|
|
212
|
+
async saveWithImageId(tableName) {
|
|
213
|
+
try {
|
|
214
|
+
return await this.api.saveGatesToAnndata(tableName);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (!/image ID column/i.test(error.message || "")) throw error;
|
|
217
|
+
const collected = await this.ctx.requirements.require(["role:image_id"]);
|
|
218
|
+
if (!collected) throw error;
|
|
219
|
+
return this.api.saveGatesToAnndata(tableName);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
populateGateSelect() {
|
|
224
|
+
const names = this.getGateMarkerNames();
|
|
225
|
+
const mount = document.getElementById("gate_marker_select");
|
|
226
|
+
if (!this.gateMarkerSelect) {
|
|
227
|
+
this.gateMarkerSelect = new SearchableSelect(mount, {
|
|
228
|
+
options: names,
|
|
229
|
+
value: this.gateMarker || "",
|
|
230
|
+
placeholder: "Search markers…",
|
|
231
|
+
getIndicator: (name) => this.describeGateIndicator(name),
|
|
232
|
+
onChange: (name) => {
|
|
233
|
+
window.clearTimeout(this.gateMarkerChangeTimer);
|
|
234
|
+
this.gateMarkerChangeTimer = window.setTimeout(() => {
|
|
235
|
+
this.setGateMarker(name);
|
|
236
|
+
}, 0);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
this.gateMarkerSelect.setOptions(names);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
getGateMarkerNames() {
|
|
245
|
+
// sidebar.columns is the image channel list -- gate-able markers are
|
|
246
|
+
// the feature table's own columns (e.g. adata.var_names), which are
|
|
247
|
+
// frequently a different set of strings entirely. CSVGatingList holds
|
|
248
|
+
// exactly that list (see its init, which takes the project's recorded
|
|
249
|
+
// marker/metadata split off ctx.dataset), so reuse it here rather than
|
|
250
|
+
// deriving a second answer that could disagree with the sliders.
|
|
251
|
+
return [...this.gatingList.columns];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setGateMarker(name, options = {}) {
|
|
255
|
+
if (!name) return;
|
|
256
|
+
if (name === this.gateMarker && !options.force) return;
|
|
257
|
+
const enableSlot = options.enableSlot !== false;
|
|
258
|
+
this.gateMarker = name;
|
|
259
|
+
if (this.gateMarkerSelect) {
|
|
260
|
+
this.gateMarkerSelect.setValue(name);
|
|
261
|
+
}
|
|
262
|
+
this.ensureGateSelection(name);
|
|
263
|
+
this.redrawGateSlider();
|
|
264
|
+
this.drawGateDistribution();
|
|
265
|
+
// Gating always works off the feature-table column (ensureGateSelection
|
|
266
|
+
// above), independent of the image -- a gate marker is very often not
|
|
267
|
+
// an image channel at all (adata.var_names vs. the image's channel
|
|
268
|
+
// names are frequently different strings/lengths). Only mirror the
|
|
269
|
+
// marker into a rendering slot when its name genuinely matches a real
|
|
270
|
+
// image channel; otherwise leave the channel section untouched and let
|
|
271
|
+
// the user find and enable the right channel themselves to verify the
|
|
272
|
+
// gate visually. The correct check is against the image channel
|
|
273
|
+
// vocabulary (imageChannels, keyed by full channel name -> tile
|
|
274
|
+
// index), not the marker vocabulary.
|
|
275
|
+
const hasMatchingImageChannel = this.ctx.dataset.image.has(this.dataLayer.getFullChannelName(name));
|
|
276
|
+
if (options.syncSlot !== false && hasMatchingImageChannel) {
|
|
277
|
+
this.sidebar.setSlotMarker(1, name, { keepColor: true, enable: enableSlot, reveal: enableSlot });
|
|
278
|
+
}
|
|
279
|
+
this.scheduleSaveGating();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
ensureGateSelection(name) {
|
|
283
|
+
const fullName = this.dataLayer.getFullChannelName(name);
|
|
284
|
+
const range = this.gatingList.gating_channels[fullName] || this.getGateRange(name);
|
|
285
|
+
this.gatingList.selections = {};
|
|
286
|
+
this.gatingList.gating_channels[fullName] = range;
|
|
287
|
+
this.gatingList.selections[fullName] = range;
|
|
288
|
+
this.updateGateReadout(range);
|
|
289
|
+
this.eventHandler.trigger(CSVGatingList.events.GATING_BRUSH_MOVE, this.gatingList.selections);
|
|
290
|
+
this.eventHandler.trigger(CSVGatingList.events.SELECTION_CHANGED, this.gatingList.selections);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
redrawGateSlider() {
|
|
294
|
+
if (!this.gateMarker) return;
|
|
295
|
+
const target = document.getElementById("gate_slider");
|
|
296
|
+
target.innerHTML = "";
|
|
297
|
+
const range = this.getGateRange(this.gateMarker);
|
|
298
|
+
const values = this.gatingList.gating_channels[this.dataLayer.getFullChannelName(this.gateMarker)] || range;
|
|
299
|
+
const width = Math.max(180, target.getBoundingClientRect().width - 16);
|
|
300
|
+
const slider = d3.sliderBottom()
|
|
301
|
+
.min(range[0])
|
|
302
|
+
.max(range[1])
|
|
303
|
+
.width(width)
|
|
304
|
+
.ticks(0)
|
|
305
|
+
.tickValues([])
|
|
306
|
+
.default(values)
|
|
307
|
+
.fill("#f36f45")
|
|
308
|
+
.handle(d3.symbol().type(d3.symbolCircle).size(120))
|
|
309
|
+
.on("onchange", (value) => this.setGateRange(value, CSVGatingList.events.GATING_BRUSH_MOVE))
|
|
310
|
+
.on("end", (value) => this.setGateRange(value, CSVGatingList.events.SELECTION_CHANGED));
|
|
311
|
+
|
|
312
|
+
this.gateSlider = slider;
|
|
313
|
+
d3.select(target)
|
|
314
|
+
.append("svg")
|
|
315
|
+
.attr("width", width + 16)
|
|
316
|
+
.attr("height", 44)
|
|
317
|
+
.append("g")
|
|
318
|
+
.attr("transform", "translate(8,18)")
|
|
319
|
+
.call(slider);
|
|
320
|
+
this.updateGateReadout(values);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
setGateRange(values, eventName) {
|
|
324
|
+
const fullName = this.dataLayer.getFullChannelName(this.gateMarker);
|
|
325
|
+
const normalized = this.normalizeGateRange(values, this.getGateRange(this.gateMarker));
|
|
326
|
+
this.gatingList.gating_channels[fullName] = normalized;
|
|
327
|
+
this.gatingList.selections = {};
|
|
328
|
+
this.gatingList.selections[fullName] = normalized;
|
|
329
|
+
this.updateGateReadout(normalized);
|
|
330
|
+
this.updateGateThresholdLines(normalized);
|
|
331
|
+
this.eventHandler.trigger(eventName, this.gatingList.selections);
|
|
332
|
+
if (eventName === CSVGatingList.events.SELECTION_CHANGED) {
|
|
333
|
+
this.scheduleSaveGating();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async autoGate() {
|
|
338
|
+
if (!this.gateMarker) return;
|
|
339
|
+
if (!(this.gateMarker in this.gatingList.hasGatingGMM)) {
|
|
340
|
+
await this.gatingList.getGatingGMM(this.gateMarker);
|
|
341
|
+
}
|
|
342
|
+
const packet = this.gatingList.hasGatingGMM[this.gateMarker];
|
|
343
|
+
if (!packet || packet.gate === undefined) return;
|
|
344
|
+
const range = this.getGateRange(this.gateMarker);
|
|
345
|
+
const factor = Math.pow(10, this.dataLayer.gateDecimals(range));
|
|
346
|
+
const gate = Math.floor(parseFloat(packet.gate) * factor) / factor;
|
|
347
|
+
const values = [gate, range[1]];
|
|
348
|
+
if (this.gateSlider) {
|
|
349
|
+
this.gateSlider.silentValue(values);
|
|
350
|
+
}
|
|
351
|
+
this.setGateRange(values, CSVGatingList.events.SELECTION_CHANGED);
|
|
352
|
+
this.redrawGateSlider();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
drawGateDistribution() {
|
|
356
|
+
const target = document.getElementById("gate_distribution_plot");
|
|
357
|
+
target.innerHTML = "";
|
|
358
|
+
this.gateDistributionScale = null;
|
|
359
|
+
if (!this.gateMarker) return;
|
|
360
|
+
const fullName = this.dataLayer.getFullChannelName(this.gateMarker);
|
|
361
|
+
const desc = this.sidebar.databaseDescription[fullName];
|
|
362
|
+
const histogram = desc?.histogram || [];
|
|
363
|
+
if (!histogram.length) return;
|
|
364
|
+
|
|
365
|
+
const box = target.getBoundingClientRect();
|
|
366
|
+
const width = Math.max(220, box.width || 280);
|
|
367
|
+
const height = 120;
|
|
368
|
+
const margin = { top: 12, right: 10, bottom: 24, left: 28 };
|
|
369
|
+
const innerWidth = width - margin.left - margin.right;
|
|
370
|
+
const innerHeight = height - margin.top - margin.bottom;
|
|
371
|
+
const xDomain = d3.extent(histogram, (d) => d.x);
|
|
372
|
+
const yMax = d3.max(histogram, (d) => d.y);
|
|
373
|
+
const xScale = d3.scaleLinear().domain(xDomain).range([0, innerWidth]);
|
|
374
|
+
const yScale = d3.scaleLinear().domain([0, yMax]).range([innerHeight, 0]);
|
|
375
|
+
const line = d3.line()
|
|
376
|
+
.x((d) => xScale(d.x))
|
|
377
|
+
.y((d) => yScale(d.y))
|
|
378
|
+
.curve(d3.curveMonotoneX);
|
|
379
|
+
const values = this.gatingList.gating_channels[fullName] || this.getGateRange(this.gateMarker);
|
|
380
|
+
|
|
381
|
+
const svg = d3.select(target)
|
|
382
|
+
.append("svg")
|
|
383
|
+
.attr("width", width)
|
|
384
|
+
.attr("height", height);
|
|
385
|
+
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
|
|
386
|
+
g.append("path")
|
|
387
|
+
.datum(histogram)
|
|
388
|
+
.attr("class", "sidebar-distribution-line")
|
|
389
|
+
.attr("d", line);
|
|
390
|
+
g.append("g")
|
|
391
|
+
.attr("class", "gate-threshold-lines")
|
|
392
|
+
.selectAll("line")
|
|
393
|
+
.data(values)
|
|
394
|
+
.enter()
|
|
395
|
+
.append("line")
|
|
396
|
+
.attr("class", "gate-threshold-line")
|
|
397
|
+
.attr("x1", (value) => xScale(value))
|
|
398
|
+
.attr("x2", (value) => xScale(value))
|
|
399
|
+
.attr("y1", 0)
|
|
400
|
+
.attr("y2", innerHeight);
|
|
401
|
+
g.append("g")
|
|
402
|
+
.attr("class", "distribution-axis")
|
|
403
|
+
.attr("transform", `translate(0,${innerHeight})`)
|
|
404
|
+
.call(d3.axisBottom(xScale).ticks(3).tickFormat(d3.format(".2f")));
|
|
405
|
+
|
|
406
|
+
this.gateDistributionScale = xScale;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Cheap per-tick update during a drag: reposition the existing threshold lines instead of
|
|
410
|
+
// tearing down and rebuilding the whole histogram/axis (which was the source of drag lag).
|
|
411
|
+
updateGateThresholdLines(values) {
|
|
412
|
+
if (!this.gateDistributionScale) {
|
|
413
|
+
this.drawGateDistribution();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const xScale = this.gateDistributionScale;
|
|
417
|
+
d3.select("#gate_distribution_plot")
|
|
418
|
+
.selectAll(".gate-threshold-line")
|
|
419
|
+
.data(values)
|
|
420
|
+
.attr("x1", (value) => xScale(value))
|
|
421
|
+
.attr("x2", (value) => xScale(value));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
applySavedGating(rows) {
|
|
425
|
+
let activeRow = null;
|
|
426
|
+
rows.forEach((row) => {
|
|
427
|
+
if (!row || !row.channel || row.channel === "Lasso") return;
|
|
428
|
+
this.gatingList.gating_channels[row.channel] = [row.gate_start, row.gate_end];
|
|
429
|
+
if (row.gate_active) {
|
|
430
|
+
activeRow = row;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
const marker = activeRow
|
|
434
|
+
? this.dataLayer.getShortChannelName(activeRow.channel)
|
|
435
|
+
: (this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0]);
|
|
436
|
+
// syncSlot:false - applySavedChannels already placed every active channel (including
|
|
437
|
+
// this one, if it was active) in its correct restored slot; letting setGateMarker's
|
|
438
|
+
// normal slot-1 mirroring run here would clobber whatever channel actually belongs there.
|
|
439
|
+
this.setGateMarker(marker, { force: true, syncSlot: false });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
persistGatingList() {
|
|
443
|
+
return this.api.saveGatingList(this.gatingList.gating_channels, this.gatingList.selections);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
scheduleSaveGating() {
|
|
447
|
+
if (this.sidebar.isRestoring()) return;
|
|
448
|
+
window.clearTimeout(this._saveGatingTimer);
|
|
449
|
+
this._saveGatingTimer = window.setTimeout(() => {
|
|
450
|
+
this._gatingSaveChain = (this._gatingSaveChain || Promise.resolve()).then(() => this.persistGatingList());
|
|
451
|
+
}, 400);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
updateGateReadout(values) {
|
|
455
|
+
document.getElementById("gate_min_value").textContent = this.sidebar.formatValue(values[0]);
|
|
456
|
+
document.getElementById("gate_max_value").textContent = this.sidebar.formatValue(values[1]);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
getGateRange(name) {
|
|
460
|
+
const fullName = this.dataLayer.getFullChannelName(name);
|
|
461
|
+
const desc = this.sidebar.databaseDescription[fullName] || {};
|
|
462
|
+
return [desc.min || 0, desc.max || 1];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// A marker only counts as "gated" once its stored range differs from its
|
|
466
|
+
// own full data range (getGateRange) -- ensureGateSelection() seeds every
|
|
467
|
+
// marker the user merely browses to in the dropdown with that same full
|
|
468
|
+
// range, so membership in gating_channels alone would over-count markers
|
|
469
|
+
// nobody actually narrowed.
|
|
470
|
+
hasCustomGate(name) {
|
|
471
|
+
const fullName = this.dataLayer.getFullChannelName(name);
|
|
472
|
+
const range = this.gatingList.gating_channels[fullName];
|
|
473
|
+
if (!range) return false;
|
|
474
|
+
const defaultRange = this.getGateRange(name);
|
|
475
|
+
return range[0] !== defaultRange[0] || range[1] !== defaultRange[1];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// {fullChannelName: [low, high]} for every marker with a real, user-set
|
|
479
|
+
// gate -- independent of gatingList.selections, which only ever holds
|
|
480
|
+
// the single currently-displayed marker (ensureGateSelection() resets it
|
|
481
|
+
// on every marker switch, by design, for the live single-marker slider/
|
|
482
|
+
// segmentation-outline view). Used for exports (e.g. save-to-AnnData)
|
|
483
|
+
// that need *all* gated markers, not just the one on screen right now.
|
|
484
|
+
getCustomGatedChannels() {
|
|
485
|
+
const result = {};
|
|
486
|
+
for (const name of this.gatingList.columns) {
|
|
487
|
+
if (!this.hasCustomGate(name)) continue;
|
|
488
|
+
const fullName = this.dataLayer.getFullChannelName(name);
|
|
489
|
+
result[fullName] = this.gatingList.gating_channels[fullName];
|
|
490
|
+
}
|
|
491
|
+
return result;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Hover-tooltip text for the marker dropdown's gated-indicator dot; null
|
|
495
|
+
// means "don't show a dot" (SearchableSelect skips rendering it).
|
|
496
|
+
describeGateIndicator(name) {
|
|
497
|
+
if (!this.hasCustomGate(name)) return null;
|
|
498
|
+
const fullName = this.dataLayer.getFullChannelName(name);
|
|
499
|
+
const range = this.gatingList.gating_channels[fullName];
|
|
500
|
+
return `Gated ${this.sidebar.formatValue(range[0])}–${this.sidebar.formatValue(range[1])}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Gate-specific rounding: precision is derived from the channel's own
|
|
504
|
+
// observed range (dataLayer.gateDecimals) instead of the isTransformed
|
|
505
|
+
// config flag, so it can't silently drift out of sync with the data's
|
|
506
|
+
// actual scale. Floors the low handle / ceils the high handle (at that
|
|
507
|
+
// precision) so rounding never excludes boundary cells.
|
|
508
|
+
normalizeGateRange(values, range) {
|
|
509
|
+
const sorted = [...values].map((value) => parseFloat(value)).sort((a, b) => a - b);
|
|
510
|
+
const factor = Math.pow(10, this.dataLayer.gateDecimals(range));
|
|
511
|
+
return [Math.floor(sorted[0] * factor) / factor, Math.ceil(sorted[1] * factor) / factor];
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<div id="csv_channel_list_wrapper">
|
|
2
|
+
<div id="lowRow3" class="row">
|
|
3
|
+
<div id="boxHeading3" class="col-auto">
|
|
4
|
+
<span class="box-heading">CSV Gating</span>
|
|
5
|
+
</div>
|
|
6
|
+
<div id="download-gating-header-div"></div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="list" id="csv_gating_list">
|
|
10
|
+
<div id="csv_gating_row" class="row">
|
|
11
|
+
<div class="col-auto">
|
|
12
|
+
<div class="lds-ellipsis" id="csv_gating_list_loader">
|
|
13
|
+
<div></div>
|
|
14
|
+
<div></div>
|
|
15
|
+
<div></div>
|
|
16
|
+
<div></div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<div id="drag-and-drop-info">
|
|
21
|
+
<div class="d-flex justify-content-md-center align-items-center vh-100">
|
|
22
|
+
<span id="drag-and-drop-text">Drag and Drop A Gating CSV File</span>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<section class="sidebar-section" id="gate_marker_section">
|
|
2
|
+
<div class="section-heading">
|
|
3
|
+
<h2><span class="fas fa-sliders section-heading-icon"></span>Gate Marker</h2>
|
|
4
|
+
<div class="section-actions">
|
|
5
|
+
<button id="gating_upload_icon" class="icon-button" title="Load gating from CSV" type="button">
|
|
6
|
+
<span class="fas fa-file-arrow-up"></span>
|
|
7
|
+
</button>
|
|
8
|
+
<button id="gating_download_icon" class="icon-button" title="Download gating CSV" type="button">
|
|
9
|
+
<span class="fas fa-file-arrow-down"></span>
|
|
10
|
+
</button>
|
|
11
|
+
<button id="gate_marker_close" class="icon-button" title="Close Thresholding" type="button">
|
|
12
|
+
<span class="fas fa-xmark"></span>
|
|
13
|
+
</button>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div id="gating_download_panel">
|
|
17
|
+
<a id="gating_exit"><span class="fas fa-xmark"></span></a>
|
|
18
|
+
<label>Download gated channel ranges:
|
|
19
|
+
<span class="download_flex">
|
|
20
|
+
<input id="download_input1" type="text" autocomplete="false" spellcheck="false"
|
|
21
|
+
value="{{ data.datasource }}_gated_channel_ranges.csv">
|
|
22
|
+
<a id="download_gated_channel_ranges" class="download_button"><span class="fas fa-arrow-up-from-bracket"></span></a>
|
|
23
|
+
</span>
|
|
24
|
+
</label>
|
|
25
|
+
<label>Download gated cell encodings:
|
|
26
|
+
<span class="download_flex">
|
|
27
|
+
<input id="download_input2" type="text" autocomplete="false" spellcheck="false"
|
|
28
|
+
value="{{ data.datasource }}_gated_cell_encodings.csv">
|
|
29
|
+
<a id="download_gated_cell_encodings" class="download_button"><span class="fas fa-arrow-up-from-bracket"></span></a>
|
|
30
|
+
</span>
|
|
31
|
+
</label>
|
|
32
|
+
<label>Cell Encoding:
|
|
33
|
+
<select id="encoding">
|
|
34
|
+
<option value="binary" selected>Binary Values</option>
|
|
35
|
+
<option value="intensity">Intensity Values</option>
|
|
36
|
+
</select>
|
|
37
|
+
</label>
|
|
38
|
+
</div>
|
|
39
|
+
<form id="arrow-upload-form">
|
|
40
|
+
<input id="gating-upload-from-arrow" class="hidden" type="file">
|
|
41
|
+
<input type="hidden" id="upload-arrow-datasource" name="upload-arrow-datasource">
|
|
42
|
+
</form>
|
|
43
|
+
<label class="control-label">Marker</label>
|
|
44
|
+
<div id="gate_marker_select" class="marker-combobox-mount"></div>
|
|
45
|
+
<div class="range-readout">
|
|
46
|
+
<span id="gate_min_value">0.00</span>
|
|
47
|
+
<span id="gate_max_value">0.00</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="gate_slider" class="sidebar-slider"></div>
|
|
50
|
+
<button id="gate_auto_button" class="sidebar-action" type="button">Auto Threshold</button>
|
|
51
|
+
<div id="gate_distribution_plot" class="distribution-plot"></div>
|
|
52
|
+
<button id="save_gates_anndata_button" class="sidebar-action-fancy" type="button" hidden>
|
|
53
|
+
<span class="fas fa-file-export"></span>
|
|
54
|
+
Save Gates to Source File
|
|
55
|
+
</button>
|
|
56
|
+
<div id="gating_save_anndata_panel" hidden>
|
|
57
|
+
<button id="save_anndata_exit" class="icon-button" title="Close" type="button">
|
|
58
|
+
<span class="fas fa-xmark"></span>
|
|
59
|
+
</button>
|
|
60
|
+
<label>Table name:
|
|
61
|
+
<input id="save_anndata_table_name" type="text" autocomplete="false" spellcheck="false" value="gates">
|
|
62
|
+
</label>
|
|
63
|
+
<div class="save-anndata-actions">
|
|
64
|
+
<button id="save_anndata_confirm" class="sidebar-action" type="button">Confirm</button>
|
|
65
|
+
<button id="save_anndata_cancel" class="sidebar-action secondary" type="button">Cancel</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="save_anndata_status"></div>
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|