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,826 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cellExplorerSidebarController.js - the panel, and the plugin's registration.
|
|
3
|
+
*
|
|
4
|
+
* Orchestration only. What a colour is lives in cellExplorerColors.js, what is
|
|
5
|
+
* on the wire in cellExplorerApi.js, what the user chose in
|
|
6
|
+
* cellExplorerState.js, and the two mode-specific panels in their own files.
|
|
7
|
+
*
|
|
8
|
+
* The costs of the three kinds of change are kept apart on purpose, because
|
|
9
|
+
* keeping them apart is what makes the tool usable on a real slide:
|
|
10
|
+
*
|
|
11
|
+
* palette / colour / visibility / range / opacity
|
|
12
|
+
* -> rebuild the lookup table, redraw. No request. No geometry.
|
|
13
|
+
* a different column
|
|
14
|
+
* -> one request, cached afterwards. Geometry untouched.
|
|
15
|
+
* a different project
|
|
16
|
+
* -> a page load, which is the only time any of this is rebuilt.
|
|
17
|
+
*
|
|
18
|
+
* Registration is at the bottom because this file is loaded last (see
|
|
19
|
+
* PLUGIN.scripts): by then every class it names exists.
|
|
20
|
+
*/
|
|
21
|
+
class CellExplorerSidebarController {
|
|
22
|
+
|
|
23
|
+
/** Long enough that a run of clicks is one save, short enough that closing
|
|
24
|
+
* the tab straight after a change does not lose it. */
|
|
25
|
+
static AUTOSAVE_MS = 800;
|
|
26
|
+
|
|
27
|
+
//: Every block of the panel below the two empty states, in document order.
|
|
28
|
+
//: Listed because the mask wait replaces all of them at once; render() puts
|
|
29
|
+
//: each one back according to what the current column is. A block added to
|
|
30
|
+
//: panel.html and forgotten here stays visible behind the wait.
|
|
31
|
+
static PANEL_BLOCKS = [
|
|
32
|
+
"cell_explorer_toolbar",
|
|
33
|
+
"cell_explorer_roi_launch",
|
|
34
|
+
"cell_explorer_override",
|
|
35
|
+
"cell_explorer_status",
|
|
36
|
+
"cell_explorer_legend_section",
|
|
37
|
+
"cell_explorer_continuous",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
constructor(ctx) {
|
|
41
|
+
this.ctx = ctx;
|
|
42
|
+
this.api = new CellExplorerApi(ctx);
|
|
43
|
+
this.state = new CellExplorerState();
|
|
44
|
+
this.legend = null;
|
|
45
|
+
//: Listens for ROI hovers and answers them with a composition summary.
|
|
46
|
+
//: Null until setup(), like every other module here.
|
|
47
|
+
this.roiBridge = null;
|
|
48
|
+
this.continuous = null;
|
|
49
|
+
this.variableSelect = null;
|
|
50
|
+
this.controller = null;
|
|
51
|
+
this._saveTimer = null;
|
|
52
|
+
this._saving = null;
|
|
53
|
+
//: A pending lookup-table rebuild, so a run of changes inside one frame
|
|
54
|
+
//: costs one. See recolor().
|
|
55
|
+
this._recolorFrame = null;
|
|
56
|
+
//: The last reading from core's mask-conversion poll, and whether the
|
|
57
|
+
//: user has chosen not to wait for it. See renderMaskWait().
|
|
58
|
+
this._maskProgress = { progress: null, message: "" };
|
|
59
|
+
this._maskSkipped = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// -- lifecycle ----------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
setup() {
|
|
65
|
+
this.legend = new CellExplorerLegend(this.el("cell_explorer_legend"), {
|
|
66
|
+
onColor: (label, hex) => {
|
|
67
|
+
this.state.setColor(this.state.column, label, hex);
|
|
68
|
+
this.recolor();
|
|
69
|
+
this.scheduleSave();
|
|
70
|
+
},
|
|
71
|
+
onVisibility: (label, hidden) => {
|
|
72
|
+
this.state.setHidden(this.state.column, label, hidden);
|
|
73
|
+
this.recolor();
|
|
74
|
+
this.render();
|
|
75
|
+
this.scheduleSave();
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.continuous = new CellExplorerContinuous(
|
|
80
|
+
this.el("cell_explorer_continuous"), {
|
|
81
|
+
onRange: (low, high) => {
|
|
82
|
+
this.state.setRange(this.state.column, low, high);
|
|
83
|
+
this.recolor();
|
|
84
|
+
this.render();
|
|
85
|
+
this.scheduleSave();
|
|
86
|
+
},
|
|
87
|
+
onPalette: (palette) => {
|
|
88
|
+
this.state.setPalette(this.state.column, palette);
|
|
89
|
+
this.recolor();
|
|
90
|
+
this.render();
|
|
91
|
+
this.scheduleSave();
|
|
92
|
+
},
|
|
93
|
+
onCustomColor: (end, hex) => {
|
|
94
|
+
this.state.setCustomColor(this.state.column, end, hex);
|
|
95
|
+
this.recolor();
|
|
96
|
+
this.render();
|
|
97
|
+
this.scheduleSave();
|
|
98
|
+
},
|
|
99
|
+
onHidden: (hidden) => {
|
|
100
|
+
this.state.setContinuousHidden(this.state.column, hidden);
|
|
101
|
+
this.recolor();
|
|
102
|
+
this.render();
|
|
103
|
+
this.scheduleSave();
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// The bridge from this plugin's variable to the ROI plugin's geometry.
|
|
108
|
+
// Built unconditionally: with no ROI plugin loaded it simply never
|
|
109
|
+
// hears an event, which is cheaper than asking whether one is there.
|
|
110
|
+
this.roiBridge = new CellExplorerRoiBridge(this.ctx, this.state);
|
|
111
|
+
|
|
112
|
+
this.bindVariablePicker();
|
|
113
|
+
this.bindLegendControls();
|
|
114
|
+
this.bindOverride();
|
|
115
|
+
|
|
116
|
+
this.el("cell_explorer_close")?.addEventListener("click", () => {
|
|
117
|
+
window.PlexoraToolLoader?.hideToolPanel("cell_explorer");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Alongside, not instead of. An ROI summarised against an overlay that
|
|
121
|
+
// has just been folded away answers a question about a picture the user
|
|
122
|
+
// can no longer see -- so this is the one place in the app that asks for
|
|
123
|
+
// two tools at once. See toolLoader's coexisting pair.
|
|
124
|
+
this.el("cell_explorer_open_roi")?.addEventListener("click", () => {
|
|
125
|
+
// Asking for the ROI tool is the first honest sign that regions are
|
|
126
|
+
// about to be hovered, and the positions behind a summary are a
|
|
127
|
+
// whole-slide fetch. Started here, it is usually finished before the
|
|
128
|
+
// first region even exists.
|
|
129
|
+
this.roiBridge?.warm();
|
|
130
|
+
window.PlexoraToolLoader?.openToolAlongside("roi", "cell_explorer");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.el("cell_explorer_requirements")?.addEventListener("click", async () => {
|
|
134
|
+
// Core asks for what this plugin declared, centrally, so another
|
|
135
|
+
// plugin needing the same thing finds it already answered. A plugin
|
|
136
|
+
// must never grow its own "type a column name" box.
|
|
137
|
+
const satisfied = await this.ctx.requirements?.require(
|
|
138
|
+
["segmentation", "role:x", "role:y"]);
|
|
139
|
+
if (satisfied) window.location.reload();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// The mask this panel is waiting for. Core owns the poll -- one loop
|
|
143
|
+
// asking the server, whatever number of plugins care about the answer --
|
|
144
|
+
// so all three of these are readings, not requests.
|
|
145
|
+
const onMaskProgress = (event) => {
|
|
146
|
+
this._maskProgress = {
|
|
147
|
+
progress: event.detail?.progress ?? null,
|
|
148
|
+
message: event.detail?.message || "",
|
|
149
|
+
};
|
|
150
|
+
this.renderMaskWait();
|
|
151
|
+
};
|
|
152
|
+
const onMaskReady = () => {
|
|
153
|
+
this._maskProgress = { progress: 100, message: "" };
|
|
154
|
+
// Core has already turned the layer on by now (adoptSegmentation),
|
|
155
|
+
// so the colours have somewhere to land -- but the table was built
|
|
156
|
+
// while nothing was drawing, and applyLUT is owner-gated.
|
|
157
|
+
this.recolor();
|
|
158
|
+
this.render();
|
|
159
|
+
};
|
|
160
|
+
const onMaskFailed = (event) => {
|
|
161
|
+
// Nothing is coming, so waiting is no longer honest. The panel falls
|
|
162
|
+
// back to what it can draw and says what happened, in the same place
|
|
163
|
+
// it would have said the mask was on its way.
|
|
164
|
+
this._maskSkipped = true;
|
|
165
|
+
this._maskProgress = {
|
|
166
|
+
progress: null,
|
|
167
|
+
message: event.detail?.error || "",
|
|
168
|
+
};
|
|
169
|
+
this.state.error = "The segmentation mask could not be prepared.";
|
|
170
|
+
window.__plexora?.viewerControls?.fallBackToCentroids?.();
|
|
171
|
+
this.render();
|
|
172
|
+
};
|
|
173
|
+
window.addEventListener("plexora:segmentation-progress", onMaskProgress);
|
|
174
|
+
window.addEventListener("plexora:segmentation-ready", onMaskReady);
|
|
175
|
+
window.addEventListener("plexora:segmentation-failed", onMaskFailed);
|
|
176
|
+
this.ctx.onCleanup?.(() => {
|
|
177
|
+
window.removeEventListener("plexora:segmentation-progress", onMaskProgress);
|
|
178
|
+
window.removeEventListener("plexora:segmentation-ready", onMaskReady);
|
|
179
|
+
window.removeEventListener("plexora:segmentation-failed", onMaskFailed);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
this.el("cell_explorer_mask_wait_skip")?.addEventListener("click", async () => {
|
|
183
|
+
// An explicit "I would rather see something", which is a different
|
|
184
|
+
// thing from the silent substitution this wait replaced. The mask
|
|
185
|
+
// still takes over when it lands -- fallBackToCentroids marks these
|
|
186
|
+
// centroids as standing in for it.
|
|
187
|
+
await window.__plexora?.viewerControls?.fallBackToCentroids?.();
|
|
188
|
+
this._maskSkipped = true;
|
|
189
|
+
this.recolor();
|
|
190
|
+
this.render();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// The Cells control is core's, and the panel follows it rather than
|
|
194
|
+
// duplicating it: which representation is showing is worth remembering
|
|
195
|
+
// per project, and it is the user's choice wherever they made it.
|
|
196
|
+
//
|
|
197
|
+
// Filtered on the layer the event names. With several tools loaded, that
|
|
198
|
+
// control edits ONE of them, and a mode change belonging to another
|
|
199
|
+
// plugin used to be written straight into this project's saved
|
|
200
|
+
// preference -- so opening a second tool and clicking Outlines quietly
|
|
201
|
+
// replaced Cell Explorer's Filled for every future session.
|
|
202
|
+
const onMode = (event) => {
|
|
203
|
+
if (event.detail?.layer !== "cell_explorer") return;
|
|
204
|
+
const mode = event.detail?.mode;
|
|
205
|
+
if (!mode || mode === this.state.settings.display.mode) return;
|
|
206
|
+
this.state.setMode(mode);
|
|
207
|
+
this.scheduleSave();
|
|
208
|
+
};
|
|
209
|
+
window.addEventListener("plexora:cell-mode-changed", onMode);
|
|
210
|
+
|
|
211
|
+
// Same arrangement for opacity, which used to be a slider in this panel.
|
|
212
|
+
// Core owns the control; this plugin owns remembering where the user put
|
|
213
|
+
// it for THIS project.
|
|
214
|
+
const onOpacity = (event) => {
|
|
215
|
+
if (event.detail?.layer !== "cell_explorer") return;
|
|
216
|
+
const value = Number(event.detail?.value);
|
|
217
|
+
if (!Number.isFinite(value)) return;
|
|
218
|
+
this.state.setOpacity(value);
|
|
219
|
+
this.scheduleSave();
|
|
220
|
+
};
|
|
221
|
+
window.addEventListener("plexora:cell-layer-opacity-changed", onOpacity);
|
|
222
|
+
|
|
223
|
+
this.ctx.onCleanup?.(() => {
|
|
224
|
+
window.removeEventListener("plexora:cell-mode-changed", onMode);
|
|
225
|
+
window.removeEventListener("plexora:cell-layer-opacity-changed", onOpacity);
|
|
226
|
+
});
|
|
227
|
+
this.ctx.onCleanup?.(() => this.destroy());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** ViewerSidebar's restore hook, awaited alongside every other module's. */
|
|
231
|
+
async fetchSaved() {
|
|
232
|
+
try {
|
|
233
|
+
const [stored, catalogue] = await Promise.all([
|
|
234
|
+
this.api.state(), this.api.variables(),
|
|
235
|
+
]);
|
|
236
|
+
this.state.adopt(stored);
|
|
237
|
+
this.state.descriptors = catalogue.variables || [];
|
|
238
|
+
this.state.canDraw = catalogue.can_draw || this.state.canDraw;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error("Cell Explorer: could not load", error);
|
|
241
|
+
this.state.error = "Could not load this project's metadata.";
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async applyOrDefault() {
|
|
247
|
+
this.restoreOpacity();
|
|
248
|
+
this.restoreMode();
|
|
249
|
+
const column = this.state.chooseColumn(this.ctx.dataset?.schema?.celltype);
|
|
250
|
+
this.render();
|
|
251
|
+
if (column) await this.select(column, { persist: false });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Called by toolLoader when this panel becomes the selected one.
|
|
256
|
+
*
|
|
257
|
+
* The colours are re-applied rather than merely redrawn because they are
|
|
258
|
+
* cheap to rebuild and this is the one moment the panel and the picture are
|
|
259
|
+
* guaranteed to agree. The layer itself survived being switched away from --
|
|
260
|
+
* it kept its table and only stopped drawing -- so this costs a table
|
|
261
|
+
* rebuild and no request at all.
|
|
262
|
+
*/
|
|
263
|
+
onShow() {
|
|
264
|
+
this.restoreOpacity();
|
|
265
|
+
this.restoreMode();
|
|
266
|
+
this.recolor();
|
|
267
|
+
this.render();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Called when another tool is opened over this one, or this one is closed.
|
|
271
|
+
*
|
|
272
|
+
* Nothing to stand down -- this panel owns no viewer handlers and no
|
|
273
|
+
* document shortcuts. Core takes the cell layer away by itself. What is
|
|
274
|
+
* worth doing is writing out anything the debounce has not got to yet. */
|
|
275
|
+
onHide() {
|
|
276
|
+
this.flushSave();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
destroy() {
|
|
280
|
+
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
281
|
+
this._saveTimer = null;
|
|
282
|
+
// A frame scheduled from a change made on the way out would run against
|
|
283
|
+
// a layer this plugin no longer owns.
|
|
284
|
+
if (this._recolorFrame) cancelAnimationFrame(this._recolorFrame);
|
|
285
|
+
this._recolorFrame = null;
|
|
286
|
+
this.controller?.abort();
|
|
287
|
+
this.controller = null;
|
|
288
|
+
// Each of these parks an element on <body> that outlives this panel's
|
|
289
|
+
// markup, so they are handed back explicitly rather than left to go
|
|
290
|
+
// with the DOM the panel was rendered into. The ROI bridge holds two
|
|
291
|
+
// window listeners on top of its card.
|
|
292
|
+
this.legend?.destroy();
|
|
293
|
+
this.variableSelect?.destroy();
|
|
294
|
+
this.variableSelect = null;
|
|
295
|
+
this.roiBridge?.destroy();
|
|
296
|
+
this.roiBridge = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// -- selection ----------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Show a column.
|
|
303
|
+
*
|
|
304
|
+
* The generation is what makes rapid switching safe. Two selections in
|
|
305
|
+
* flight can finish in either order, and the older one finishing last would
|
|
306
|
+
* paint the previous variable's colours under the current variable's
|
|
307
|
+
* legend. The outgoing request is aborted too, but an abort is best-effort
|
|
308
|
+
* -- a response already on the wire still resolves, so the check on arrival
|
|
309
|
+
* is the one that decides.
|
|
310
|
+
*/
|
|
311
|
+
async select(column, { persist = true } = {}) {
|
|
312
|
+
const generation = this.state.nextGeneration();
|
|
313
|
+
this.controller?.abort();
|
|
314
|
+
this.controller = new AbortController();
|
|
315
|
+
|
|
316
|
+
this.state.column = column;
|
|
317
|
+
this.state.error = null;
|
|
318
|
+
// The overlay goes rather than lingering at reduced opacity: a stale
|
|
319
|
+
// picture that is still recognisable is worse than none, because there
|
|
320
|
+
// is nothing to say it is stale.
|
|
321
|
+
this.state.data = null;
|
|
322
|
+
this.state.status = "loading";
|
|
323
|
+
this.applyLUT(null);
|
|
324
|
+
// Same reasoning one step earlier: with no values loaded there is
|
|
325
|
+
// nothing to summarise a region by, so an open card goes with the
|
|
326
|
+
// overlay rather than sitting there full of the old column's counts.
|
|
327
|
+
this.roiBridge?.refreshOpenCard();
|
|
328
|
+
this.render();
|
|
329
|
+
|
|
330
|
+
if (persist) {
|
|
331
|
+
this.state.dirty = true;
|
|
332
|
+
this.scheduleSave();
|
|
333
|
+
}
|
|
334
|
+
if (!column) {
|
|
335
|
+
this.state.status = "idle";
|
|
336
|
+
this.render();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const data = await this.api.values(
|
|
342
|
+
column, this.state.requestedKind(column), this.controller.signal);
|
|
343
|
+
if (!this.state.isCurrent(generation)) return;
|
|
344
|
+
this.state.data = data;
|
|
345
|
+
this.state.status = "ready";
|
|
346
|
+
this.recolor();
|
|
347
|
+
// A region summary is a statement about THIS variable, so a card
|
|
348
|
+
// left open across a switch has to be re-tallied rather than left
|
|
349
|
+
// describing the column before it. Membership is untouched: the
|
|
350
|
+
// shapes have not moved.
|
|
351
|
+
this.roiBridge?.refreshOpenCard();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
if (error.name === "AbortError" || !this.state.isCurrent(generation)) return;
|
|
354
|
+
console.error(`Cell Explorer: could not load "${column}"`, error);
|
|
355
|
+
this.state.status = "error";
|
|
356
|
+
this.state.error = `Could not load "${column}".`;
|
|
357
|
+
}
|
|
358
|
+
this.render();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Rebuild the lookup table and hand it to core. No request, no geometry --
|
|
363
|
+
* and at most once per frame.
|
|
364
|
+
*
|
|
365
|
+
* The frame is what makes dragging a colour bearable. A native colour input
|
|
366
|
+
* fires `input` continuously while the pointer moves around the OS picker,
|
|
367
|
+
* tens of times a second, and each one of those would otherwise rebuild a
|
|
368
|
+
* table with an entry per cell id and re-render every label tile on screen.
|
|
369
|
+
* On a slide with a few hundred thousand cells the drag turns into a
|
|
370
|
+
* slideshow that keeps painting long after the pointer has stopped.
|
|
371
|
+
*
|
|
372
|
+
* Coalescing costs nothing anywhere else: every caller here is a display
|
|
373
|
+
* change whose result is a repaint, and a repaint cannot show more than one
|
|
374
|
+
* state per frame however many times it is asked to.
|
|
375
|
+
*/
|
|
376
|
+
recolor() {
|
|
377
|
+
if (this._recolorFrame) return;
|
|
378
|
+
this._recolorFrame = requestAnimationFrame(() => {
|
|
379
|
+
this._recolorFrame = null;
|
|
380
|
+
this.applyLUT(this.state.buildLUT());
|
|
381
|
+
// Every hide, show, All/None and colour change funnels through here,
|
|
382
|
+
// which makes it the one place an open region card can be kept
|
|
383
|
+
// truthful: it reports the visible categories only, in their current
|
|
384
|
+
// colours, so anything that changes either has to reach it.
|
|
385
|
+
this.roiBridge?.refreshOpenCard();
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
applyLUT(lut) {
|
|
390
|
+
// A table handed over directly supersedes a pending rebuild. select()
|
|
391
|
+
// clears the overlay this way, and a frame scheduled before it that
|
|
392
|
+
// landed afterwards would paint the previous column's colours back on.
|
|
393
|
+
if (this._recolorFrame) {
|
|
394
|
+
cancelAnimationFrame(this._recolorFrame);
|
|
395
|
+
this._recolorFrame = null;
|
|
396
|
+
}
|
|
397
|
+
const viewer = this.ctx.viewer;
|
|
398
|
+
if (!viewer?.setCellColorLUT) return;
|
|
399
|
+
// Owner-gated on the other side. Being turned away is the correct
|
|
400
|
+
// outcome when this tool is not the visible one -- see onShow, which is
|
|
401
|
+
// where it is applied again.
|
|
402
|
+
viewer.setCellColorLUT("cell_explorer", lut);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Put the Cells control back where this project left it.
|
|
407
|
+
*
|
|
408
|
+
* Only when the user has not chosen a mode for THIS layer in this session.
|
|
409
|
+
* Core turns a layer on when a colouring plugin activates
|
|
410
|
+
* (viewerControls.enableCellLayer), and whatever the user has since chosen
|
|
411
|
+
* outranks a stored preference -- so this fills in a gap rather than
|
|
412
|
+
* overruling a decision.
|
|
413
|
+
*
|
|
414
|
+
* The question used to be asked of the control as a whole ("is anything
|
|
415
|
+
* showing?"), which stopped being the right one the moment two plugins could
|
|
416
|
+
* draw at once: another tool having turned the mask on is not a decision
|
|
417
|
+
* about this layer, and it silently suppressed the restore every time.
|
|
418
|
+
*/
|
|
419
|
+
restoreMode() {
|
|
420
|
+
const controls = window.__plexora?.viewerControls;
|
|
421
|
+
const layer = this.ctx.viewer?.getCellLayer?.("cell_explorer");
|
|
422
|
+
const mode = this.state.settings.display.mode;
|
|
423
|
+
if (!controls || !mode || mode === "none") return;
|
|
424
|
+
if (layer ? layer.userMode : controls.mode !== "none") return;
|
|
425
|
+
if (!controls.offeredModes()[mode]) return;
|
|
426
|
+
controls.selectMode(mode);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Put the shared opacity slider back where this project left it.
|
|
431
|
+
*
|
|
432
|
+
* Applied to this plugin's own layer by name, so it lands on the right one
|
|
433
|
+
* whether or not this tool is the selected one at the time.
|
|
434
|
+
*/
|
|
435
|
+
restoreOpacity() {
|
|
436
|
+
const value = this.state.settings.display.opacity;
|
|
437
|
+
if (!Number.isFinite(value)) return;
|
|
438
|
+
this.ctx.viewer?.setLayerOpacity?.("cell_explorer", value);
|
|
439
|
+
window.__plexora?.viewerControls?.paintLayerOpacity?.();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// -- panel --------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
el(id) {
|
|
445
|
+
return document.getElementById(id);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The Colour-by control: core's searchable combobox in its button shape.
|
|
450
|
+
*
|
|
451
|
+
* A button rather than a field, for two reasons that happen to point the
|
|
452
|
+
* same way. It is sized to the column name instead of to a text input, so
|
|
453
|
+
* it can share its line with the two controls that act on the legend. And
|
|
454
|
+
* the search lives inside the menu it opens, where somebody looking for a
|
|
455
|
+
* search box will look -- a field that doubles as the value display gives
|
|
456
|
+
* no sign at all that it can be typed into.
|
|
457
|
+
*
|
|
458
|
+
* Every metadata column is in the list -- the server stopped deciding which
|
|
459
|
+
* ones were worth offering (see variables.eligible_columns). What used to
|
|
460
|
+
* be a refusal is a hint here: the dot and the hover text say a column
|
|
461
|
+
* behaves like an identifier, and picking it anyway works.
|
|
462
|
+
*/
|
|
463
|
+
bindVariablePicker() {
|
|
464
|
+
const mount = this.el("cell_explorer_variable");
|
|
465
|
+
if (!mount) return;
|
|
466
|
+
this.variableSelect = new SearchableSelect(mount, {
|
|
467
|
+
options: [],
|
|
468
|
+
value: "",
|
|
469
|
+
trigger: "button",
|
|
470
|
+
emptyLabel: "Choose a column",
|
|
471
|
+
searchPlaceholder: "Search metadata...",
|
|
472
|
+
ariaLabel: "Colour by",
|
|
473
|
+
emptyText: "No columns match",
|
|
474
|
+
describeOption: (name) => this.describeColumn(name),
|
|
475
|
+
getIndicator: (name) => this.warnAboutColumn(name),
|
|
476
|
+
onChange: (name) => this.select(name || null),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The hint shown beside a column name in the dropdown. */
|
|
481
|
+
describeColumn(name) {
|
|
482
|
+
const descriptor = this.state.descriptor(name);
|
|
483
|
+
if (!descriptor) return "";
|
|
484
|
+
if (descriptor.kind === "continuous") return "numeric";
|
|
485
|
+
return `${descriptor.n_categories || 0} categories`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* A dot, and why, for a column that will not draw well -- or "" for the
|
|
490
|
+
* ordinary case, which is SearchableSelect's way of saying "no dot".
|
|
491
|
+
*/
|
|
492
|
+
warnAboutColumn(name) {
|
|
493
|
+
const descriptor = this.state.descriptor(name);
|
|
494
|
+
if (!descriptor?.identifier_like) return "";
|
|
495
|
+
return `${(descriptor.n_unique || 0).toLocaleString()} distinct values -- `
|
|
496
|
+
+ "this behaves like an identifier, and will draw about as many colours "
|
|
497
|
+
+ "as there are cells.";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
bindLegendControls() {
|
|
501
|
+
this.el("cell_explorer_search")?.addEventListener("input", (event) => {
|
|
502
|
+
// Filters the legend only. Which cells are drawn does not change --
|
|
503
|
+
// typing "T" must not hide the macrophages.
|
|
504
|
+
this.legend.setFilter(event.target.value);
|
|
505
|
+
this.renderLegend();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// All / None, as one delegated handler over the pair. Clicking the lit
|
|
509
|
+
// one is a no-op rather than a toggle: these say which state the legend
|
|
510
|
+
// is in, and clicking "All" while everything already shows should not
|
|
511
|
+
// hide everything.
|
|
512
|
+
this.el("cell_explorer_visibility")?.addEventListener("click", (event) => {
|
|
513
|
+
const button = event.target.closest?.("[data-visibility]");
|
|
514
|
+
if (!button || !this.state.column) return;
|
|
515
|
+
const hide = button.dataset.visibility === "none";
|
|
516
|
+
if (this.state.visibilityState(this.state.column) === button.dataset.visibility) return;
|
|
517
|
+
this.state.setAllHidden(this.state.column, hide);
|
|
518
|
+
this.recolor();
|
|
519
|
+
this.render();
|
|
520
|
+
this.scheduleSave();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
bindOverride() {
|
|
525
|
+
this.el("cell_explorer_override")?.addEventListener("click", (event) => {
|
|
526
|
+
const button = event.target.closest?.("[data-kind]");
|
|
527
|
+
if (!button || button.disabled) return;
|
|
528
|
+
const column = this.state.column;
|
|
529
|
+
const descriptor = this.state.descriptor(column);
|
|
530
|
+
if (!descriptor) return;
|
|
531
|
+
const kind = button.dataset.kind;
|
|
532
|
+
this.state.setOverride(column, kind === descriptor.kind ? null : kind);
|
|
533
|
+
// No cache flush: the cache key carries the kind, so the two
|
|
534
|
+
// encodings of one column are separate entries and flipping back
|
|
535
|
+
// and forth is free after the first of each.
|
|
536
|
+
this.select(column);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
render() {
|
|
541
|
+
// First, because it can take the whole panel over: there is nothing
|
|
542
|
+
// useful to choose a colour-by column for until the mask this project
|
|
543
|
+
// draws cells on exists.
|
|
544
|
+
if (this.renderMaskWait()) return;
|
|
545
|
+
|
|
546
|
+
// The two blocks the wait may have taken away that nothing further down
|
|
547
|
+
// owns. Everything else in PANEL_BLOCKS has its visibility decided
|
|
548
|
+
// below, or by renderStatus/renderOverride.
|
|
549
|
+
const toolbar = this.el("cell_explorer_toolbar");
|
|
550
|
+
if (toolbar) toolbar.hidden = false;
|
|
551
|
+
const launch = this.el("cell_explorer_roi_launch");
|
|
552
|
+
if (launch) launch.hidden = false;
|
|
553
|
+
|
|
554
|
+
this.renderVariablePicker();
|
|
555
|
+
this.renderStatus();
|
|
556
|
+
this.renderOverride();
|
|
557
|
+
|
|
558
|
+
const column = this.state.column;
|
|
559
|
+
const kind = column ? this.state.kindFor(column) : null;
|
|
560
|
+
const legend = this.el("cell_explorer_legend_section");
|
|
561
|
+
const continuous = this.el("cell_explorer_continuous");
|
|
562
|
+
const ready = this.state.status === "ready" && Boolean(column);
|
|
563
|
+
const categorical = ready && kind === "categorical";
|
|
564
|
+
|
|
565
|
+
if (legend) legend.hidden = !categorical;
|
|
566
|
+
if (continuous) continuous.hidden = !(ready && kind === "continuous");
|
|
567
|
+
// The filter and All/None sit in the toolbar, on the colour-by control's
|
|
568
|
+
// line, but they belong to the list -- so they come and go with it
|
|
569
|
+
// rather than staying behind as two controls with nothing to act on.
|
|
570
|
+
const categoryControls = this.el("cell_explorer_category_controls");
|
|
571
|
+
if (categoryControls) categoryControls.hidden = !categorical;
|
|
572
|
+
|
|
573
|
+
if (!ready) return;
|
|
574
|
+
if (kind === "categorical") {
|
|
575
|
+
this.renderLegend();
|
|
576
|
+
} else {
|
|
577
|
+
this.continuous.render(
|
|
578
|
+
this.state.current,
|
|
579
|
+
this.state.continuous(column),
|
|
580
|
+
this.state.domainFor(column),
|
|
581
|
+
this.state.isAutoRange(column),
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The whole panel, replaced by "the mask is still being made".
|
|
588
|
+
*
|
|
589
|
+
* Only while a mask genuinely IS on its way -- `pending` is a live getter
|
|
590
|
+
* over the config core rewrites when the job lands, so this stops being true
|
|
591
|
+
* on its own. Never for a project that simply has no mask: that one has
|
|
592
|
+
* nothing to wait for and is handled by the "nothing to draw these on"
|
|
593
|
+
* state above.
|
|
594
|
+
*
|
|
595
|
+
* Everything else is hidden rather than left visible behind the wait,
|
|
596
|
+
* because a colour-by control that recolours a picture nobody can see is an
|
|
597
|
+
* invitation to make a choice with no feedback. The panel comes back whole
|
|
598
|
+
* the moment the mask lands or the user asks for centroids instead.
|
|
599
|
+
*
|
|
600
|
+
* @returns whether the wait took the panel over.
|
|
601
|
+
*/
|
|
602
|
+
renderMaskWait() {
|
|
603
|
+
const wait = this.el("cell_explorer_mask_wait");
|
|
604
|
+
if (!wait) return false;
|
|
605
|
+
const waiting = Boolean(this.ctx.dataset?.segmentation?.pending)
|
|
606
|
+
&& !this._maskSkipped
|
|
607
|
+
&& this.state.descriptors.length > 0;
|
|
608
|
+
|
|
609
|
+
wait.hidden = !waiting;
|
|
610
|
+
// Blocks named by id rather than a wrapper hidden as one: the panel is a
|
|
611
|
+
// flat list, and wrapping every block inside a new element for the sake
|
|
612
|
+
// of one state would change the markup everything else is written
|
|
613
|
+
// against. Only the hiding is done here -- render() decides what each
|
|
614
|
+
// block should be when the wait is over.
|
|
615
|
+
if (waiting) {
|
|
616
|
+
for (const id of CellExplorerSidebarController.PANEL_BLOCKS) {
|
|
617
|
+
const block = this.el(id);
|
|
618
|
+
if (block) block.hidden = true;
|
|
619
|
+
}
|
|
620
|
+
this.renderMaskProgress();
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
renderMaskProgress() {
|
|
627
|
+
const bar = this.el("cell_explorer_mask_wait_bar");
|
|
628
|
+
const percent = this._maskProgress.progress;
|
|
629
|
+
if (bar) {
|
|
630
|
+
// No reading yet, or a job that reports none: a full-width bar at low
|
|
631
|
+
// opacity says "working" without claiming a position it does not
|
|
632
|
+
// know. See the .is-indeterminate rule.
|
|
633
|
+
const known = Number.isFinite(percent);
|
|
634
|
+
bar.classList.toggle("is-indeterminate", !known);
|
|
635
|
+
bar.style.width = known ? `${Math.min(100, Math.max(0, percent))}%` : "100%";
|
|
636
|
+
}
|
|
637
|
+
const message = this.el("cell_explorer_mask_wait_message");
|
|
638
|
+
if (message) {
|
|
639
|
+
message.textContent = this._maskProgress.message
|
|
640
|
+
|| "Converting segmentation mask…";
|
|
641
|
+
}
|
|
642
|
+
// Offered only where there is something to fall back TO. On a project
|
|
643
|
+
// with no coordinates the choice does not exist, and a button that
|
|
644
|
+
// cannot do anything is worse than no button at all.
|
|
645
|
+
const skip = this.el("cell_explorer_mask_wait_skip");
|
|
646
|
+
if (skip) skip.hidden = !this.state.canDraw.centroids;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
renderLegend() {
|
|
650
|
+
const column = this.state.column;
|
|
651
|
+
if (!column) return;
|
|
652
|
+
this.legend.render(this.state.current, this.state.categorical(column));
|
|
653
|
+
this.renderVisibilityState();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Which of All / None the legend is currently in, or neither. */
|
|
657
|
+
renderVisibilityState() {
|
|
658
|
+
const group = this.el("cell_explorer_visibility");
|
|
659
|
+
if (!group) return;
|
|
660
|
+
const state = this.state.visibilityState(this.state.column);
|
|
661
|
+
group.querySelectorAll("[data-visibility]").forEach((button) => {
|
|
662
|
+
const on = button.dataset.visibility === state;
|
|
663
|
+
button.classList.toggle("is-active", on);
|
|
664
|
+
button.setAttribute("aria-checked", on ? "true" : "false");
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
renderVariablePicker() {
|
|
669
|
+
if (!this.variableSelect) return;
|
|
670
|
+
// Every column, in the table's own order. The combobox does the
|
|
671
|
+
// filtering itself, so this is set once per catalogue rather than
|
|
672
|
+
// rebuilt per keystroke.
|
|
673
|
+
this.variableSelect.setOptions(
|
|
674
|
+
this.state.descriptors.map((descriptor) => descriptor.name));
|
|
675
|
+
this.variableSelect.setValue(this.state.column || "");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
renderStatus() {
|
|
679
|
+
const status = this.el("cell_explorer_status");
|
|
680
|
+
const empty = this.el("cell_explorer_empty");
|
|
681
|
+
const nothing = this.el("cell_explorer_nowhere");
|
|
682
|
+
if (empty) empty.hidden = this.state.descriptors.length > 0;
|
|
683
|
+
|
|
684
|
+
// The state where the tool cannot work at all: cells to colour, and
|
|
685
|
+
// nowhere to draw them. Distinct from having no metadata, and the fix
|
|
686
|
+
// is different -- this one is a file, that one is the column screen.
|
|
687
|
+
const canDraw = this.state.canDraw.segmentation || this.state.canDraw.centroids;
|
|
688
|
+
if (nothing) nothing.hidden = canDraw || this.state.descriptors.length === 0;
|
|
689
|
+
|
|
690
|
+
if (!status) return;
|
|
691
|
+
const descriptor = this.state.current;
|
|
692
|
+
if (this.state.error) {
|
|
693
|
+
status.textContent = this.state.error;
|
|
694
|
+
} else if (this.state.status === "loading") {
|
|
695
|
+
status.textContent = `Loading ${this.state.column}...`;
|
|
696
|
+
} else if (this.ctx.dataset?.segmentation?.pending) {
|
|
697
|
+
// Reached only once the wait screen has been dismissed, or was never
|
|
698
|
+
// shown: it hides this line along with the rest of the panel. So
|
|
699
|
+
// whatever is drawing now is standing in for a mask still on its way,
|
|
700
|
+
// and this is where the panel keeps saying so.
|
|
701
|
+
status.textContent = "Preparing the segmentation mask...";
|
|
702
|
+
} else if (descriptor?.notice === "high_cardinality") {
|
|
703
|
+
status.textContent = `${descriptor.n_categories.toLocaleString()} categories `
|
|
704
|
+
+ "-- search or hide categories for a clearer view.";
|
|
705
|
+
} else if (descriptor?.notice === "many_categories") {
|
|
706
|
+
status.textContent = `${descriptor.n_categories} categories.`;
|
|
707
|
+
} else if (this.state.readOnly) {
|
|
708
|
+
status.textContent = "These settings were saved by a newer version of "
|
|
709
|
+
+ "Plexora, so changes here are not being saved.";
|
|
710
|
+
} else {
|
|
711
|
+
status.textContent = "";
|
|
712
|
+
}
|
|
713
|
+
status.hidden = !status.textContent;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
renderOverride() {
|
|
717
|
+
const section = this.el("cell_explorer_override");
|
|
718
|
+
if (!section) return;
|
|
719
|
+
const descriptor = this.state.current;
|
|
720
|
+
// Shown only where the inference was a guess. An override on every
|
|
721
|
+
// variable is noise on the ninety percent that were never in doubt.
|
|
722
|
+
section.hidden = !descriptor?.ambiguous;
|
|
723
|
+
if (section.hidden) return;
|
|
724
|
+
const active = this.state.kindFor(this.state.column);
|
|
725
|
+
section.querySelectorAll("[data-kind]").forEach((button) => {
|
|
726
|
+
const on = button.dataset.kind === active;
|
|
727
|
+
button.classList.toggle("is-active", on);
|
|
728
|
+
button.setAttribute("aria-checked", on ? "true" : "false");
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// -- persistence --------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Write after a pause, and only for a committed change.
|
|
736
|
+
*
|
|
737
|
+
* Every caller here is a decision that has landed -- a colour chosen, a
|
|
738
|
+
* category hidden, a slider released. The opacity slider's own drag never
|
|
739
|
+
* reaches this: core fires the event it saves from once, on release.
|
|
740
|
+
*/
|
|
741
|
+
scheduleSave() {
|
|
742
|
+
if (this.state.readOnly) return;
|
|
743
|
+
this.state.dirty = true;
|
|
744
|
+
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
745
|
+
this._saveTimer = setTimeout(() => this.persistIfNeeded(),
|
|
746
|
+
CellExplorerSidebarController.AUTOSAVE_MS);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
flushSave() {
|
|
750
|
+
if (this._saveTimer) clearTimeout(this._saveTimer);
|
|
751
|
+
this._saveTimer = null;
|
|
752
|
+
return this.persistIfNeeded();
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async persistIfNeeded() {
|
|
756
|
+
if (!this.state.dirty || this.state.readOnly) return;
|
|
757
|
+
if (this._saving) return this._saving;
|
|
758
|
+
this.state.dirty = false;
|
|
759
|
+
|
|
760
|
+
this._saving = (async () => {
|
|
761
|
+
try {
|
|
762
|
+
const result = await this.api.save(
|
|
763
|
+
this.state.revision, this.state.toSettings());
|
|
764
|
+
if (result.conflict) {
|
|
765
|
+
// Another tab wrote first. Theirs is taken rather than
|
|
766
|
+
// overwritten -- both sides hold a full copy, and clobbering
|
|
767
|
+
// silently reverts every change made in the other one.
|
|
768
|
+
console.info("Cell Explorer: settings changed in another session");
|
|
769
|
+
const fresh = await this.api.state();
|
|
770
|
+
this.state.adopt(fresh);
|
|
771
|
+
this.recolor();
|
|
772
|
+
this.render();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.state.revision = result.revision;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
console.error("Cell Explorer: could not save settings", error);
|
|
778
|
+
// Put it back, so the next committed change tries again.
|
|
779
|
+
this.state.dirty = true;
|
|
780
|
+
} finally {
|
|
781
|
+
this._saving = null;
|
|
782
|
+
}
|
|
783
|
+
})();
|
|
784
|
+
return this._saving;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
if (window.Plexora) {
|
|
790
|
+
window.Plexora.registerPlugin({
|
|
791
|
+
name: "cell_explorer",
|
|
792
|
+
ownsCellLayer: true,
|
|
793
|
+
/**
|
|
794
|
+
* Filled, not outlines. This tool gives every cell a colour that means
|
|
795
|
+
* something, and an outline shows that colour in a one-pixel ring
|
|
796
|
+
* around tissue that is still the tissue's own colour -- at any zoom
|
|
797
|
+
* where more than a few hundred cells are on screen, the phenotype map
|
|
798
|
+
* this plugin exists to draw is simply not legible. Opacity is the
|
|
799
|
+
* control for seeing the tissue through it.
|
|
800
|
+
*
|
|
801
|
+
* Core falls back to outlines by itself when the mask was stored
|
|
802
|
+
* already reduced to boundaries, so this is a preference and not an
|
|
803
|
+
* assumption about the file.
|
|
804
|
+
*/
|
|
805
|
+
preferredCellMode: "filled",
|
|
806
|
+
/**
|
|
807
|
+
* The selection-provider shape core expects from whoever holds the cell
|
|
808
|
+
* layer. Cell Explorer answers "nothing" to all three on purpose: that
|
|
809
|
+
* interface is about GATING -- which cells to draw at all, and the
|
|
810
|
+
* shader's colour-coded range table. This plugin decides what colour a
|
|
811
|
+
* cell is, through setCellColorLUT, which is a different channel
|
|
812
|
+
* entirely. Answering otherwise would put an empty gate on the layer and
|
|
813
|
+
* hide every cell.
|
|
814
|
+
*/
|
|
815
|
+
createInstance() {
|
|
816
|
+
return {
|
|
817
|
+
getSelectedIds: async () => new Set(),
|
|
818
|
+
supportsColorCoding: () => false,
|
|
819
|
+
getColorCodedRanges: () => ({}),
|
|
820
|
+
};
|
|
821
|
+
},
|
|
822
|
+
createSidebarController(ctx) {
|
|
823
|
+
return new CellExplorerSidebarController(ctx);
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
}
|