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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @class ColorSwatchPicker - one-click curated color palette popover, with a
|
|
3
|
+
* native <input type="color"> escape hatch for exact/custom hex values.
|
|
4
|
+
*/
|
|
5
|
+
class ColorSwatchPicker {
|
|
6
|
+
constructor(mount, options = {}) {
|
|
7
|
+
this.mount = mount;
|
|
8
|
+
this.value = options.value || "#2388ff";
|
|
9
|
+
this.presets = options.presets || ColorSwatchPicker.DEFAULT_PRESETS;
|
|
10
|
+
this.title = options.title || "Channel color";
|
|
11
|
+
this.onChange = options.onChange || (() => {});
|
|
12
|
+
this.isOpen = false;
|
|
13
|
+
this.render();
|
|
14
|
+
this.bindDismissHandlers();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
render() {
|
|
18
|
+
this.mount.innerHTML = "";
|
|
19
|
+
this.mount.classList.add("color-swatch-mount");
|
|
20
|
+
|
|
21
|
+
this.button = document.createElement("button");
|
|
22
|
+
this.button.type = "button";
|
|
23
|
+
this.button.className = "channel-color-swatch";
|
|
24
|
+
this.button.title = this.title;
|
|
25
|
+
this.button.setAttribute("aria-label", this.title);
|
|
26
|
+
this.button.style.setProperty("--swatch-color", this.value);
|
|
27
|
+
this.button.setAttribute("aria-haspopup", "true");
|
|
28
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
29
|
+
this.button.addEventListener("click", (event) => {
|
|
30
|
+
event.stopPropagation();
|
|
31
|
+
this.toggle();
|
|
32
|
+
});
|
|
33
|
+
this.mount.appendChild(this.button);
|
|
34
|
+
|
|
35
|
+
this.popover = document.createElement("div");
|
|
36
|
+
this.popover.className = "color-swatch-popover";
|
|
37
|
+
this.popover.hidden = true;
|
|
38
|
+
this.popover.addEventListener("click", (event) => event.stopPropagation());
|
|
39
|
+
|
|
40
|
+
const grid = document.createElement("div");
|
|
41
|
+
grid.className = "color-swatch-grid";
|
|
42
|
+
this.presets.forEach((preset) => {
|
|
43
|
+
const swatch = document.createElement("button");
|
|
44
|
+
swatch.type = "button";
|
|
45
|
+
swatch.className = "color-swatch-option";
|
|
46
|
+
swatch.title = preset.label;
|
|
47
|
+
swatch.style.setProperty("--swatch-color", preset.hex);
|
|
48
|
+
swatch.classList.toggle("is-selected", preset.hex.toLowerCase() === this.value.toLowerCase());
|
|
49
|
+
swatch.addEventListener("click", () => this.selectColor(preset.hex));
|
|
50
|
+
grid.appendChild(swatch);
|
|
51
|
+
});
|
|
52
|
+
this.popover.appendChild(grid);
|
|
53
|
+
|
|
54
|
+
const customRow = document.createElement("label");
|
|
55
|
+
customRow.className = "color-swatch-custom-row";
|
|
56
|
+
const customLabel = document.createElement("span");
|
|
57
|
+
customLabel.textContent = "Custom";
|
|
58
|
+
customRow.appendChild(customLabel);
|
|
59
|
+
this.customInput = document.createElement("input");
|
|
60
|
+
this.customInput.type = "color";
|
|
61
|
+
this.customInput.value = this.value;
|
|
62
|
+
this.customInput.addEventListener("input", (event) => this.selectColor(event.target.value, { keepOpen: true }));
|
|
63
|
+
customRow.appendChild(this.customInput);
|
|
64
|
+
this.popover.appendChild(customRow);
|
|
65
|
+
|
|
66
|
+
// Appended to <body> (a positioning "portal"), not this.mount: a disabled/dimmed
|
|
67
|
+
// ancestor row has opacity < 1, which creates its own CSS stacking context and
|
|
68
|
+
// would trap this popover's z-index behind the *next* row otherwise.
|
|
69
|
+
document.body.appendChild(this.popover);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
toggle() {
|
|
73
|
+
if (this.isOpen) {
|
|
74
|
+
this.close();
|
|
75
|
+
} else {
|
|
76
|
+
this.open();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
open() {
|
|
81
|
+
if (ColorSwatchPicker.activeInstance && ColorSwatchPicker.activeInstance !== this) {
|
|
82
|
+
ColorSwatchPicker.activeInstance.close();
|
|
83
|
+
}
|
|
84
|
+
ColorSwatchPicker.activeInstance = this;
|
|
85
|
+
this.isOpen = true;
|
|
86
|
+
this.positionPopover();
|
|
87
|
+
this.popover.hidden = false;
|
|
88
|
+
requestAnimationFrame(() => this.popover.classList.add("is-open"));
|
|
89
|
+
this.button.setAttribute("aria-expanded", "true");
|
|
90
|
+
this.bindDismissListeners();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
positionPopover() {
|
|
94
|
+
const rect = this.button.getBoundingClientRect();
|
|
95
|
+
this.popover.style.left = `${rect.left}px`;
|
|
96
|
+
this.popover.style.top = `${rect.bottom + 6}px`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
close() {
|
|
100
|
+
this.isOpen = false;
|
|
101
|
+
if (!this.popover) return;
|
|
102
|
+
this.popover.classList.remove("is-open");
|
|
103
|
+
this.button.setAttribute("aria-expanded", "false");
|
|
104
|
+
window.setTimeout(() => {
|
|
105
|
+
// The popover may have been destroyed inside this delay -- closing
|
|
106
|
+
// is the first thing destroy() does.
|
|
107
|
+
if (!this.isOpen && this.popover) this.popover.hidden = true;
|
|
108
|
+
}, 150);
|
|
109
|
+
if (ColorSwatchPicker.activeInstance === this) {
|
|
110
|
+
ColorSwatchPicker.activeInstance = null;
|
|
111
|
+
}
|
|
112
|
+
this.unbindDismissListeners();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bindDismissListeners() {
|
|
116
|
+
if (this._dismissScroll) return;
|
|
117
|
+
this._dismissScroll = (event) => {
|
|
118
|
+
if (event.target === this.popover || this.popover.contains(event.target)) return;
|
|
119
|
+
this.close();
|
|
120
|
+
};
|
|
121
|
+
this._dismissResize = () => this.close();
|
|
122
|
+
window.addEventListener("scroll", this._dismissScroll, true);
|
|
123
|
+
window.addEventListener("resize", this._dismissResize);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
unbindDismissListeners() {
|
|
127
|
+
if (!this._dismissScroll) return;
|
|
128
|
+
window.removeEventListener("scroll", this._dismissScroll, true);
|
|
129
|
+
window.removeEventListener("resize", this._dismissResize);
|
|
130
|
+
this._dismissScroll = null;
|
|
131
|
+
this._dismissResize = null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
selectColor(hex, opts = {}) {
|
|
135
|
+
this.setValue(hex);
|
|
136
|
+
this.onChange(hex);
|
|
137
|
+
if (!opts.keepOpen) this.close();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setValue(hex) {
|
|
141
|
+
this.value = hex;
|
|
142
|
+
this.button.style.setProperty("--swatch-color", hex);
|
|
143
|
+
this.customInput.value = hex;
|
|
144
|
+
this.popover.querySelectorAll(".color-swatch-option").forEach((el) => {
|
|
145
|
+
el.classList.toggle("is-selected", el.style.getPropertyValue("--swatch-color").toLowerCase() === hex.toLowerCase());
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bindDismissHandlers() {
|
|
150
|
+
// Kept on the instance so destroy() can take them off again. Anonymous
|
|
151
|
+
// handlers here meant every picker ever built stayed subscribed to
|
|
152
|
+
// document for the life of the page, which is invisible while pickers
|
|
153
|
+
// are per-channel and created once, and is not once a list rebuilds a
|
|
154
|
+
// row of them on every keystroke.
|
|
155
|
+
this._documentClick = (event) => {
|
|
156
|
+
if (this.isOpen && !this.mount.contains(event.target) && !this.popover.contains(event.target)) {
|
|
157
|
+
this.close();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
this._documentKeydown = (event) => {
|
|
161
|
+
if (event.key === "Escape" && this.isOpen) {
|
|
162
|
+
this.close();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
document.addEventListener("click", this._documentClick);
|
|
166
|
+
document.addEventListener("keydown", this._documentKeydown);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Take this picker off the page for good.
|
|
171
|
+
*
|
|
172
|
+
* The popover lives on <body> rather than inside the mount (see render), so
|
|
173
|
+
* removing the row that owns it leaves the popover behind. Callers that
|
|
174
|
+
* rebuild their rows must call this, or the orphans accumulate.
|
|
175
|
+
*/
|
|
176
|
+
destroy() {
|
|
177
|
+
this.close();
|
|
178
|
+
this.unbindDismissListeners();
|
|
179
|
+
document.removeEventListener("click", this._documentClick);
|
|
180
|
+
document.removeEventListener("keydown", this._documentKeydown);
|
|
181
|
+
this.popover?.remove();
|
|
182
|
+
this.popover = null;
|
|
183
|
+
if (ColorSwatchPicker.activeInstance === this) {
|
|
184
|
+
ColorSwatchPicker.activeInstance = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
ColorSwatchPicker.DEFAULT_PRESETS = [
|
|
190
|
+
{ label: "Blue", hex: "#2388ff" },
|
|
191
|
+
{ label: "Red", hex: "#ff2d2d" },
|
|
192
|
+
{ label: "Green", hex: "#2bd46f" },
|
|
193
|
+
{ label: "White", hex: "#ffffff" },
|
|
194
|
+
{ label: "Yellow", hex: "#ffd60a" },
|
|
195
|
+
{ label: "Magenta", hex: "#ec4899" },
|
|
196
|
+
{ label: "Cyan", hex: "#22e6e6" },
|
|
197
|
+
{ label: "Orange", hex: "#f97316" },
|
|
198
|
+
{ label: "Violet", hex: "#a78bfa" },
|
|
199
|
+
{ label: "Gray", hex: "#94a3b8" },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
ColorSwatchPicker.activeInstance = null;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* columnClassifier.js -- which columns are markers, and which are metadata.
|
|
3
|
+
*
|
|
4
|
+
* Two boxes side by side. The server predicts the split (adapters/classify.py)
|
|
5
|
+
* and the user corrects it by dragging; the metadata box additionally carries
|
|
6
|
+
* the role slots, because a role always names a metadata column -- the cell id
|
|
7
|
+
* and the coordinates are measurements *about* a cell, never markers measured
|
|
8
|
+
* on one.
|
|
9
|
+
*
|
|
10
|
+
* Mounted by three callers -- the CSV import step, the requirements modal, and
|
|
11
|
+
* the project edit page -- which is why it is a component rather than page
|
|
12
|
+
* code. All three are asking the same question and must store the same answer.
|
|
13
|
+
*
|
|
14
|
+
* Uses window.Sortable (vendored via webpack's vendor bundle, loaded by
|
|
15
|
+
* base.html on every page).
|
|
16
|
+
*/
|
|
17
|
+
window.PlexoraColumnClassifier = (function () {
|
|
18
|
+
/**
|
|
19
|
+
* @param mount element to render into
|
|
20
|
+
* @param options.markers predicted marker columns, in table order
|
|
21
|
+
* @param options.metadata predicted metadata columns, in table order
|
|
22
|
+
* @param options.roles role -> column name, as already recorded
|
|
23
|
+
* @param options.roleLabels role -> human label (from the server, so the
|
|
24
|
+
* wording matches the requirements modal)
|
|
25
|
+
* @param options.onChange called after every edit
|
|
26
|
+
* @returns { value(), destroy() }
|
|
27
|
+
*/
|
|
28
|
+
function mount(mount, options = {}) {
|
|
29
|
+
const roleLabels = options.roleLabels || {};
|
|
30
|
+
const roleNames = Object.keys(roleLabels);
|
|
31
|
+
const roles = { ...(options.roles || {}) };
|
|
32
|
+
let markers = [...(options.markers || [])];
|
|
33
|
+
let metadata = [...(options.metadata || [])];
|
|
34
|
+
const sortables = [];
|
|
35
|
+
|
|
36
|
+
mount.innerHTML = `
|
|
37
|
+
<div class="column-classifier">
|
|
38
|
+
<div class="column-box" data-group="metadata">
|
|
39
|
+
<div class="column-box-head">
|
|
40
|
+
<h2>Metadata</h2>
|
|
41
|
+
<span class="field-hint">Identifiers, coordinates, morphology, annotations.</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="column-roles"></div>
|
|
44
|
+
<ul class="column-list" data-group="metadata"></ul>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="column-box" data-group="markers">
|
|
47
|
+
<div class="column-box-head">
|
|
48
|
+
<h2>Markers</h2>
|
|
49
|
+
<span class="field-hint">Per-cell intensities that can be plotted or thresholded.</span>
|
|
50
|
+
</div>
|
|
51
|
+
<ul class="column-list" data-group="markers"></ul>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<p class="column-classifier-hint">
|
|
55
|
+
Drag a column between the boxes to correct it.
|
|
56
|
+
</p>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const lists = {
|
|
60
|
+
metadata: mount.querySelector('.column-list[data-group="metadata"]'),
|
|
61
|
+
markers: mount.querySelector('.column-list[data-group="markers"]'),
|
|
62
|
+
};
|
|
63
|
+
const rolesMount = mount.querySelector('.column-roles');
|
|
64
|
+
|
|
65
|
+
function item(name) {
|
|
66
|
+
const li = document.createElement('li');
|
|
67
|
+
li.className = 'column-chip';
|
|
68
|
+
li.dataset.column = name;
|
|
69
|
+
li.textContent = name;
|
|
70
|
+
return li;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renderList(group, names) {
|
|
74
|
+
lists[group].replaceChildren(...names.map(item));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderRoles() {
|
|
78
|
+
// Rebuilt whenever the metadata box changes: a role can only name a
|
|
79
|
+
// column that is in it, and dragging a column out has to clear any
|
|
80
|
+
// role pointing at it rather than leave a dangling name.
|
|
81
|
+
rolesMount.replaceChildren(...roleNames.map((role) => {
|
|
82
|
+
const field = document.createElement('label');
|
|
83
|
+
field.className = 'column-role';
|
|
84
|
+
field.textContent = roleLabels[role];
|
|
85
|
+
|
|
86
|
+
const select = document.createElement('select');
|
|
87
|
+
select.className = 'form-select';
|
|
88
|
+
select.dataset.role = role;
|
|
89
|
+
select.append(new Option('—', ''));
|
|
90
|
+
metadata.forEach((name) => {
|
|
91
|
+
select.append(new Option(name, name, false, roles[role] === name));
|
|
92
|
+
});
|
|
93
|
+
if (roles[role] && !metadata.includes(roles[role])) {
|
|
94
|
+
roles[role] = null;
|
|
95
|
+
}
|
|
96
|
+
select.addEventListener('change', () => {
|
|
97
|
+
roles[role] = select.value || null;
|
|
98
|
+
options.onChange?.();
|
|
99
|
+
});
|
|
100
|
+
field.appendChild(select);
|
|
101
|
+
return field;
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readLists() {
|
|
106
|
+
metadata = [...lists.metadata.children].map((li) => li.dataset.column);
|
|
107
|
+
markers = [...lists.markers.children].map((li) => li.dataset.column);
|
|
108
|
+
renderRoles();
|
|
109
|
+
options.onChange?.();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
renderList('metadata', metadata);
|
|
113
|
+
renderList('markers', markers);
|
|
114
|
+
renderRoles();
|
|
115
|
+
|
|
116
|
+
// One shared group name is what makes the two lists exchange items;
|
|
117
|
+
// without it each list only reorders within itself.
|
|
118
|
+
Object.values(lists).forEach((list) => {
|
|
119
|
+
sortables.push(new Sortable(list, {
|
|
120
|
+
group: 'plexora-columns',
|
|
121
|
+
animation: 150,
|
|
122
|
+
ghostClass: 'column-chip-ghost',
|
|
123
|
+
onSort: readLists,
|
|
124
|
+
}));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
value() {
|
|
129
|
+
// Only the roles this instance actually drew. A caller that
|
|
130
|
+
// narrows `roleLabels` is saying "I am not the surface that
|
|
131
|
+
// asks about the rest" -- and echoing an unrendered role back
|
|
132
|
+
// would have the server record it as confirmed, retiring a
|
|
133
|
+
// question nobody was ever shown.
|
|
134
|
+
const answered = Object.fromEntries(
|
|
135
|
+
roleNames.map((role) => [role, roles[role] ?? null]));
|
|
136
|
+
return { markers: [...markers], metadata: [...metadata], roles: answered };
|
|
137
|
+
},
|
|
138
|
+
destroy() {
|
|
139
|
+
sortables.forEach((s) => s.destroy());
|
|
140
|
+
mount.innerHTML = '';
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { mount };
|
|
146
|
+
})();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "where are the cell coordinates" control.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the requirements modal and the project edit page, the same way
|
|
5
|
+
* columnClassifier.js is -- both ask this question and neither should own a
|
|
6
|
+
* private version of the answer shape, which is a read spec the server has to
|
|
7
|
+
* be able to apply either way.
|
|
8
|
+
*
|
|
9
|
+
* Why it is one control rather than the two column selects it replaces:
|
|
10
|
+
*
|
|
11
|
+
* A file can put a cell's position in either of two places. `obs` holds it as
|
|
12
|
+
* a pair of columns, which two selects express fine. `obsm` holds it as one
|
|
13
|
+
* (n, 2) array holding BOTH axes -- and "which column is X" has no answer for
|
|
14
|
+
* that. So the obsm case could previously only appear as a blank option with
|
|
15
|
+
* the detected key printed on it: visible, unpickable, never confirmed. The
|
|
16
|
+
* source got decided by matching the key name against a hardcoded list.
|
|
17
|
+
*
|
|
18
|
+
* That is not a safe way to decide it. A store routinely carries `spatial`
|
|
19
|
+
* and `X_umap` as identically-shaped (n, 2) float32 arrays, so nothing about
|
|
20
|
+
* the data separates a position from an embedding -- and a file whose UMAP
|
|
21
|
+
* happens to be named `spatial` would have had every cell drawn in UMAP space
|
|
22
|
+
* with nothing said. Hence: list every candidate with its shape, and let the
|
|
23
|
+
* person who knows the file choose.
|
|
24
|
+
*
|
|
25
|
+
* mount() returns { value() }, where value() is the payload the server takes:
|
|
26
|
+
*
|
|
27
|
+
* { source: "obsm", obsm_key: "spatial" }
|
|
28
|
+
* { source: "obs", x_column: "X_centroid", y_column: "Y_centroid" }
|
|
29
|
+
*
|
|
30
|
+
* or null while the choice is incomplete, so a caller can tell "not answered
|
|
31
|
+
* yet" from "answered" without inspecting the controls.
|
|
32
|
+
*/
|
|
33
|
+
window.PlexoraCoordinateField = (function () {
|
|
34
|
+
|
|
35
|
+
function el(tag, className, text) {
|
|
36
|
+
const node = document.createElement(tag);
|
|
37
|
+
if (className) node.className = className;
|
|
38
|
+
if (text) node.textContent = text;
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `obsm["spatial"] · 42609 × 2` -- the name, and the shape that is the
|
|
43
|
+
* only other thing on offer to tell candidates apart. */
|
|
44
|
+
function describe(entry) {
|
|
45
|
+
const shape = (entry.shape || []).join(" × ");
|
|
46
|
+
return shape ? `obsm["${entry.name}"] · ${shape}` : `obsm["${entry.name}"]`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param container element to render into
|
|
51
|
+
* @param options { obsm: [{name, shape}], obs: [name], current: {...},
|
|
52
|
+
* name: unique radio-group name, onChange(value) }
|
|
53
|
+
*/
|
|
54
|
+
function mount(container, options) {
|
|
55
|
+
const obsmEntries = options.obsm || [];
|
|
56
|
+
const obsColumns = options.obs || [];
|
|
57
|
+
const current = options.current || {};
|
|
58
|
+
const onChange = options.onChange || function () {};
|
|
59
|
+
const groupName = options.name || "coordinates";
|
|
60
|
+
|
|
61
|
+
const root = el("div", "coordinate-field");
|
|
62
|
+
|
|
63
|
+
const obsmSelect = el("select", "form-select");
|
|
64
|
+
obsmSelect.append(new Option("Choose an array…", ""));
|
|
65
|
+
obsmEntries.forEach((entry) => {
|
|
66
|
+
obsmSelect.append(new Option(describe(entry), entry.name));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const obsRow = el("div", "coordinate-field-pair");
|
|
70
|
+
const xSelect = el("select", "form-select");
|
|
71
|
+
const ySelect = el("select", "form-select");
|
|
72
|
+
[[xSelect, "X column…"], [ySelect, "Y column…"]].forEach(([select, blank]) => {
|
|
73
|
+
select.append(new Option(blank, ""));
|
|
74
|
+
obsColumns.forEach((name) => select.append(new Option(name, name)));
|
|
75
|
+
obsRow.appendChild(select);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let source = current.source === "obs" ? "obs" : "obsm";
|
|
79
|
+
|
|
80
|
+
function value() {
|
|
81
|
+
if (source === "obsm") {
|
|
82
|
+
return obsmSelect.value
|
|
83
|
+
? { source: "obsm", obsm_key: obsmSelect.value } : null;
|
|
84
|
+
}
|
|
85
|
+
return (xSelect.value && ySelect.value)
|
|
86
|
+
? { source: "obs", x_column: xSelect.value, y_column: ySelect.value }
|
|
87
|
+
: null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function show(next) {
|
|
91
|
+
source = next;
|
|
92
|
+
obsmSelect.hidden = next !== "obsm";
|
|
93
|
+
obsRow.hidden = next !== "obs";
|
|
94
|
+
onChange(value());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A radio rather than a third dropdown: the choice changes what the
|
|
98
|
+
// control below it is asking, and the user has to see both places at
|
|
99
|
+
// once to know the other one exists.
|
|
100
|
+
const radios = el("div", "coordinate-field-source");
|
|
101
|
+
[["obsm", ".obsm"], ["obs", ".obs"]].forEach(([choice, label]) => {
|
|
102
|
+
const wrapper = el("label", "coordinate-field-radio");
|
|
103
|
+
const input = document.createElement("input");
|
|
104
|
+
input.type = "radio";
|
|
105
|
+
input.name = groupName;
|
|
106
|
+
input.value = choice;
|
|
107
|
+
input.checked = choice === source;
|
|
108
|
+
input.addEventListener("change", () => {
|
|
109
|
+
if (input.checked) show(choice);
|
|
110
|
+
});
|
|
111
|
+
wrapper.appendChild(input);
|
|
112
|
+
wrapper.appendChild(el("span", null, label));
|
|
113
|
+
radios.appendChild(wrapper);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (current.obsm_key) obsmSelect.value = current.obsm_key;
|
|
117
|
+
if (current.x_column) xSelect.value = current.x_column;
|
|
118
|
+
if (current.y_column) ySelect.value = current.y_column;
|
|
119
|
+
[obsmSelect, xSelect, ySelect].forEach((select) => {
|
|
120
|
+
select.addEventListener("change", () => onChange(value()));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
root.appendChild(radios);
|
|
124
|
+
root.appendChild(obsmSelect);
|
|
125
|
+
root.appendChild(obsRow);
|
|
126
|
+
container.appendChild(root);
|
|
127
|
+
|
|
128
|
+
show(source);
|
|
129
|
+
return { value };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { mount };
|
|
133
|
+
}());
|