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.
Files changed (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,947 @@
1
+ /**
2
+ * @class CSVGatingList - A view to select and deselect channels for gating, and to set gates (filter ranges) in the tabular data
3
+ */
4
+ class CSVGatingList {
5
+
6
+ /**
7
+ * @constructor
8
+ * @param config the cinfiguration file (json)
9
+ * @param columns - all the channel names
10
+ * @param dataLayer - the data layer (stub) that executes server requests and holds client side data
11
+ * @param eventHandler - the event handler for distributing interface and data updates
12
+ */
13
+ constructor(ctx) {
14
+ // Everything arrives through the plugin context. This class used to
15
+ // read `__plexora`, `imageChannels` and `datasource` straight off
16
+ // window, which tied it to core's global names and to script order.
17
+ this.ctx = ctx;
18
+ this.config = ctx.config;
19
+ this.columns = [...ctx.columns];
20
+ this.databaseDescription = {};
21
+ this.maxSelections = ctx.config.maxSelections;
22
+ this.eventHandler = ctx.eventHandler;
23
+ this.dataLayer = ctx.dataLayer;
24
+ this.api = new GatingApi(ctx);
25
+ this.dataset = ctx.dataset;
26
+ this.datasource = ctx.datasource;
27
+ this.selections = {};
28
+ this.hasGatingGMM = {};
29
+ this.gatingIDs = {};
30
+ this.sliders = new Map();
31
+ this.container = d3.select("#csv_gating_list");
32
+ // Gating vars
33
+ this.global_channel_list = ctx.channelList;
34
+ this.global_image_channels = ctx.dataset.image.index;
35
+ this.gating_default_range = [0, 65536];
36
+ this.gating_channels = this.initGatingChannels();
37
+ this.gating_list = null;
38
+ // Download vars
39
+ this.download_panel_visible = false;
40
+ this.download_input1 = null;
41
+ this.download_input2 = null;
42
+ // Eval settings
43
+ this.eval_mode = 'and'
44
+ }
45
+
46
+ /**
47
+ * Selects a channel as active and adds the respective visual components to the channel panel in the list view
48
+ * @param name - the channel to set and display as selected
49
+ */
50
+ selectChannel(name) {
51
+ const fullName = this.dataLayer.getFullChannelName(name);
52
+ const values = this.gating_channels[fullName];
53
+ this.selections[fullName] = values;
54
+ this.sliders.get(name).value(values);
55
+ this.eventHandler.trigger(CSVGatingList.events.GATING_BRUSH_MOVE, this.selections);
56
+ if (!(name in this.hasGatingGMM)) {
57
+ this.getAndDrawGatingGMM(name).then(() => {
58
+ this.eventHandler.trigger(CSVGatingList.events.SELECTION_CHANGED, this.selections);
59
+ });
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Removes a channel form the current selection
65
+ * @param name - the name of the channel to remove
66
+ */
67
+ removeChannel(name) {
68
+ // Delete
69
+ const fullName = this.dataLayer.getFullChannelName(name);
70
+ delete this.selections[fullName];
71
+
72
+ // Trigger
73
+ this.eventHandler.trigger(CSVGatingList.events.SELECTION_CHANGED, this.selections);
74
+ }
75
+
76
+ /**
77
+ * initializes the view (channel list)
78
+ * @param dd - database description
79
+ * @param seaDragonViewer - the ImageViewer instance
80
+ * @returns {Promise<void>}
81
+ */
82
+ init(dd, seaDragonViewer) {
83
+ this.databaseDescription = dd;
84
+ this.seaDragonViewer = seaDragonViewer;
85
+ document.getElementById('drag-and-drop-info').style.display = "none";
86
+ // Hide the Loader
87
+ document.getElementById('csv_gating_list_loader').style.display = "none";
88
+ this.gating_list = document.getElementById("csv_gating_list");
89
+ let list = document.createElement("ul");
90
+ list.classList.add("list-group")
91
+ list.setAttribute("id", "gating_list_ul")
92
+ this.gating_list.appendChild(list)
93
+ // What gets a row, and a slider.
94
+ //
95
+ // The project's recorded marker/metadata split, resolved once by core
96
+ // for every plugin (datasetContext.js's table.markers). This used to be
97
+ // derived here -- "every column with a histogram that is not id/x/y" --
98
+ // and that cannot tell a stain from a measurement: Area, Eccentricity
99
+ // and a numeric slide label all have perfectly good histograms, and in
100
+ // a CSV they sit in the same header as the markers, which is the entire
101
+ // reason the import step asks. Gating offered a threshold slider for
102
+ // every one of them.
103
+ //
104
+ // Feature-table columns, never the image channel list. An image
105
+ // channel's display name (from OME-XML/antibody nomenclature) and its
106
+ // feature-table column (adata.var_names/gene symbols, or a plain CSV
107
+ // header) are frequently different strings for the same marker, and
108
+ // every gating query (get_gated_cells, get_gating_gmm) keys directly
109
+ // off the feature-table column. Any correspondence to a specific image
110
+ // channel (if ever needed) would be positional (marker i <-> image
111
+ // channel i), not by matching names.
112
+ this.columns = [...this.dataset.table.markers];
113
+ _.each(this.columns, (column, index) => {
114
+
115
+ let channelID = `channel_${index}`;
116
+ this.gatingIDs[column] = channelID;
117
+ // div for each row in gating list
118
+ let listItemParentDiv = document.createElement("div");
119
+ listItemParentDiv.classList.add("list-group-item");
120
+ listItemParentDiv.classList.add("container");
121
+ listItemParentDiv.classList.add("gating-list-content");
122
+ // row
123
+ let row = document.createElement("div");
124
+ row.classList.add("row");
125
+ listItemParentDiv.appendChild(row);
126
+ // row
127
+ let row2 = document.createElement("div");
128
+ row2.classList.add("row");
129
+ listItemParentDiv.appendChild(row2);
130
+
131
+ // column within row that contains the name of the gating
132
+ let nameCol = document.createElement("div");
133
+ nameCol.classList.add("col-md-4");
134
+ nameCol.classList.add("gating-col");
135
+ row.appendChild(nameCol);
136
+
137
+ // column within row that cintains the slider for the gating
138
+ let sliderCol = document.createElement("div");
139
+ sliderCol.classList.add("col-md-12");
140
+ sliderCol.classList.add("csv_gating-slider");
141
+ sliderCol.setAttribute('id', "csv_gating-slider_" + channelID)
142
+ row2.appendChild(sliderCol);
143
+
144
+ // column within row that contains svg for color pickers
145
+ let svgCol = document.createElement("div");
146
+ svgCol.classList.add("col-md-4");
147
+ svgCol.classList.add("ms-auto");
148
+ svgCol.classList.add("gating-col");
149
+ svgCol.classList.add("gating-svg-wrapper");
150
+ svgCol.classList.add("col-svg-wrapper");
151
+ row.appendChild(svgCol);
152
+
153
+
154
+ let svg = d3.select(svgCol)
155
+ .append("svg")
156
+ .attr("width", 30)
157
+ .attr("height", 15)
158
+ svgCol.style.display = "none";
159
+
160
+ let gatingName = document.createElement("span");
161
+ gatingName.classList.add('gating-name');
162
+ gatingName.classList.add('list-button');
163
+ gatingName.textContent = column;
164
+ nameCol.appendChild(gatingName);
165
+ listItemParentDiv.addEventListener("click", e => {
166
+ return this.toggleChannelPanel(e, svgCol);
167
+ })
168
+ list.appendChild(listItemParentDiv);
169
+
170
+ //add and hide gating sliders (will be visible when gating is active)
171
+ const fullName = this.dataLayer.getFullChannelName(column);
172
+ const sliderRange = [this.databaseDescription[fullName].min, this.databaseDescription[fullName].max];
173
+ this.gating_channels[fullName] = sliderRange;
174
+ const gatingListEl = document.getElementById("csv_gating_list");
175
+ const swidth = gatingListEl.getBoundingClientRect().width;
176
+ this.addSlider(column, swidth, sliderRange, sliderRange);
177
+ d3.select('div#csv_gating-slider_' + channelID).style('display', "none");
178
+
179
+ let autoCol = document.createElement("div");
180
+ autoCol.classList.add("col-md-4");
181
+ autoCol.classList.add("ms-auto");
182
+ autoCol.classList.add("csv_gating-auto")
183
+ autoCol.setAttribute('id', "csv_gating-auto_" + channelID)
184
+ autoCol.classList.add("gating-col");
185
+ autoCol.classList.add("gating-svg-wrapper");
186
+ row.appendChild(autoCol);
187
+
188
+ let autoBtn = document.createElement("button");
189
+ autoBtn.classList.add('auto-btn');
190
+ autoBtn.classList.add('auto-loading');
191
+ autoBtn.setAttribute('id', "auto-btn-gating_" + channelID);
192
+ autoBtn.textContent = "auto";
193
+ autoBtn.addEventListener("click", async () => {
194
+ const shortName = this.dataLayer.getShortChannelName(fullName);
195
+ await this.autoGate(shortName);
196
+ });
197
+
198
+ autoCol.appendChild(autoBtn);
199
+ autoBtn.addEventListener("click", e => e.stopPropagation());
200
+ d3.select(autoCol).style('display', "none");
201
+ });
202
+
203
+ var dropzone = new Dropzone("#csv_gating_list", {
204
+ url: this.ctx.url("plugins/gating/upload_gates"),
205
+ clickable: false,
206
+ disablePreview: true,
207
+ createImageThumbnails: false
208
+ });
209
+ dropzone.on("sending", (file, xhr, formData) => {
210
+ formData.append("datasource", this.datasource);
211
+ });
212
+ dropzone.on("queuecomplete", (file, xhr, formData) => {
213
+ return this.applyGates()
214
+ });
215
+
216
+ // Adding upload when you press on the up arrow
217
+ let arrow = document.getElementById('gating_upload_icon')
218
+ arrow.onclick = () => {
219
+ let elem = document.getElementById('gating-upload-from-arrow');
220
+ if (elem && document.createEvent) {
221
+ let evt = document.createEvent("MouseEvents");
222
+ evt.initEvent("click", true, false);
223
+ elem.dispatchEvent(evt);
224
+ }
225
+ }
226
+ document.getElementById("gating-upload-from-arrow").onchange = async () => {
227
+ if (document.getElementById("gating-upload-from-arrow").files) {
228
+ let file = document.getElementById("gating-upload-from-arrow").files[0]
229
+ let formData = new FormData();
230
+ formData.append("file", file);
231
+ await this.api.submitGatingUpload(formData);
232
+ document.getElementById("gating-upload-from-arrow").value = []
233
+ await this.applyGates('file')
234
+ }
235
+ }
236
+
237
+
238
+ // Adding dropzone for CSV_Gating_List
239
+ let parent = document.getElementById('csv_gating_list');
240
+ let rect = parent.getBoundingClientRect();
241
+ parent.addEventListener("dragover", (ev) => {
242
+ document.getElementById('gating_list_ul').style.display = "none";
243
+ document.getElementById('drag-and-drop-info').style.display = "block";
244
+ })
245
+ parent.addEventListener("dragleave", (ev) => {
246
+ if (ev.x > rect.left + rect.width || ev.x < rect.left
247
+ || ev.y > rect.top + rect.height || ev.y < rect.top) {
248
+ document.getElementById('gating_list_ul').style.display = "block";
249
+ document.getElementById('drag-and-drop-info').style.display = "none";
250
+ }
251
+ })
252
+ parent.addEventListener("drop", (ev) => {
253
+ document.getElementById('gating_list_ul').style.display = "block";
254
+ document.getElementById('drag-and-drop-info').style.display = "none";
255
+ })
256
+
257
+ // Add events
258
+ this.addDownloadEvents();
259
+ this.addEventsLinked();
260
+ }
261
+
262
+ /**
263
+ * @function applyGates
264
+ * Applies settings (from file or db) to the gates in the tool
265
+ * @parms {String} source Whether it is from new file upload or saved
266
+ */
267
+ async applyGates(source) {
268
+ let gates;
269
+ if (source === 'file') {
270
+ gates = await this.api.getUploadedGatingCsvValues();
271
+ } else {
272
+ gates = await this.api.getSavedGatingList();
273
+ }
274
+
275
+ this.eventHandler.trigger(CSVGatingList.events.RESET_GATINGLIST)
276
+ _.each(gates, async (col) => {
277
+ if (col.channel == 'Lasso') {
278
+ // Lasso-drawn regions are no longer supported; ignore any
279
+ // legacy 'Lasso' rows from older saved/exported gate lists.
280
+ } else {
281
+ let shortName = this.dataLayer.getShortChannelName(col.channel);
282
+ let channelID = this.gatingIDs[shortName];
283
+ if (this.sliders.get(shortName)) {
284
+ let toggle_off
285
+ if (!col.gate_active && col.channel in this.selections) {
286
+ toggle_off = true;
287
+ } else {
288
+ toggle_off = false;
289
+ }
290
+ this.gating_channels[col.channel] = [col.gate_start, col.gate_end];
291
+ if (col.gate_active) {
292
+ // IF the channel isn't active, make it so
293
+ if (!this.selections[col.channel]) {
294
+ let selector = `#csv_gating-slider_${channelID}`;
295
+ document.querySelector(selector).click();
296
+ }
297
+ this.selections[col.channel] = [col.gate_start, col.gate_end];
298
+
299
+ // Update the slider values to reflect the new gate
300
+ const slider = this.sliders.get(shortName);
301
+ if (slider) {
302
+ // Apply the same data type conversion as in addSlider
303
+ const fullName = this.dataLayer.getFullChannelName(shortName);
304
+ const channelRange = [this.databaseDescription[fullName].min, this.databaseDescription[fullName].max];
305
+ const factor = Math.pow(10, this.dataLayer.gateDecimals(channelRange));
306
+ const v0 = Math.floor(parseFloat(col.gate_start) * factor) / factor;
307
+ const v1 = Math.ceil(parseFloat(col.gate_end) * factor) / factor;
308
+
309
+ slider.silentValue([v0, v1]);
310
+ // Update the input fields
311
+ d3.select('#gating_slider-input_' + channelID + '_0').attr('value', v0);
312
+ d3.select('#gating_slider-input_' + channelID + '_0').property('value', v0);
313
+ d3.select('#gating_slider-input_' + channelID + '_1').attr('value', v1);
314
+ d3.select('#gating_slider-input_' + channelID + '_1').property('value', v1);
315
+ }
316
+
317
+ } else {
318
+ // If channel is currently active, but shouldn't be, update it
319
+ if (toggle_off) {
320
+ let selector = `#csv_gating-slider_${channelID}`;
321
+ document.querySelector(selector).click();
322
+ }
323
+ delete this.selections[col.channel];
324
+ }
325
+ }
326
+ }
327
+ })
328
+ // Trigger brush
329
+ this.eventHandler.trigger(CSVGatingList.events.SELECTION_CHANGED, this.selections);
330
+ }
331
+
332
+ /**
333
+ * @function autoGate - applies thresholds based on Gaussian Mixture Model
334
+ * @param name - the name of the channel to apply it to
335
+ */
336
+ async autoGate(shortName) {
337
+ const fullName = this.dataLayer.getFullChannelName(shortName);
338
+ // A column with no mixture to find -- a flag, a constant, a nearly
339
+ // empty channel -- comes back without a gate rather than with one
340
+ // derived from noise. Leave the slider where the user put it.
341
+ const packet = this.hasGatingGMM[shortName];
342
+ if (packet?.gate === undefined) return;
343
+ const input = packet['gate'].toFixed(7);
344
+ const channelRange = [this.databaseDescription[fullName].min, this.databaseDescription[fullName].max];
345
+ const factor = Math.pow(10, this.dataLayer.gateDecimals(channelRange));
346
+ const gate = Math.floor(parseFloat(input) * factor) / factor;
347
+ if (fullName in this.selections) {
348
+ const channelID = this.gatingIDs[shortName];
349
+ const gate_end = this.selections[fullName][1];
350
+ const slider = this.sliders.get(shortName);
351
+ const values = [gate, gate_end];
352
+ this.moveSliderHandles(slider, values, shortName, 'SELECTION_CHANGED');
353
+ d3.select('#gating_slider-input_' + channelID + '_0').attr('value', gate)
354
+ d3.select('#gating_slider-input_' + channelID + '_0').property('value', gate);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * @function initGatingChannels - creates the data structure for channels
360
+ * @return obj - allChannels and their default range
361
+ */
362
+ initGatingChannels() {
363
+
364
+ // Init
365
+ const obj = {};
366
+
367
+ // Iterate to create fields
368
+ for (let key in this.global_image_channels) {
369
+
370
+ obj[key] = this.gating_default_range;
371
+ }
372
+
373
+ // Return
374
+ return obj;
375
+ }
376
+
377
+ /**
378
+ * @function toggleChannelPane - expands or collapses a channel panel in the list that was clicked on
379
+ * @param event - the click vent
380
+ * @param svgCol - the column to expand or collapse
381
+ */
382
+ toggleChannelPanel(event, svgCol) {
383
+
384
+ // If you clicked on the svg, ignore this behavior
385
+ if (event.target.closest("svg")) {
386
+ return;
387
+ }
388
+
389
+ // Get info
390
+ let parent = event.target.closest(".list-group-item");
391
+ let name = parent.querySelector('.gating-name').textContent;
392
+ let channelID = this.gatingIDs[name];
393
+ let status = !parent.classList.contains("active");
394
+
395
+ // If active - else inactive
396
+ if (status) {
397
+
398
+ // Clear everything
399
+ // clearOut();
400
+
401
+ // Don't add gating is the max are selected
402
+ if (_.size(this.selections) >= this.maxSelections) {
403
+ return;
404
+ }
405
+
406
+ // Update properties and add slider
407
+ d3.select(parent).classed("active", true);
408
+ svgCol.style.display = "block";
409
+ d3.select('div#csv_gating-slider_' + channelID).style('display', "block")
410
+ d3.select('div#csv_gating-auto_' + channelID).style('display', "block");
411
+
412
+ // Add channel
413
+ this.selectChannel(name);
414
+
415
+ } else {
416
+ // Clear panel visibility
417
+ // clearOut();
418
+
419
+ // Remove channel and rerender
420
+ this.removeChannel(name);
421
+
422
+ // Hide
423
+ d3.select(parent).classed("active", false);
424
+ svgCol.style.display = "none";
425
+ d3.select('div#csv_gating-slider_' + channelID).style('display', "none")
426
+ d3.select('div#csv_gating-auto_' + channelID).style('display', "none");
427
+ }
428
+
429
+ let selectionsHeaderDiv = document.getElementById("csv_selected-gatings-header-div");
430
+ if (selectionsHeaderDiv) {
431
+ if (_.size(this.selections) >= this.maxSelections) {
432
+ selectionsHeaderDiv.classList.add('bold-selections-header');
433
+ } else {
434
+ selectionsHeaderDiv.classList.remove('bold-selections-header');
435
+ }
436
+ document.getElementById("csv_num-selected-gatings").textContent = _.size(this.selections);
437
+ }
438
+ }
439
+
440
+ /**
441
+ * @function addDownloadEvents - adds eventl listeners an functionality to the download buttons
442
+ */
443
+ addDownloadEvents() {
444
+
445
+ // Els
446
+ const gating_download_icon = document.querySelector('#gating_download_icon');
447
+ const gating_download_panel = document.querySelector('#gating_download_panel');
448
+ const gating_exit = document.querySelector('#gating_exit');
449
+ const download_gated_channel_ranges = document.querySelector('#download_gated_channel_ranges');
450
+ const download_gated_cell_encodings = document.querySelector('#download_gated_cell_encodings');
451
+ const download_input1 = document.querySelector('#download_input1');
452
+ const download_input2 = document.querySelector('#download_input2');
453
+
454
+ // Events ::
455
+
456
+ // Open / close download panel
457
+ gating_download_icon.addEventListener('click', () => {
458
+ // Update class var
459
+ this.download_panel_visible = !this.download_panel_visible;
460
+ // Condition to update download panel visibility
461
+ if (this.download_panel_visible) {
462
+ gating_download_panel.style.visibility = 'visible';
463
+ } else {
464
+ gating_download_panel.style.visibility = 'hidden';
465
+ }
466
+ });
467
+
468
+ // Close download panel
469
+ gating_exit.addEventListener('click', () => {
470
+ // Update class var
471
+ this.download_panel_visible = !this.download_panel_visible;
472
+ // Hide download panel
473
+ gating_download_panel.style.visibility = 'hidden';
474
+ });
475
+
476
+ // Download gated channel ranges
477
+ download_gated_channel_ranges.addEventListener('click', () => {
478
+ this.api.downloadGatingCSV(this.gating_channels, this.selections, false);
479
+ })
480
+
481
+ // Download gated channel ranges
482
+ download_gated_cell_encodings.addEventListener('click', () => {
483
+ this.api.downloadGatingCSV(this.gating_channels, this.selections, this.seaDragonViewer.pickedIds, true);
484
+ })
485
+
486
+ }
487
+
488
+
489
+ /**
490
+ * @function addEventsLinked
491
+ * ???
492
+ */
493
+ addEventsLinked() {
494
+
495
+ // Add events to channel
496
+ const channelListContent = document.querySelectorAll('.channel-list-content');
497
+ const gatingListContent = document.querySelectorAll('.gating-list-content');
498
+
499
+ // Attach
500
+ // Gating markers (adata.var_names) and image channels are matched
501
+ // by name only when the strings happen to coincide -- the two lists
502
+ // are frequently different lengths (e.g. structural channels like
503
+ // DNA/AF1 have no marker, or var_names uses gene symbols that don't
504
+ // match the image's antibody-named channels), so this can never be
505
+ // a positional/index link. When a gating marker is selected and its
506
+ // exact name also appears in the image channel list, auto-open that
507
+ // channel for convenience; when there's no match, nothing happens
508
+ // and the user opens the right channel manually.
509
+ const attach = (targets, matches, target_class, match_class) => {
510
+ targets.forEach(cLC => {
511
+ cLC.addEventListener('click', e => {
512
+
513
+ // If event target is not an svg el (from slider)
514
+ const svgEls = ['path']
515
+ if (!svgEls.includes(e.currentTarget.tagName)) {
516
+
517
+ // Get channel name
518
+ const name = _.get(e.currentTarget.querySelector(`.${target_class}`), 'innerText');
519
+
520
+ // Find match el in channel list
521
+ if (name) {
522
+ const match = Array.from(matches).find(
523
+ row => row.querySelector(`.${match_class}`).innerText === name);
524
+
525
+ // Real click so it goes through ChannelList's own
526
+ // toggleChannelPanel handler (already bound per-row
527
+ // at row-creation time) -- no need to reconstruct
528
+ // its event/closure state here.
529
+ if (match && !Array.from(match.classList).includes('active')) {
530
+ match.click();
531
+ }
532
+ }
533
+ }
534
+ });
535
+ });
536
+ }
537
+ attach(gatingListContent, channelListContent, 'gating-name', 'channel-name');
538
+
539
+ }
540
+
541
+ /**
542
+ * @function addSlider - add a slider
543
+ * @param data - the min and max range of the slider
544
+ * @param activeRange - the predefined values for the lower and upper handle
545
+ * @param name - the name of the slider (used as part of the id)
546
+ * @param swidth - the pixel width of the slider
547
+ */
548
+ addSlider(name, swidth, data, activeRange) {
549
+
550
+ if (!data) return;
551
+
552
+ const fullName = this.dataLayer.getFullChannelName(name);
553
+ const { xDomain, yDomain, histogramData } = this.histogramData(fullName);
554
+ let channelID = this.gatingIDs[name];
555
+
556
+ // data is already [min, max] (see call sites) -- gateDecimals derives
557
+ // slider precision from that observed span instead of the old
558
+ // isTransformed config flag (see dataLayer.gateDecimals for why).
559
+ const channelRange = [d3.min(data), d3.max(data)];
560
+ const gateFactor = Math.pow(10, this.dataLayer.gateDecimals(channelRange));
561
+ const data_min = Math.floor(channelRange[0] * gateFactor) / gateFactor;
562
+ const data_max = Math.ceil(channelRange[1] * gateFactor) / gateFactor;
563
+ const handle_min = Math.floor(parseFloat(activeRange[0]) * gateFactor) / gateFactor;
564
+ const handle_max = Math.ceil(parseFloat(activeRange[1]) * gateFactor) / gateFactor;
565
+ let f = d3.format("d")
566
+ //add range slider row content
567
+ const sliderSimple = d3.sliderBottom()
568
+ .min(data_min)
569
+ .max(data_max)
570
+ .width(swidth - 75)
571
+ .tickFormat(f)
572
+ .fill('orange')
573
+ .ticks(1)
574
+ .default([handle_min, handle_max])
575
+ .handle(
576
+ d3.symbol()
577
+ .type(d3.symbolCircle)
578
+ .size(100))
579
+ .tickValues([])
580
+ .on('end', (range) => {
581
+ const v0 = Math.floor(parseFloat(range[0]) * gateFactor) / gateFactor;
582
+ const v1 = Math.ceil(parseFloat(range[1]) * gateFactor) / gateFactor;
583
+ this.moveSliderHandles(sliderSimple, [v0, v1], name, "SELECTION_CHANGED");
584
+ }).on('onchange', (range) => {
585
+ const v0 = Math.floor(parseFloat(range[0]) * gateFactor) / gateFactor;
586
+ const v1 = Math.ceil(parseFloat(range[1]) * gateFactor) / gateFactor;
587
+ d3.select('#gating_slider-input_' + channelID + '_0').attr('value', v0)
588
+ d3.select('#gating_slider-input_' + channelID + '_0').property('value', v0);
589
+ d3.select('#gating_slider-input_' + channelID + '_1').attr('value', v1);
590
+ d3.select('#gating_slider-input_' + channelID + '_1').property('value', v1);
591
+ this.moveSliderHandles(sliderSimple, [v0, v1], name, "GATING_BRUSH_MOVE");
592
+ });
593
+
594
+ this.sliders.set(name, sliderSimple);
595
+
596
+ //create the slider svg and call the slider
597
+ var gSimple = d3
598
+ .select('#csv_gating-slider_' + channelID)
599
+ .append('svg')
600
+ .attr('class', 'svgslider')
601
+ .attr('id', 'csv_gating-slider_svg_' + channelID)
602
+ .attr('width', swidth)
603
+ .attr('height', 80)
604
+ .append('g')
605
+ .attr('transform', 'translate(20,40)');
606
+
607
+ let xScale = d3.scaleLinear()
608
+ .domain(xDomain)
609
+ .range([0, swidth - 73])
610
+
611
+ let yScale = d3.scaleLinear()
612
+ .domain(yDomain)
613
+ .range([0, 25])
614
+
615
+ let line = d3.line()
616
+ .x(d => xScale(d.x))
617
+ .y(d => yScale(d.y))
618
+ .curve(d3.curveMonotoneX)
619
+
620
+ const lines = gSimple.selectAll('.distribution_line');
621
+ const paths = lines.data([histogramData]).enter().append('path');
622
+ paths
623
+ .append('path')
624
+ .attr('d', line)
625
+ .attr('class', 'distribution_line')
626
+ .attr('transform', 'translate(0,-31)')
627
+ .attr('fill', 'none')
628
+
629
+ gSimple.call(sliderSimple);
630
+
631
+ //slider value to be displayed closer to the slider than default
632
+ d3.selectAll('.parameter-value').select('text')
633
+ .attr("y", 10);
634
+
635
+ //both handles
636
+ const { sliders } = this;
637
+ const handles = d3.select('#csv_gating-slider_' + channelID).selectAll(".parameter-value");
638
+ handles.each(function (d, i) {
639
+ d3.select(this).append("foreignObject")
640
+ .attr('id', 'c_foreignObject_' + channelID + i)
641
+ .attr("width", 50)
642
+ .attr("height", 40)
643
+ .attr('x', -25)
644
+ .attr('y', -17)
645
+ .style('padding', "10px")
646
+ .append("xhtml:body")
647
+ .attr('xmlns', 'http://www.w3.org/1999/xhtml')
648
+ .style('background', 'none')
649
+ .append('input')
650
+ .attr('y', -17)
651
+ .attr('id', 'gating_slider-input_' + channelID + '_' + i)
652
+ .attr('type', 'text')
653
+ .attr('class', 'input')
654
+ .attr('value', () => {
655
+ return sliders.get(name).value()[i]
656
+ });
657
+ //remove the previous text label
658
+ d3.select(this).select('text').remove();
659
+ });
660
+
661
+ //entering a value in the input field of a slider handle
662
+ const moveSliderHandles = this.moveSliderHandles.bind(this);
663
+ handles.selectAll('.input').on('keydown', function (event, d) {
664
+ // Note: `this` here is the DOM <input> (d3's `function` handler
665
+ // convention, needed below for `this.value`) -- gateFactor is
666
+ // captured from the enclosing addSlider closure instead of
667
+ // going through `this.dataLayer`, which doesn't exist on a DOM
668
+ // node (a pre-existing bug: this handler would have thrown on
669
+ // every Enter keypress before gateFactor existed to close over).
670
+ if (event.key == "Enter") {
671
+ const val = Math.round(parseFloat(this.value.replace("%", "")) * gateFactor) / gateFactor;
672
+ const vals = sliderSimple.silentValue();
673
+ vals[d.index] = val;
674
+ moveSliderHandles(sliderSimple, vals, name, "SELECTION_CHANGED");
675
+ }
676
+ })
677
+
678
+ return sliderSimple;
679
+ };
680
+
681
+ histogramData(fullName) {
682
+ const histogramData = this.databaseDescription[fullName].histogram;
683
+ const xMin = Math.floor(Math.min(...histogramData.map(e => e.x)));
684
+ const xMax = Math.ceil(Math.max(...histogramData.map(e => e.x)));
685
+ const yMax = Math.max(...histogramData.map(e => e.y));
686
+ return {
687
+ histogramData,
688
+ xDomain: [xMin, xMax],
689
+ yDomain: [yMax, 0],
690
+ };
691
+ }
692
+
693
+ async getGatingGMM(name, selection_ids = []) {
694
+ const fullName = this.dataLayer.getFullChannelName(name);
695
+ let packet = await this.api.getGatingGMM(fullName, selection_ids);
696
+ this.hasGatingGMM[name] = packet;
697
+ return packet;
698
+ }
699
+
700
+ drawGatingGMM(name) {
701
+ let channelID = this.gatingIDs[name];
702
+ const fullName = this.dataLayer.getFullChannelName(name);
703
+ const { xDomain, yDomain } = this.histogramData(fullName);
704
+ const packet = this.hasGatingGMM[name];
705
+ let gmm1Data = packet['gmm_1'];
706
+ let gmm2Data = packet['gmm_2'];
707
+
708
+ const gatingListEl = document.getElementById("csv_gating_list");
709
+ const swidth = gatingListEl.getBoundingClientRect().width;
710
+
711
+ let xScale = d3.scaleLinear()
712
+ .domain(xDomain)
713
+ .range([0, swidth - 73])
714
+
715
+ let yScale = d3.scaleLinear()
716
+ .domain(yDomain)
717
+ .range([0, 25])
718
+
719
+ let line = d3.line()
720
+ .x(d => xScale(d.x))
721
+ .y(d => yScale(d.y))
722
+ .curve(d3.curveMonotoneX)
723
+
724
+ let gSimple = d3.select('#csv_gating-slider_svg_' + channelID + ' g')
725
+
726
+ gSimple.selectAll('.gmm1_line')
727
+ .data([gmm1Data])
728
+ .enter()
729
+ .append('path')
730
+ .attr('d', line)
731
+ .attr('class', 'gmm_line')
732
+ .attr('class', 'gmm_line_' + name)
733
+ .attr('id', 'gmm1_line_' + name)
734
+ .attr('transform', 'translate(0,-31)')
735
+ .attr('fill', 'none')
736
+ .attr('stroke', 'blue')
737
+
738
+ gSimple.selectAll('.gmm2_line')
739
+ .data([gmm2Data])
740
+ .enter()
741
+ .append('path')
742
+ .attr('d', line)
743
+ .attr('class', 'gmm_line')
744
+ .attr('class', 'gmm_line_' + name)
745
+ .attr('id', 'gmm2_line_' + name)
746
+ .attr('transform', 'translate(0,-31)')
747
+ .attr('fill', 'none')
748
+ .attr('stroke', 'red')
749
+ }
750
+
751
+ async getAndDrawGatingGMM(name) {
752
+ await this.getGatingGMM(name);
753
+
754
+ const channelID = this.gatingIDs[name];
755
+ const autoBtn = document.getElementById(`auto-btn-gating_${channelID}`);
756
+ autoBtn.classList.remove("auto-loading")
757
+
758
+ this.drawGatingGMM(name);
759
+ }
760
+
761
+ /**
762
+ * @function resetGatingList - resets all channels in the list to its initial range
763
+ */
764
+ resetGatingList() {
765
+ let gatingList = Object.keys(this.selections);
766
+ _.each(gatingList, col => {
767
+ let shortName = this.dataLayer.getShortChannelName(col);
768
+ let channelID = this.gatingIDs[shortName];
769
+ let gating_selector = `#csv_gating-slider_${channelID}`;
770
+ document.querySelector(gating_selector).click();
771
+ });
772
+ };
773
+
774
+ /**
775
+ * @function moveSliderHandles - move the slider handles and input fields so that input fields don't overlap when handles are close
776
+ *
777
+ * @param slider - the slider affected
778
+ * @param vals - holds the new positions
779
+ * @param name - the name of the slider
780
+ * @param eventName - the name of the event
781
+ */
782
+ moveSliderHandles(slider, vals, name, eventName) {
783
+ const fullName = this.dataLayer.getFullChannelName(name);
784
+ const channelID = this.gatingIDs[name];
785
+ this.gating_channels[fullName] = vals;
786
+ this.selections[fullName] = vals;
787
+ slider.silentValue(vals);
788
+ const diff = Math.abs(vals[1] - vals[0]);
789
+ const total = Math.abs(slider.max() - slider.min());
790
+ const percentage = diff / total;
791
+ if (percentage < 0.15) {
792
+ console.log('slider handles overlap..do something');
793
+ d3.select('#c_foreignObject_' + channelID + 1).attr('x', 5);
794
+ } else {
795
+ d3.select('#c_foreignObject_' + channelID + 1).attr('x', -25);
796
+ }
797
+ const packet = this.selections;
798
+ this.eventHandler.trigger(CSVGatingList.events[eventName], packet);
799
+ }
800
+
801
+ /**
802
+ * @function dist - caclulates the distance between two rects
803
+ * @param el1
804
+ * @param el2
805
+ * @param buffer
806
+ * @returns {number}
807
+ */
808
+ dist(el1, el2, buffer) {
809
+ var rect1 = el1.getBoundingClientRect();
810
+ var rect2 = el2.getBoundingClientRect();
811
+ return rect2.left - rect1.right;
812
+ }
813
+
814
+ // ==== ImageViewer cell-layer provider contract (see imageViewer.js) ====
815
+ // Replaces ImageViewer's former direct dataLayer.getGatedCellIds() calls --
816
+ // the gating-specific "gates" shape now stays entirely on this side of the seam.
817
+
818
+ /**
819
+ * @function rebuildSliders - re-measure and redraw every legacy-list slider.
820
+ *
821
+ * Slider widths come from a measured bounding box, so a window resize
822
+ * invalidates them. Called by the resize listener the plugin registers in
823
+ * bindEvents(), which is torn down with the plugin -- this used to be a
824
+ * script-scope listener reading a global, with no way to detach it.
825
+ */
826
+ rebuildSliders() {
827
+ const gatingListEl = document.getElementById("csv_gating_list");
828
+ if (!gatingListEl) return;
829
+ const swidth = gatingListEl.getBoundingClientRect().width;
830
+ this.sliders.forEach((slider, name) => {
831
+ const gatingID = this.gatingIDs[name];
832
+ d3.select('div#csv_gating-slider_' + gatingID).select('svg').remove();
833
+ const fullName = this.dataLayer.getFullChannelName(name);
834
+ const sliderRange = [this.databaseDescription[fullName].min, this.databaseDescription[fullName].max];
835
+ this.addSlider(name, swidth, sliderRange, slider.value());
836
+ if (this.hasGatingGMM[name]) {
837
+ this.drawGatingGMM(name);
838
+ }
839
+ });
840
+ }
841
+
842
+ /**
843
+ * @function getSelectedIds - resolve a gates filter to matching cell ids
844
+ * @param filter - gates dict ({channel: [min, max]}); defaults to the
845
+ * currently active selections when omitted
846
+ * @returns {Promise<Set<number>>}
847
+ */
848
+ async getSelectedIds(filter) {
849
+ const gates = filter || this.selections;
850
+ // The role, from ctx.dataset -- the last place this plugin reached into
851
+ // the raw config entry. What the id column is called is core's to know.
852
+ const idField = this.dataset.schema.cellId;
853
+ const rows = await this.api.getGatedCellIds(gates, [idField]);
854
+ if (!Array.isArray(rows)) return new Set();
855
+ return new Set(rows.map((row) => Number(row[idField] ?? row.id)));
856
+ }
857
+
858
+ /**
859
+ * @function supportsColorCoding - gating owns the multi-range colorized
860
+ * rendering path (u_cell_range_shape/texture_ranges); always true here.
861
+ * @returns {boolean}
862
+ */
863
+ supportsColorCoding() {
864
+ return true;
865
+ }
866
+
867
+ /**
868
+ * @function getColorCodedRanges - per-channel gate ranges for the
869
+ * colorized rendering path.
870
+ * @returns {object}
871
+ */
872
+ getColorCodedRanges() {
873
+ return this.selections;
874
+ }
875
+ }
876
+
877
+
878
+ //hide gating control panel when scrolled down to access all channels..
879
+ // $(document).ready(function()
880
+ // {
881
+ // $('#csv_gating_list').scroll(function()
882
+ // {
883
+ // var div = $(this);
884
+ // if (div[0].scrollHeight - div.scrollTop() < div.height()+10)
885
+ // {
886
+ // $('#seg_controls_panel').hide();
887
+ // }else{
888
+ // $('#seg_controls_panel').show();
889
+ // }
890
+ // });
891
+ // });
892
+
893
+ //static vars: events introduced in this class and used across the app
894
+ CSVGatingList.events = {
895
+ GATING_BRUSH_MOVE: "GATING_BRUSH_MOVE",
896
+ SELECTION_CHANGED: "SELECTION_CHANGED",
897
+ RESET_GATINGLIST: "RESET_GATINGLIST"
898
+ };
899
+
900
+ // Self-registers the gating plugin (see pluginRegistry.js for the shape and main.js
901
+ // for the call sites). Only ever loaded when gating's tool is open for the current
902
+ // page view, so this always runs alongside GatingSidebarController.
903
+ if (window.Plexora) {
904
+ window.Plexora.registerPlugin({
905
+ name: "gating",
906
+ // Gating decides which cells are drawn at all, by marker threshold, so
907
+ // it registers a cell layer of its own. It supplies no colours: the
908
+ // layer draws the plain white the mask has always used, restricted to
909
+ // the gated set. Stacked over a tool that DOES colour cells, that reads
910
+ // as "these are the ones inside the gate" over the phenotype map, which
911
+ // is the pairing the layer model exists for.
912
+ // See ImageViewer.registerCellLayer.
913
+ ownsCellLayer: true,
914
+ createInstance(ctx) {
915
+ return new CSVGatingList(ctx);
916
+ },
917
+ createSidebarController(ctx) {
918
+ return new GatingSidebarController(ctx);
919
+ },
920
+ bindEvents(ctx) {
921
+ const { eventHandler, moduleInstance, seaDragonViewer, updateSeaDragonSelection, updateCentroidsForGate, runSegmentationGate, onCleanup } = ctx;
922
+ // The legacy list's sliders are sized from measured widths, so they
923
+ // need rebuilding on resize. Registered through onCleanup so
924
+ // deactivating the plugin actually removes it -- this listener used
925
+ // to be attached at script scope with no way to detach.
926
+ const onResize = () => moduleInstance?.rebuildSliders?.();
927
+ window.addEventListener("resize", onResize);
928
+ onCleanup?.(() => window.removeEventListener("resize", onResize));
929
+ eventHandler.bind(CSVGatingList.events.SELECTION_CHANGED, () => {
930
+ updateSeaDragonSelection();
931
+ updateCentroidsForGate();
932
+ runSegmentationGate(true);
933
+ });
934
+ eventHandler.bind(CSVGatingList.events.GATING_BRUSH_MOVE, () => {
935
+ updateSeaDragonSelection();
936
+ runSegmentationGate(false);
937
+ });
938
+ eventHandler.bind(CSVGatingList.events.RESET_GATINGLIST, () => {
939
+ moduleInstance.resetGatingList();
940
+ seaDragonViewer.forceRepaint();
941
+ });
942
+ eventHandler.bind(ctx.coreEvents.RESET_LISTS, () => {
943
+ moduleInstance.resetGatingList();
944
+ });
945
+ },
946
+ });
947
+ }