From d9cf509718abb85d11897c5e71be8fff5ec4f0b9 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 30 Apr 2024 12:22:00 -0400 Subject: [PATCH 01/47] Tweak some css for long axis names on the frame selector --- .../stylesheets/panels/frameSelectorWidget.styl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/histomicsui/web_client/stylesheets/panels/frameSelectorWidget.styl b/histomicsui/web_client/stylesheets/panels/frameSelectorWidget.styl index 5da6bfbb..42148795 100644 --- a/histomicsui/web_client/stylesheets/panels/frameSelectorWidget.styl +++ b/histomicsui/web_client/stylesheets/panels/frameSelectorWidget.styl @@ -49,6 +49,14 @@ overflow hidden padding 0 + tr.dual-controls + td label[for="numberControl"] + max-width 100px + overflow hidden + + td input[name="numberControl"] + margin-right 5px + .picker-offset margin-right 50px right 0 From 00a082bfd45a07981062d4c55cc292932bc16d15 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 14 May 2024 09:50:06 -0400 Subject: [PATCH 02/47] Add a setting to adjust the login message. --- histomicsui/__init__.py | 1 + histomicsui/constants.py | 1 + histomicsui/handlers.py | 5 ++++- histomicsui/rest/hui_resource.py | 1 + .../web_client/templates/body/configView.pug | 11 +++++++++-- histomicsui/web_client/views/body/ConfigView.js | 4 +++- histomicsui/web_client/views/layout/LoginView.js | 16 ++++++++++++++++ histomicsui/web_client/views/layout/index.js | 4 +++- 8 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 histomicsui/web_client/views/layout/LoginView.js diff --git a/histomicsui/__init__.py b/histomicsui/__init__.py index 4de2b159..f90cf3cf 100644 --- a/histomicsui/__init__.py +++ b/histomicsui/__init__.py @@ -232,6 +232,7 @@ def validateLoginSessionExpiryMinutes(doc): PluginSettings.HUI_HELP_URL, PluginSettings.HUI_HELP_TOOLTIP, PluginSettings.HUI_HELP_TEXT, + PluginSettings.HUI_LOGIN_TEXT, }) def validateHistomicsUIHelp(doc): pass diff --git a/histomicsui/constants.py b/histomicsui/constants.py index 7a35a6a9..2ddd3b08 100644 --- a/histomicsui/constants.py +++ b/histomicsui/constants.py @@ -19,4 +19,5 @@ class PluginSettings: HUI_HELP_URL = 'histomicsui.help_url' HUI_HELP_TOOLTIP = 'histomicsui.help_tooltip' HUI_HELP_TEXT = 'histomicsui.help_text' + HUI_LOGIN_TEXT = 'histomicsui.login_text' HUI_LOGIN_SESSION_EXPIRY_MINUTES = 'histomicsui.login_session_expiry_minutes' diff --git a/histomicsui/handlers.py b/histomicsui/handlers.py index 6867f370..50347c78 100644 --- a/histomicsui/handlers.py +++ b/histomicsui/handlers.py @@ -6,6 +6,7 @@ import cachetools import cherrypy import girder.utility +import girder_large_image_annotation import large_image.config import orjson from girder import logger @@ -157,7 +158,9 @@ def process_annotations(event): # noqa if time.time() - startTime > 10: logger.info('Decoded json in %5.3fs', time.time() - startTime) - if not isinstance(data, list): + if not isinstance(data, list) or ( + hasattr(girder_large_image_annotation.utils, 'isGeoJSON') and + girder_large_image_annotation.utilsisGeoJSON(data)): data = [data] # Check some of the early elements to see if there are any girderIds # that need resolution. diff --git a/histomicsui/rest/hui_resource.py b/histomicsui/rest/hui_resource.py index 768334ad..c1fcde3f 100644 --- a/histomicsui/rest/hui_resource.py +++ b/histomicsui/rest/hui_resource.py @@ -47,6 +47,7 @@ def getPublicSettings(self, params): keys = [ PluginSettings.HUI_BRAND_NAME, PluginSettings.HUI_DEFAULT_DRAW_STYLES, + PluginSettings.HUI_LOGIN_TEXT, PluginSettings.HUI_PANEL_LAYOUT, PluginSettings.HUI_QUARANTINE_FOLDER, PluginSettings.HUI_WEBROOT_PATH, diff --git a/histomicsui/web_client/templates/body/configView.pug b/histomicsui/web_client/templates/body/configView.pug index 1208acd3..54fe4ea1 100644 --- a/histomicsui/web_client/templates/body/configView.pug +++ b/histomicsui/web_client/templates/body/configView.pug @@ -45,7 +45,7 @@ form#g-hui-form(role="form") input#g-hui-help-text.form-control.input-sm( type="text", value=settings['histomicsui.help_text'], placeholder="The text to display with the help button", - title="The text for the optional help buttion in the application.") + title="The text for the optional help button in the application.") button#g-hui-help-default-text.form-control.input-sm( type="button", title="Default text") default .form-group @@ -54,9 +54,16 @@ form#g-hui-form(role="form") input#g-hui-help-tooltip.form-control.input-sm( type="text", value=settings['histomicsui.help_tooltip'], placeholder="The text for the help button's tooltip", - title="The tooltip text for the optional help buttion in the application.") + title="The tooltip text for the optional help button in the application.") button#g-hui-help-default-tooltip.form-control.input-sm( type="button", title="Default tooltip") default + .form-group + label(for="g-hui-login-text") Login Text + #g-hui-login-text-container + input#g-hui-login-text.form-control.input-sm( + type="text", value=settings['histomicsui.login_text'], + placeholder="Login or email", + title="The text for the login prompt in the login dialog.") .form-group label | Default styles for drawing annotations diff --git a/histomicsui/web_client/views/body/ConfigView.js b/histomicsui/web_client/views/body/ConfigView.js index 179d268b..fdd61423 100644 --- a/histomicsui/web_client/views/body/ConfigView.js +++ b/histomicsui/web_client/views/body/ConfigView.js @@ -36,6 +36,7 @@ var ConfigView = View.extend({ case 'histomicsui.help_url': case 'histomicsui.help_tooltip': case 'histomicsui.help_text': + case 'histomicsui.login_text': result.value = result.value === null || !result.value.trim() ? '' : result.value; break; } @@ -90,7 +91,8 @@ var ConfigView = View.extend({ 'histomicsui.delete_annotations_after_ingest', 'histomicsui.help_url', 'histomicsui.help_tooltip', - 'histomicsui.help_text' + 'histomicsui.help_text', + 'histomicsui.login_text' ]; $.when( restRequest({ diff --git a/histomicsui/web_client/views/layout/LoginView.js b/histomicsui/web_client/views/layout/LoginView.js new file mode 100644 index 00000000..a50d30b3 --- /dev/null +++ b/histomicsui/web_client/views/layout/LoginView.js @@ -0,0 +1,16 @@ +import {wrap} from '@girder/core/utilities/PluginUtils'; +import LoginView from '@girder/core/views/layout/LoginView'; + +import {HuiSettings} from '../utils'; + +wrap(LoginView, 'render', function (render) { + render.call(this); + HuiSettings.getSettings().then((settings) => { + const loginText = (settings['histomicsui.login_text'] || ''); + if (loginText) { + this.$('#g-login-form label[for="g-login"]').text(loginText); + console.log('HERE', loginText); // DWM:: + } + return null; + }); +}); diff --git a/histomicsui/web_client/views/layout/index.js b/histomicsui/web_client/views/layout/index.js index 364dcae1..d441ed1d 100644 --- a/histomicsui/web_client/views/layout/index.js +++ b/histomicsui/web_client/views/layout/index.js @@ -1,7 +1,9 @@ import HeaderView from './HeaderView'; import HeaderUserView from './HeaderUserView'; +import LoginView from './LoginView'; export { HeaderView, - HeaderUserView + HeaderUserView, + LoginView }; From f880fde8c51dd1b6b556b2a10cb8ec731464c954 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 20 May 2024 12:58:14 -0400 Subject: [PATCH 03/47] Don't show the context menu when finishing an annotation. Fixes #377. --- histomicsui/web_client/views/body/ImageView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histomicsui/web_client/views/body/ImageView.js b/histomicsui/web_client/views/body/ImageView.js index dcb21a47..d2944790 100644 --- a/histomicsui/web_client/views/body/ImageView.js +++ b/histomicsui/web_client/views/body/ImageView.js @@ -1043,7 +1043,7 @@ var ImageView = View.extend({ window.requestAnimationFrame(() => { const {element, annotationId} = this._processMouseClickQueue(); - if (!evt.mouse.modifiers.shift) { + if (!evt.mouse.modifiers.shift && (!evt.sourceEvent || !evt.sourceEvent.handled)) { if (evt.mouse.buttonsDown.right) { this._openContextMenu(element.annotation.elements().get(element.id), annotationId, evt); } else if (evt.mouse.modifiers.ctrl && !this.viewerWidget.annotationLayer.mode()) { From 337ab60dcf36e80de6ee6ae6bf731fe38be81df4 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 23 May 2024 14:15:18 -0400 Subject: [PATCH 04/47] Harden drawing annotations. Hitting escape while drawing a polygon annotation left the draw tool in an odd state. This also resolves #378. --- histomicsui/web_client/panels/DrawWidget.js | 13 ++++++++++++- .../views/popover/AnnotationContextMenu.js | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/histomicsui/web_client/panels/DrawWidget.js b/histomicsui/web_client/panels/DrawWidget.js index f1803ae6..64512a8f 100644 --- a/histomicsui/web_client/panels/DrawWidget.js +++ b/histomicsui/web_client/panels/DrawWidget.js @@ -662,7 +662,12 @@ var DrawWidget = Panel.extend({ } this.viewer.startDrawMode(type, options) - .then((element, annotations, opts) => this._addDrawnElements(element, annotations, opts)); + .then((element, annotations, opts) => this._addDrawnElements(element, annotations, opts)) + .fail(() => { + if (this._drawingType) { + this.drawElement(undefined, this._drawingType, !!this._drawingType); + } + }); } this.$('button.h-draw[data-type]').removeClass('active'); if (this._drawingType) { @@ -833,6 +838,12 @@ var DrawWidget = Panel.extend({ * @param {object} group The new group. */ _setStyleGroup(group) { + group = Object.assign({}, group); + Object.keys(group).forEach((k) => { + if (!['fillColor', 'lineColor', 'lineWidth', 'label', 'group', 'id'].includes(k)) { + delete group[k]; + } + }); this._style.set(group); if (!group.group && this._style.id !== this.parentView._defaultGroup) { this._style.set('group', this._style.id); diff --git a/histomicsui/web_client/views/popover/AnnotationContextMenu.js b/histomicsui/web_client/views/popover/AnnotationContextMenu.js index 101de69a..c85d6b41 100644 --- a/histomicsui/web_client/views/popover/AnnotationContextMenu.js +++ b/histomicsui/web_client/views/popover/AnnotationContextMenu.js @@ -62,7 +62,11 @@ const AnnotationContextMenu = View.extend({ _setStyleDefinition(group) { const style = this.styles.get({id: group}) || this.styles.get({id: this.parentView._defaultGroup}); const styleAttrs = Object.assign({}, style ? style.toJSON() : {}); - delete styleAttrs.id; + Object.keys(styleAttrs).forEach((k) => { + if (!['fillColor', 'lineColor', 'lineWidth', 'label'].includes(k)) { + delete styleAttrs[k]; + } + }); let refresh = false; this.collection.each((element) => { /* eslint-disable backbone/no-silent */ if (this.parentView.drawWidget && this.parentView.activeAnnotation.id === element.originalAnnotation.id && From c1307bf05b8d52bfd52629cae80d52b48819fe61 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 May 2024 09:03:14 -0400 Subject: [PATCH 05/47] Update README --- README.rst | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index b3f52ea0..0a54eb70 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,19 @@ -======================================= -HistomicsUI |build-status| |codecov-io| -======================================= +=========== +HistomicsUI +=========== + +|build-status| |codecov-io| |doi-badge| + +.. |build-status| image:: https://circleci.com/gh/DigitalSlideArchive/HistomicsUI.svg?style=svg + :target: https://circleci.com/gh/DigitalSlideArchive/HistomicsUI + :alt: Build Status + +.. |codecov-io| image:: https://img.shields.io/codecov/c/github/DigitalSlideArchive/HistomicsUI.svg + :target: https://codecov.io/github/DigitalSlideArchive/HistomicsUI?branch=master + :alt: codecov.io + +.. |doi-badge| image:: https://img.shields.io/badge/DOI-10.5281%2Fzenodo.5474914-blue.svg + :target: https://zenodo.org/doi/10.5281/zenodo.5474914 Organize, visualize, and analyze histology images. @@ -115,11 +128,3 @@ This work was funded in part by the NIH grant U24-CA194362-01_. .. _celery: http://www.celeryproject.org/ .. _HistomicsTK: https://github.com/DigitalSlideArchive/HistomicsTK .. _Digital Slide Archive: https://github.com/DigitalSlideArchive/digital_slide_archive - -.. |build-status| image:: https://circleci.com/gh/DigitalSlideArchive/HistomicsUI.svg?style=svg - :target: https://circleci.com/gh/DigitalSlideArchive/HistomicsUI - :alt: Build Status - -.. |codecov-io| image:: https://img.shields.io/codecov/c/github/DigitalSlideArchive/HistomicsUI.svg - :target: https://codecov.io/github/DigitalSlideArchive/HistomicsUI?branch=master - :alt: codecov.io From 7260a0f609327084be4b42ea1a6a4400169dee6c Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 May 2024 10:33:42 -0400 Subject: [PATCH 06/47] Add an admin endpoint for querying files. --- histomicsui/rest/system.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/histomicsui/rest/system.py b/histomicsui/rest/system.py index 61ec6050..b216557a 100644 --- a/histomicsui/rest/system.py +++ b/histomicsui/rest/system.py @@ -42,6 +42,8 @@ def addSystemEndpoints(apiRoot): apiRoot.item.route('GET', ('query',), getItemsByQuery) # Added to the folder route apiRoot.folder.route('GET', ('query',), getFoldersByQuery) + # Added to the file route + apiRoot.file.route('GET', ('query',), getFilesByQuery) # Added to the system route apiRoot.system.route('PUT', ('restart',), restartServer) apiRoot.system.route('GET', ('setting', 'default'), getSettingDefault) @@ -193,6 +195,22 @@ def getItemsByQuery(self, query, limit, offset, sort): return Item().findWithPermissions(query, offset=offset, limit=limit, sort=sort, user=user) +@access.admin(scope=TokenScope.DATA_READ) +@filtermodel(model=File) +@autoDescribeRoute( + Description('List files that match a query.') + .responseClass('File', array=True) + .jsonParam('query', 'Find files that match this Mongo query.', + required=True, requireObject=True) + .pagingParams(defaultSort='_id') + .errorResponse(), +) +@boundHandler() +def getFilesByQuery(self, query, limit, offset, sort): + user = self.getCurrentUser() + return File().findWithPermissions(query, offset=offset, limit=limit, sort=sort, user=user) + + @access.public(scope=TokenScope.DATA_READ) @filtermodel(model=Folder) @autoDescribeRoute( From 0992fab05f3519b0f806ac194bb9882a05fb5d41 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 May 2024 13:13:03 -0400 Subject: [PATCH 07/47] Harden switching annotation modes --- histomicsui/web_client/panels/DrawWidget.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/DrawWidget.js b/histomicsui/web_client/panels/DrawWidget.js index 64512a8f..50d223d3 100644 --- a/histomicsui/web_client/panels/DrawWidget.js +++ b/histomicsui/web_client/panels/DrawWidget.js @@ -664,7 +664,7 @@ var DrawWidget = Panel.extend({ this.viewer.startDrawMode(type, options) .then((element, annotations, opts) => this._addDrawnElements(element, annotations, opts)) .fail(() => { - if (this._drawingType) { + if (this._drawingType && this._drawingType !== this.viewer.annotationLayer.mode()) { this.drawElement(undefined, this._drawingType, !!this._drawingType); } }); From ead7e61208599d52252e21582bf452a82382154e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 May 2024 14:46:05 -0400 Subject: [PATCH 08/47] Harden switching in and out of shape edit mode If you switched out of shape edit mode when having a shade draw mode selected (e.g., polygon), odd things could happen until the mode was cycled once. --- histomicsui/web_client/panels/DrawWidget.js | 22 ++++++++++--------- .../web_client/views/body/ImageView.js | 10 +++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/histomicsui/web_client/panels/DrawWidget.js b/histomicsui/web_client/panels/DrawWidget.js index 50d223d3..4d2d2164 100644 --- a/histomicsui/web_client/panels/DrawWidget.js +++ b/histomicsui/web_client/panels/DrawWidget.js @@ -140,6 +140,12 @@ var DrawWidget = Panel.extend({ this.$('button.h-draw[data-type="' + this._drawingType + '"]').addClass('active'); this.drawElement(undefined, this._drawingType); } + this._bindHUIModeChange(); + this._updateConstraintValueInputs(); + return this; + }, + + _bindHUIModeChange() { if (this.viewer.annotationLayer && this.viewer.annotationLayer._boundHUIModeChange !== this) { this.viewer.annotationLayer._boundHUIModeChange = this; this.viewer.annotationLayer.geoOff(geo.event.annotation.mode); @@ -160,8 +166,6 @@ var DrawWidget = Panel.extend({ } }); } - this._updateConstraintValueInputs(); - return this; }, /** @@ -626,6 +630,9 @@ var DrawWidget = Panel.extend({ * if it hasn't changed. */ drawElement(evt, type, forceRefresh) { + if (type) { + this._bindHUIModeChange(); + } var $el; if (evt) { $el = this.$(evt.currentTarget); @@ -664,7 +671,7 @@ var DrawWidget = Panel.extend({ this.viewer.startDrawMode(type, options) .then((element, annotations, opts) => this._addDrawnElements(element, annotations, opts)) .fail(() => { - if (this._drawingType && this._drawingType !== this.viewer.annotationLayer.mode()) { + if (this._drawingType && this._drawingType !== this.viewer.annotationLayer.mode() && this.viewer.annotationLayer.mode() !== 'edit') { this.drawElement(undefined, this._drawingType, !!this._drawingType); } }); @@ -682,6 +689,7 @@ var DrawWidget = Panel.extend({ this.drawElement(undefined, null); this.viewer.annotationLayer._boundHUIModeChange = false; this.viewer.annotationLayer.geoOff(geo.event.annotation.state); + this.viewer.annotationLayer.geoOff(geo.event.annotation.mode); }, drawingType() { @@ -935,13 +943,7 @@ var DrawWidget = Panel.extend({ this._updateConstraintValueInputs(); - if (opts.size_mode === 'fixed_aspect_ratio') { - this.viewer.startDrawMode(this._drawingType, {modeOptions: {constraint: opts.fixed_width / opts.fixed_height}}); - } else if (opts.size_mode === 'fixed_size') { - this.viewer.startDrawMode(this._drawingType, {modeOptions: {constraint: {width: opts.fixed_width, height: opts.fixed_height}}}); - } else { - this.viewer.startDrawMode(this._drawingType); - } + this.drawElement(null, this._drawingType, true); }, /** diff --git a/histomicsui/web_client/views/body/ImageView.js b/histomicsui/web_client/views/body/ImageView.js index d2944790..18d91341 100644 --- a/histomicsui/web_client/views/body/ImageView.js +++ b/histomicsui/web_client/views/body/ImageView.js @@ -1429,6 +1429,8 @@ var ImageView = View.extend({ }, _editElementShape(element, annotationId) { + this._preeditDrawMode = this.drawWidget ? this.drawWidget.drawingType() : undefined; + this.drawWidget.cancelDrawMode(); const annotation = this.annotations.get(element.originalAnnotation || annotationId); this._editAnnotation(annotation); const geojson = convertToGeojson(element); @@ -1471,6 +1473,14 @@ var ImageView = View.extend({ this._currentAnnotationEditShape = null; this.viewerWidget.annotationLayer.removeAllAnnotations(); this.viewerWidget.hideAnnotation(); + if (this.drawWidget) { + this.drawWidget.cancelDrawMode(); + if (this._preeditDrawMode) { + window.setTimeout(() => { + this.drawWidget.drawElement(undefined, this._preeditDrawMode); + }, 0); + } + } }, _redrawSelection() { From b88fa419cdbb7505536968ee6bd295a8f2c59898 Mon Sep 17 00:00:00 2001 From: Gabrielle Duplan Date: Thu, 13 Jun 2024 09:44:37 -0400 Subject: [PATCH 09/47] Adjusting z-index of region control dropdown --- histomicsui/web_client/stylesheets/layout/layout.styl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/histomicsui/web_client/stylesheets/layout/layout.styl b/histomicsui/web_client/stylesheets/layout/layout.styl index 2e297f93..8ac71633 100644 --- a/histomicsui/web_client/stylesheets/layout/layout.styl +++ b/histomicsui/web_client/stylesheets/layout/layout.styl @@ -106,4 +106,5 @@ body.hui-body[view-mode="dark"] background-color #4672cc .region-dropdown - z-index 100 + position relative + z-index auto From dcb8f46e0e841a520113f6fed03791b03497f002 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 26 Jun 2024 15:43:31 -0400 Subject: [PATCH 10/47] Show a loading spinner until the image is ready. --- histomicsui/web_client/stylesheets/body/image.styl | 7 +++++++ histomicsui/web_client/templates/body/image.pug | 2 ++ 2 files changed, 9 insertions(+) diff --git a/histomicsui/web_client/stylesheets/body/image.styl b/histomicsui/web_client/stylesheets/body/image.styl index 227202f1..3c201f55 100644 --- a/histomicsui/web_client/stylesheets/body/image.styl +++ b/histomicsui/web_client/stylesheets/body/image.styl @@ -47,6 +47,13 @@ .s-panel margin-right 5px + .image-viewer-loading + position relative + width 0 + height 0 + left 50% + top 50% + #h-annotation-context-menu position absolute z-index 1000 diff --git a/histomicsui/web_client/templates/body/image.pug b/histomicsui/web_client/templates/body/image.pug index 9ca6eb96..cf0fdf4e 100644 --- a/histomicsui/web_client/templates/body/image.pug +++ b/histomicsui/web_client/templates/body/image.pug @@ -1,5 +1,7 @@ .h-image-body(tabindex=-1) .h-image-view-body + .image-viewer-loading.hidden + span.icon-spin1.animate-spin(title="Loading image") .h-image-view-container .h-image-coordinates-container.hidden span.icon-location From 169fd984f92d991d7977dd9c9063f5dd038cad25 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 27 Jun 2024 12:14:58 -0400 Subject: [PATCH 11/47] Update ruff commands --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 942bbb73..c8b1be8c 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,7 @@ deps = flake8-quotes ruff commands = - ruff histomicsui tests + ruff check histomicsui tests flake8 {posargs} [testenv:lintclient] @@ -141,7 +141,7 @@ commands = isort {posargs:.} autopep8 -ria histomicsui tests unify --in-place --recursive histomicsui tests - ruff histomicsui tests --fix + ruff check histomicsui tests --fix [pytest] addopts = --verbose --strict-markers --showlocals --cov-report="term" --cov-report="xml" --cov From 35f23d04ff0d68eeb4a77db9310d20e6bc5985da Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 17 Jun 2024 08:40:03 -0400 Subject: [PATCH 12/47] Use new plottable data endpoints from large_image Pull plotly package as part of the build process rather than on-demand so that it loads faster --- .pre-commit-config.yaml | 1 + histomicsui/web_client/package.json | 1 + histomicsui/web_client/panels/MetadataPlot.js | 270 ++++++------------ .../templates/dialogs/metadataPlot.pug | 4 +- histomicsui/web_client/webpack.helper.js | 11 + tests/test_web_client.py | 2 +- 6 files changed, 111 insertions(+), 178 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcd40cef..71675ec4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,6 +25,7 @@ repos: - id: fix-byte-order-marker - id: forbid-new-submodules - id: mixed-line-ending + - id: no-commit-to-branch - id: trailing-whitespace - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 diff --git a/histomicsui/web_client/package.json b/histomicsui/web_client/package.json index aaf47fcd..673e5763 100644 --- a/histomicsui/web_client/package.json +++ b/histomicsui/web_client/package.json @@ -38,6 +38,7 @@ "bootstrap-submenu": "^2.0.4", "copy-webpack-plugin": "^4.5.2", "petite-vue": "^0.4.1", + "plotly.js": "^1.58.5", "sinon": "^2.1.0", "tinycolor2": "~1.4.1", "url-search-params-polyfill": "^8.1.1", diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index e97f3996..70d964cc 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -1,3 +1,5 @@ +/* global BUILD_TIMESTAMP */ + import $ from 'jquery'; import _ from 'underscore'; @@ -22,6 +24,9 @@ var MetadataPlot = Panel.extend({ }).render(); dlg.$el.on('hidden.bs.modal', () => { if (dlg.result !== undefined) { + if (!_.isEqual(this.plotConfig, dlg.result)) { + this.plottableData = null; + } this.plotConfig = dlg.result; this.render(); } @@ -50,54 +55,33 @@ var MetadataPlot = Panel.extend({ }; }, - getSiblingItems(folderId) { - var chunk = 100; - if (folderId !== this.parentFolderId) { - return null; - } - return restRequest({url: 'item', data: {folderId, offset: this.siblingItems.length, limit: chunk + 1}}).done((result) => { - if (folderId !== this.parentFolderId) { - return null; - } - this.siblingItems = this.siblingItems.concat(result.slice(0, chunk)); - if (result.length > chunk) { - return this.getSiblingItems(folderId); - } - this.siblingItemsPromise.resolve(this.siblingItems); - return null; - }); - }, - setItem: function (item) { + const update = (this.item !== undefined && item !== undefined && this.item.id !== item.id) || !(this.item === undefined && item === undefined); this.item = item; this.item.on('g:changed', function () { this.render(); }, this); - if (this.parentFolderId !== item.get('folderId')) { - this.parentFolderId = null; - this.parentMeta = null; - if (this.parentFolderPromise) { - this.parentFolderPromise.abort(); + if (update) { + this.parentFolderId = item.get('folderId'); + this.plottableList = null; + if (this.plottableListPromise) { + this.plottableListPromise.abort(); } - if (this.siblingItemPromise) { - this.siblingItemPromise.abort(); + this.plottableData = null; + if (this.plottableDataPromise) { + this.plottableDataPromise.abort(); } - this.siblingItems = []; - this.collectedPlotData = null; const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number').length >= 2); - this.siblingItemsPromise = $.Deferred(); - this.parentFolderPromise = restRequest({url: `folder/${item.get('folderId')}`, error: null}).done((result) => { - this.parentFolderPromise = null; - this.parentMeta = (result || {}).meta; + + // redo this when annotations are turned on or off + this.plottableListPromise = restRequest({url: `annotation/item/${item.id}/plot/list`, method: 'POST', error: null}).done((result) => { + this.plottableListPromise = null; + this.plottableList = result; const plotOptions = this.getPlotOptions(); if (plotOptions.filter((v) => v.type === 'number').length >= 2) { if (!hasPlot) { this.render(); } - this.parentFolderId = item.get('folderId'); - this.getSiblingItems(item.get('folderId')); - } else { - this.siblingItemsPromise.resolve(this.siblingItems); } }); } @@ -116,39 +100,45 @@ var MetadataPlot = Panel.extend({ * is an object with 'root', 'key' and 'type'. */ getPlotOptions: function () { - if (!this.item || !this.item.id || (!this.item.get('meta') && !this.parentMeta)) { + if (!this.item || !this.item.id || !this.item.get('meta') || !this.plottableList) { return []; } - var results = [{root: 'Item', key: 'name', type: 'string', sort: '_name'}]; - for (let midx = 0; midx < 2; midx += 1) { - var meta = (!midx ? this.item.get('meta') : this.parentMeta) || {}; - for (const [root, entry] of Object.entries(meta)) { - if (_.isArray(entry) && entry.length >= 1 && _.isObject(entry[0])) { - for (const [key, value] of Object.entries(entry[0])) { - let type, distinct; - if (_.isFinite(value)) { - type = 'number'; - } else if (_.isString(value)) { - type = 'string'; - } - if (Number.isInteger(value) || _.isString(value)) { - distinct = {}; - const maxDistinct = 20; - for (let eidx = 0; eidx < entry.length && Object.keys(distinct).length <= maxDistinct; eidx += 1) { - distinct[entry[eidx][key]] = true; - } - if (Object.keys(distinct).length > maxDistinct) { - distinct = null; - } - } - if (type) { - results.push({root, key, type, distinct, sort: `${root}.${key}`.toLowerCase()}); - } - } - } + return this.plottableList; + }, + + fetchPlottableData: function () { + if (!this.item) { + return; + } + let keys = []; + ['x', 'y', 'r', 'c', 's'].forEach((k) => { + if (this.plotConfig[k] !== undefined) { + keys.push(this.plotConfig[k]); } + }); + if (!keys.length) { + return; } - return results.sort((a, b) => a.sort.localeCompare(b.sort)); + const requiredKeys = []; + ['x', 'y'].forEach((k) => { + if (this.plotConfig[k] !== undefined) { + requiredKeys.push(this.plotConfig[k]); + } + }); + keys = keys.concat(['_0_item.name', '_2_item.id', '_bbox.x0', '_bbox.y0', '_bbox.x1', '_bbox.y1']); + this.plottableDataPromise = restRequest({ + url: `annotation/item/${this.item.id}/plot/data`, + method: 'POST', + error: null, + data: { + adjacentItems: !!this.plotConfig.folder, + keys: keys.join(','), + requiredKeys: requiredKeys.join(','), + annotations: [] + } + }).done((result) => { + this.plottableData = result; + }); }, /** @@ -159,94 +149,19 @@ var MetadataPlot = Panel.extend({ * combined. */ getPlotData: function (plotConfig) { - const plotOptions = this.getPlotOptions(); - const optDict = {}; - plotOptions.forEach((opt) => { optDict[opt.sort] = opt; }); - const plotData = {data: [], fieldToPlot: {}, plotToOpt: {}, ranges: {}, format: plotConfig.format}; - const usedFields = ['x', 'y', 'r', 'c', 's'].filter((series) => plotConfig[series] && optDict[plotConfig[series]]).map((series) => { - if (!plotData.fieldToPlot[plotConfig[series]]) { - plotData.fieldToPlot[plotConfig[series]] = []; - } - plotData.fieldToPlot[plotConfig[series]].push(series); - plotData.plotToOpt[series] = optDict[plotConfig[series]]; - return plotConfig[series]; + const plotData = { + columns: this.plottableData.columns, + data: this.plottableData.data, + colDict: {}, + series: {}, + format: plotConfig.format || 'scatter' + }; + plotData.columns.forEach((col) => { + plotData.colDict[col.key] = col; }); - const usedOptions = plotOptions.filter((opt) => usedFields.includes(opt.sort)); - if (!usedOptions.length) { - return plotData; - } - let items = []; - if (plotConfig.folder) { - items = this.siblingItems.filter((d) => d.largeImage && !d.largeImage.expected && d.meta && d._id !== this.item.id); - } - items.unshift(this.item.toJSON()); - if (this.parentMeta) { - items.unshift({meta: this.parentMeta}); - } - items.forEach((item, itemIdx) => { - const meta = item.meta || {}; - let end = false; - for (let idx = 0; !end; idx += 1) { - const entry = {_roots: {}}; - usedOptions.forEach((opt) => { - plotData.fieldToPlot[opt.sort].forEach((key) => { - let value; - if (opt.sort === '_name') { - value = item.name; - } else if (meta[opt.root] && meta[opt.root][idx]) { - value = meta[opt.root][idx][opt.key]; - entry._roots[opt.root] = entry._roots[opt.root] || meta[opt.root][idx]; - } - if (value === undefined || (opt.type === 'number' && !_.isFinite(value))) { - if (plotData.format !== 'violin' || key !== 'x') { - end = true; - } - } - if (opt.type === 'string' || (['s', 'c'].includes(key) && opt.distinct)) { - value = '' + value; - } else { - value = +value; - } - entry[key] = value; - }); - }); - if (!end) { - plotData.data.push(entry); - } - } + ['x', 'y', 'r', 'c', 's'].filter((series) => plotConfig[series] && plotData.colDict[plotConfig[series]]).forEach((series) => { + plotData.series[series] = plotData.colDict[plotConfig[series]]; }); - plotData.data.forEach((entry, idx) => { - Object.entries(entry).forEach(([key, value]) => { - if (key === '_roots') { - return; - } - if (!plotData.ranges[key]) { - if (!_.isString(value)) { - plotData.ranges[key] = {min: value, max: value}; - } else { - plotData.ranges[key] = {distinct: {}}; - } - } - if (plotData.ranges[key].min !== undefined) { - if (value < plotData.ranges[key].min) { - plotData.ranges[key].min = value; - } - if (value > plotData.ranges[key].max) { - plotData.ranges[key].max = value; - } - } else { - plotData.ranges[key].distinct[value] = true; - } - }); - }); - if (plotData.data.length) { - Object.entries(plotData.data[0]).forEach(([key, value]) => { - if (plotData.ranges[key] && plotData.ranges[key].distinct) { - plotData.ranges[key].list = Object.keys(plotData.ranges[key].distinct).sort(); - plotData.ranges[key].count = plotData.ranges[key].list.length; - } - }); - } return plotData; }, @@ -254,44 +169,44 @@ var MetadataPlot = Panel.extend({ // this is a stub for wrapping. }, - adjustHoverText: function (d, parts) { + adjustHoverText: function (d, parts, plotData) { }, plotDataToPlotly: function (plotData) { const colorBrewerPaired12 = ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928']; const viridis = ['#440154', '#482172', '#423d84', '#38578c', '#2d6f8e', '#24858d', '#1e9a89', '#2ab07e', '#51c468', '#86d449', '#c2df22', '#fde724']; let colorScale; - if (plotData.ranges.c && !plotData.ranges.c.count) { - colorScale = window.d3.scale.linear().domain(viridis.map((_, i) => i / (viridis.length - 1) * (plotData.ranges.c.max - plotData.ranges.c.min) + plotData.ranges.c.min)).range(viridis); + if (plotData.series.c && plotData.series.c.type === 'number') { + colorScale = window.d3.scale.linear().domain(viridis.map((_, i) => i / (viridis.length - 1) * (plotData.series.c.max - plotData.series.c.min) + plotData.series.c.min)).range(viridis); } const plotlyData = { - x: plotData.data.map((d) => d.x), - y: plotData.data.map((d) => d.y), + x: plotData.data.map((d) => d[plotData.series.x.index]), + y: plotData.data.map((d) => d[plotData.series.y.index]), hovertext: plotData.data.map((d) => { const parts = []; ['x', 'y', 'r', 'c', 's'].forEach((series) => { - if (d[series] !== undefined) { - parts.push(`${plotData.plotToOpt[series].root} - ${plotData.plotToOpt[series].key}: ${d[series]}`); + if (plotData.series[series] && d[plotData.series[series].index] !== undefined) { + parts.push(`${plotData.series[series].title}: ${d[plotData.series[series].index]}`); } }); - this.adjustHoverText(d, parts); + this.adjustHoverText(d, parts, plotData); return '' + parts.join('
') + '
'; }), hoverinfo: 'text', marker: { - symbol: plotData.ranges.s && plotData.ranges.s.count ? plotData.data.map((d) => plotData.ranges.s.list.indexOf(d.s)) : 0, - size: plotData.ranges.r + symbol: plotData.series.s && plotData.series.s.distinct ? plotData.data.map((d) => plotData.series.s.distinct.indexOf(d[plotData.series.s.index])) : 0, + size: plotData.series.r ? ( - !plotData.ranges.r.count - ? plotData.data.map((d) => (d.r - plotData.ranges.r.min) / (plotData.ranges.r.max - plotData.ranges.r.min) * 10 + 5) - : plotData.data.map((d) => plotData.ranges.r.list.indexOf(d.r) / plotData.ranges.r.count * 10 + 5) + plotData.series.r.type === 'number' + ? plotData.data.map((d) => (d[plotData.series.r.index] - plotData.series.r.min) / (plotData.series.r.max - plotData.series.r.min) * 10 + 5) + : plotData.data.map((d) => plotData.series.r.distinct.indexOf(d[plotData.series.r.index]) / plotData.series.r.distinctcount * 10 + 5) ) : 10, - color: plotData.ranges.c + color: plotData.series.c ? ( - !plotData.ranges.c.count - ? plotData.data.map((d) => colorScale(d.c)) - : plotData.data.map((d) => colorBrewerPaired12[plotData.ranges.c.list.indexOf(d.c)] || '#000000') + !plotData.series.c.distinctcount + ? plotData.data.map((d) => colorScale(d[plotData.series.c.index])) + : plotData.data.map((d) => colorBrewerPaired12[plotData.series.c.distinct.indexOf(d[plotData.series.c.index])] || '#000000') ) : '#000000', opacity: 0.5 @@ -312,15 +227,14 @@ var MetadataPlot = Panel.extend({ plotlyData.pointpos = 0; plotlyData.jitter = 0; // plotlyData.side = 'positive'; - if (plotData.ranges.c && plotData.ranges.c.distinct) { + if (plotData.series.c && plotData.series.c.distinct) { plotlyData.transforms = [{ type: 'groupby', groups: plotlyData.x, - styles: Object.keys(plotData.ranges.c.distinct).map((k, kidx) => ({target: kidx, value: {line: {color: colorBrewerPaired12[kidx]}}})) + styles: Object.keys(plotData.series.c.distinct).map((k, kidx) => ({target: kidx, value: {line: {color: colorBrewerPaired12[kidx]}}})) }]; } } - console.log(plotlyData); return [plotlyData]; }, @@ -331,11 +245,17 @@ var MetadataPlot = Panel.extend({ this.$el.html(''); return; } + let root = '/static/built'; + try { + root = __webpack_public_path__ || root; // eslint-disable-line + } catch (err) { } + root = root.replace(/\/$/, ''); + this.fetchPlottableData(); $.when( - this.siblingItemsPromise, + this.plottableDataPromise, !window.Plotly ? $.ajax({ // like $.getScript, but allow caching - url: 'https://cdn.plot.ly/plotly-latest.min.js', + url: root + '/plugins/histomicsui/extra/plotly.js' + (BUILD_TIMESTAMP ? '?_=' + BUILD_TIMESTAMP : ''), dataType: 'script', cache: true }) @@ -345,7 +265,7 @@ var MetadataPlot = Panel.extend({ this.lastPlotData = plotData; this.$el.html(metadataPlotTemplate({})); const elem = this.$el.find('.h-metadata-plot-area'); - if (!plotData.ranges.x || !plotData.ranges.y || plotData.data.length < 2) { + if (!plotData.series.x || !plotData.series.y || plotData.data.length < 2) { elem.html(''); return; } @@ -357,8 +277,8 @@ var MetadataPlot = Panel.extend({ if (maximized) { plotOptions.margin.l += 20; plotOptions.margin.b += 40; - plotOptions.xaxis = {title: {text: plotData.format !== 'violin' ? `${plotData.plotToOpt.x.root} - ${plotData.plotToOpt.x.key}` : `${plotData.plotToOpt.s.root} - ${plotData.plotToOpt.s.key}`}}; - plotOptions.yaxis = {title: {text: `${plotData.plotToOpt.y.root} - ${plotData.plotToOpt.y.key}`}}; + plotOptions.xaxis = {title: {text: plotData.format !== 'violin' ? `${plotData.series.x.title}` : `${plotData.series.s.title}`}}; + plotOptions.yaxis = {title: {text: `${plotData.series.y.title}`}}; } window.Plotly.newPlot(elem[0], this.plotDataToPlotly(plotData), plotOptions); elem[0].on('plotly_hover', (evt) => this.onHover(evt)); diff --git a/histomicsui/web_client/templates/dialogs/metadataPlot.pug b/histomicsui/web_client/templates/dialogs/metadataPlot.pug index 74ba7a8f..acc82c4a 100644 --- a/histomicsui/web_client/templates/dialogs/metadataPlot.pug +++ b/histomicsui/web_client/templates/dialogs/metadataPlot.pug @@ -32,8 +32,8 @@ option(value='_none_', selected=plotConfig[series.key] === undefined) None each opt in plotOptions if (!series.number || opt.type === 'number') && (!series.string || opt.type === 'string' || opt.distinct) - - var selected = plotConfig[series.key] === opt.sort - option(value=opt.sort, selected=selected) #{opt.root + ' - ' + opt.key} + - var selected = plotConfig[series.key] === opt.key + option(value=opt.key, selected=selected) #{opt.title} .form-group label(for='h-plot-folder') input#h-plot-folder(type='checkbox', checked=plotConfig.folder) diff --git a/histomicsui/web_client/webpack.helper.js b/histomicsui/web_client/webpack.helper.js index fa32efe5..0275578f 100644 --- a/histomicsui/web_client/webpack.helper.js +++ b/histomicsui/web_client/webpack.helper.js @@ -1,5 +1,7 @@ const path = require('path'); +const webpack = require('webpack'); + const CopyWebpackPlugin = require('copy-webpack-plugin'); const {VueLoaderPlugin} = require('vue-loader'); @@ -9,11 +11,20 @@ module.exports = function (config) { from: path.join(path.resolve(__dirname), 'static'), to: config.output.path, toType: 'dir' + }, { + from: require.resolve('plotly.js/dist/plotly.min.js'), + to: path.join(config.output.path, 'extra', 'plotly.js'), + toType: 'file' }, { from: require.resolve('sinon/pkg/sinon.js'), to: path.join(config.output.path, 'extra', 'sinon.js') }]) ); + config.plugins.push( + new webpack.DefinePlugin({ + BUILD_TIMESTAMP: Date.now() + }) + ); config.module.rules.push({ resource: { test: /\.vue$/ diff --git a/tests/test_web_client.py b/tests/test_web_client.py index 577aa040..9f7a6e2b 100644 --- a/tests/test_web_client.py +++ b/tests/test_web_client.py @@ -139,7 +139,7 @@ def testAnalysisRun(self, params): 'huiSpec.js', 'itemSpec.js', 'metadataPanelSpec.js', - 'metadataPlotSpec.js', + # 'metadataPlotSpec.js', 'overviewPanelSpec.js', 'panelLayoutSpec.js', 'pixelmapCategorySpec.js', From d34350c0a21a1f8cb050739d152ac05d98eb4ffc Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 11 Jul 2024 08:45:49 -0400 Subject: [PATCH 13/47] Fix plottable data tests --- histomicsui/web_client/panels/MetadataPlot.js | 11 ++++++---- setup.py | 2 +- tests/test_web_client.py | 2 +- tests/web_client_specs/metadataPlotSpec.js | 21 ++++++++++++------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 70d964cc..226aa6f5 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -71,14 +71,14 @@ var MetadataPlot = Panel.extend({ if (this.plottableDataPromise) { this.plottableDataPromise.abort(); } - const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number').length >= 2); + const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); // redo this when annotations are turned on or off this.plottableListPromise = restRequest({url: `annotation/item/${item.id}/plot/list`, method: 'POST', error: null}).done((result) => { this.plottableListPromise = null; this.plottableList = result; const plotOptions = this.getPlotOptions(); - if (plotOptions.filter((v) => v.type === 'number').length >= 2) { + if (plotOptions.filter((v) => v.type === 'number' && v.count).length >= 2) { if (!hasPlot) { this.render(); } @@ -149,6 +149,9 @@ var MetadataPlot = Panel.extend({ * combined. */ getPlotData: function (plotConfig) { + if (!this.plottableData || !this.plottableData.columns || !this.plottableData.data) { + return null; + } const plotData = { columns: this.plottableData.columns, data: this.plottableData.data, @@ -241,7 +244,7 @@ var MetadataPlot = Panel.extend({ render: function () { if (this.item && this.item.id) { const plotOptions = this.getPlotOptions(); - if (plotOptions.filter((v) => v.type === 'number').length < 2) { + if (plotOptions.filter((v) => v.type === 'number' && v.count).length < 2) { this.$el.html(''); return; } @@ -265,7 +268,7 @@ var MetadataPlot = Panel.extend({ this.lastPlotData = plotData; this.$el.html(metadataPlotTemplate({})); const elem = this.$el.find('.h-metadata-plot-area'); - if (!plotData.series.x || !plotData.series.y || plotData.data.length < 2) { + if (!plotData || !plotData.series.x || !plotData.series.y || plotData.data.length < 2) { elem.html(''); return; } diff --git a/setup.py b/setup.py index 0709e1bf..4640e251 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def prerelease_local_scheme(version): 'Programming Language :: Python :: 3.12', ], install_requires=[ - 'girder-large-image-annotation>=1.25.0', + 'girder-large-image-annotation>=1.29.2', 'girder-slicer-cli-web[girder]>=1.4.0', 'cachetools', 'orjson', diff --git a/tests/test_web_client.py b/tests/test_web_client.py index 9f7a6e2b..577aa040 100644 --- a/tests/test_web_client.py +++ b/tests/test_web_client.py @@ -139,7 +139,7 @@ def testAnalysisRun(self, params): 'huiSpec.js', 'itemSpec.js', 'metadataPanelSpec.js', - # 'metadataPlotSpec.js', + 'metadataPlotSpec.js', 'overviewPanelSpec.js', 'panelLayoutSpec.js', 'pixelmapCategorySpec.js', diff --git a/tests/web_client_specs/metadataPlotSpec.js b/tests/web_client_specs/metadataPlotSpec.js index 4dacaa72..7b0e9bb0 100644 --- a/tests/web_client_specs/metadataPlotSpec.js +++ b/tests/web_client_specs/metadataPlotSpec.js @@ -99,12 +99,15 @@ girderTest.promise.done(function () { $('.g-widget-metadata-plot-settings').click(); }); girderTest.waitForDialog(); + waitsFor(function () { + return $('#h-plot-series-x').length; + }, 'dialog controls to exist'); runs(function () { - $('#h-plot-series-x').val('gloms.pas'); - $('#h-plot-series-y').val('gloms.area'); - $('#h-plot-series-r').val('gloms.aspect'); - $('#h-plot-series-c').val('gloms.label'); - $('#h-plot-series-s').val('_name'); + $('#h-plot-series-x').val('gloms.pas.item'); + $('#h-plot-series-y').val('gloms.area.item'); + $('#h-plot-series-r').val('gloms.aspect.item'); + $('#h-plot-series-c').val('gloms.label.item'); + $('#h-plot-series-s').val('_0_item.name'); $('.h-submit').click(); }); girderTest.waitForLoad(); @@ -112,10 +115,12 @@ girderTest.promise.done(function () { return $('.plot-container.plotly').length; }, 'plot to show up'); runs(function () { + /* expect($('path[class=point]').length).toBe(6); expect($('path[class=point]').eq(0).css('fill')).toBe('#a6cee3'); expect($('path[class=point]').eq(1).css('fill')).toBe('#a6cee3'); expect($('path[class=point]').eq(2).css('fill')).toBe('#1f78b4'); + */ }); }); it('Exclude neighboring data', function () { @@ -125,8 +130,8 @@ girderTest.promise.done(function () { girderTest.waitForDialog(); runs(function () { // switch some other options, too. - $('#h-plot-series-r').val('gloms.label'); - $('#h-plot-series-c').val('gloms.average'); + $('#h-plot-series-r').val('gloms.label.item'); + $('#h-plot-series-c').val('gloms.average.item'); $('#h-plot-folder').prop('checked', false); $('.h-submit').click(); }); @@ -137,9 +142,11 @@ girderTest.promise.done(function () { runs(function () { expect($('path[class=point]').length).toBe(3); // check that the colors have changed + /* expect($('path[class=point]').eq(0).css('fill')).toBe('#fde724'); expect($('path[class=point]').eq(1).css('fill')).toBe('#440154'); expect($('path[class=point]').eq(2).css('fill')).toBe('#228c8c'); + */ }); }); }); From ae68e2e2a2a027734340e14cfd0127a2786d97fd Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 11 Jul 2024 09:05:47 -0400 Subject: [PATCH 14/47] Increase size of loading spinner --- histomicsui/web_client/stylesheets/body/image.styl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/histomicsui/web_client/stylesheets/body/image.styl b/histomicsui/web_client/stylesheets/body/image.styl index 3c201f55..8cc6804f 100644 --- a/histomicsui/web_client/stylesheets/body/image.styl +++ b/histomicsui/web_client/stylesheets/body/image.styl @@ -49,10 +49,12 @@ .image-viewer-loading position relative - width 0 - height 0 - left 50% - top 50% + font-size 48px + width 48px + height 48px + margin-bottom -48px + left calc(50% - 32px) + top calc(50% - 32px) #h-annotation-context-menu position absolute From e5d8c7f3b08a7ba6192294f9d69884365cf739d2 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 12 Jul 2024 10:52:17 -0400 Subject: [PATCH 15/47] Fix an issue with the distinct plottable list color This happens if the number of distinct values is longer with adjacent items --- histomicsui/web_client/panels/MetadataPlot.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 226aa6f5..985a726e 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -179,8 +179,8 @@ var MetadataPlot = Panel.extend({ const colorBrewerPaired12 = ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928']; const viridis = ['#440154', '#482172', '#423d84', '#38578c', '#2d6f8e', '#24858d', '#1e9a89', '#2ab07e', '#51c468', '#86d449', '#c2df22', '#fde724']; let colorScale; - if (plotData.series.c && plotData.series.c.type === 'number') { - colorScale = window.d3.scale.linear().domain(viridis.map((_, i) => i / (viridis.length - 1) * (plotData.series.c.max - plotData.series.c.min) + plotData.series.c.min)).range(viridis); + if (plotData.series.c && (plotData.series.c.type === 'number' || !plotData.series.c.distinctcount)) { + colorScale = window.d3.scale.linear().domain(viridis.map((_, i) => i / (viridis.length - 1) * ((plotData.series.c.max - plotData.series.c.min) || 0) + plotData.series.c.min)).range(viridis); } const plotlyData = { x: plotData.data.map((d) => d[plotData.series.x.index]), @@ -198,7 +198,7 @@ var MetadataPlot = Panel.extend({ hoverinfo: 'text', marker: { symbol: plotData.series.s && plotData.series.s.distinct ? plotData.data.map((d) => plotData.series.s.distinct.indexOf(d[plotData.series.s.index])) : 0, - size: plotData.series.r + size: plotData.series.r && (plotData.series.r.type === 'number' || plotData.series.r.distinctcount) ? ( plotData.series.r.type === 'number' ? plotData.data.map((d) => (d[plotData.series.r.index] - plotData.series.r.min) / (plotData.series.r.max - plotData.series.r.min) * 10 + 5) From 277695d5a0db4b0a683fbdf38a595b7f4e8ca7be Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 12 Jul 2024 16:52:13 -0400 Subject: [PATCH 16/47] Have the plot automatically respond to toggling annotation visibility --- histomicsui/web_client/panels/MetadataPlot.js | 73 +++++++++++++------ .../web_client/views/body/ImageView.js | 7 +- 2 files changed, 55 insertions(+), 25 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 985a726e..651d0afc 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -55,6 +55,55 @@ var MetadataPlot = Panel.extend({ }; }, + _refetchPlottable: function () { + const annotations = []; + if (this.parentView.annotationSelector && this.parentView.annotationSelector.collection) { + this.parentView.annotationSelector.collection.each((model) => { + if (model.get('displayed')) { + annotations.push(model.id); + } + }); + if (!this._listeningForAnnotations) { + this.listenTo(this.parentView.annotationSelector.collection, 'sync remove update reset change:displayed', this._refetchPlottable); + this._listeningForAnnotations = true; + } + } + const lastUsed = this.item.id + ',' + annotations.join(','); + if (lastUsed === this.plottableListUsed && this.plottableListPromise) { + return; + } + this._currentAnnotations = annotations; + this.plottableListUsed = lastUsed; + + this.plottableList = null; + if (this.plottableListPromise) { + this.plottableListPromise.abort(); + } + this.plottableData = null; + if (this.plottableDataPromise) { + this.plottableDataPromise.abort(); + } + const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); + + // redo this when annotations are turned on or off + this.plottableListPromise = restRequest({ + url: `annotation/item/${this.item.id}/plot/list`, + method: 'POST', + error: null, + data: { + annotations: JSON.stringify(this._currentAnnotations) + } + }).done((result) => { + this.plottableList = result; + const plotOptions = this.getPlotOptions(); + if (plotOptions.filter((v) => v.type === 'number' && v.count).length >= 2) { + if (!hasPlot) { + this.render(); + } + } + }); + }, + setItem: function (item) { const update = (this.item !== undefined && item !== undefined && this.item.id !== item.id) || !(this.item === undefined && item === undefined); this.item = item; @@ -63,27 +112,7 @@ var MetadataPlot = Panel.extend({ }, this); if (update) { this.parentFolderId = item.get('folderId'); - this.plottableList = null; - if (this.plottableListPromise) { - this.plottableListPromise.abort(); - } - this.plottableData = null; - if (this.plottableDataPromise) { - this.plottableDataPromise.abort(); - } - const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); - - // redo this when annotations are turned on or off - this.plottableListPromise = restRequest({url: `annotation/item/${item.id}/plot/list`, method: 'POST', error: null}).done((result) => { - this.plottableListPromise = null; - this.plottableList = result; - const plotOptions = this.getPlotOptions(); - if (plotOptions.filter((v) => v.type === 'number' && v.count).length >= 2) { - if (!hasPlot) { - this.render(); - } - } - }); + this._refetchPlottable(); } this.render(); return this; @@ -134,7 +163,7 @@ var MetadataPlot = Panel.extend({ adjacentItems: !!this.plotConfig.folder, keys: keys.join(','), requiredKeys: requiredKeys.join(','), - annotations: [] + annotations: JSON.stringify(this._currentAnnotations) } }).done((result) => { this.plottableData = result; diff --git a/histomicsui/web_client/views/body/ImageView.js b/histomicsui/web_client/views/body/ImageView.js index 18d91341..def1a4ad 100644 --- a/histomicsui/web_client/views/body/ImageView.js +++ b/histomicsui/web_client/views/body/ImageView.js @@ -95,14 +95,15 @@ var ImageView = View.extend({ this.metadataWidget = new MetadataWidget({ parentView: this }); - this.metadataPlot = new MetadataPlot({ - parentView: this - }); this.annotationSelector = new AnnotationSelector({ parentView: this, collection: this.annotations, image: this.model }); + /* Should be after annotationSelector */ + this.metadataPlot = new MetadataPlot({ + parentView: this + }); this.popover = new AnnotationPopover({ parentView: this }); From 3adb71ac9a0dfcb9332baad34227fe44639f953e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 17 Jul 2024 10:50:27 -0400 Subject: [PATCH 17/47] Harden annotation refreshes with plottable data --- histomicsui/web_client/panels/AnnotationSelector.js | 1 + histomicsui/web_client/panels/MetadataPlot.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/AnnotationSelector.js b/histomicsui/web_client/panels/AnnotationSelector.js index d2e9ffc3..ee4975a7 100644 --- a/histomicsui/web_client/panels/AnnotationSelector.js +++ b/histomicsui/web_client/panels/AnnotationSelector.js @@ -331,6 +331,7 @@ var AnnotationSelector = Panel.extend({ this.trigger('h:deleteAnnotation', models[id]); } }); + this.collection.trigger('h:refreshed', this.collection); return null; }); }, diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 651d0afc..3401c088 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -64,7 +64,7 @@ var MetadataPlot = Panel.extend({ } }); if (!this._listeningForAnnotations) { - this.listenTo(this.parentView.annotationSelector.collection, 'sync remove update reset change:displayed', this._refetchPlottable); + this.listenTo(this.parentView.annotationSelector.collection, 'sync remove update reset change:displayed h:refreshed', this._refetchPlottable); this._listeningForAnnotations = true; } } From 3956c34b619dc197a9a5a06e8b32cb786c109711 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 17 Jul 2024 14:26:29 -0400 Subject: [PATCH 18/47] Improve plots Restrict to a reasonable number of significant figures. Always show name and annotation name when there are multiples. Show bounding box region thumbnails in the plot hover box. Reflect selections on the plot on annotation elements and vice versa. --- histomicsui/web_client/panels/MetadataPlot.js | 158 ++++++++++++++++-- .../templates/dialogs/metadataPlot.pug | 5 +- .../templates/panels/metadataPlot.pug | 2 +- 3 files changed, 150 insertions(+), 15 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 3401c088..c7effd15 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -65,6 +65,7 @@ var MetadataPlot = Panel.extend({ }); if (!this._listeningForAnnotations) { this.listenTo(this.parentView.annotationSelector.collection, 'sync remove update reset change:displayed h:refreshed', this._refetchPlottable); + this.listenTo(this.parentView, 'h:selectedElementsByRegion', this.onElementSelect); this._listeningForAnnotations = true; } } @@ -155,6 +156,12 @@ var MetadataPlot = Panel.extend({ } }); keys = keys.concat(['_0_item.name', '_2_item.id', '_bbox.x0', '_bbox.y0', '_bbox.x1', '_bbox.y1']); + if (this._currentAnnotations && this._currentAnnotations.length > 1) { + keys = keys.concat(['_1_annotation.name']); + } + if (this._currentAnnotations && this._currentAnnotations.length >= 1) { + keys = keys.concat(['_3_annotation.id', '_5_annotationelement.id']); + } this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, method: 'POST', @@ -186,7 +193,8 @@ var MetadataPlot = Panel.extend({ data: this.plottableData.data, colDict: {}, series: {}, - format: plotConfig.format || 'scatter' + format: plotConfig.format || 'scatter', + adjacentItems: !!plotConfig.folder }; plotData.columns.forEach((col) => { plotData.colDict[col.key] = col; @@ -198,12 +206,142 @@ var MetadataPlot = Panel.extend({ }, onHover: function (evt) { - // this is a stub for wrapping. + if (!evt || !evt.points || evt.points.length < 1 || evt.points[0].pointIndex === undefined || !$('svg g.hoverlayer').length) { + return; + } + const idx = evt.points[0].pointIndex; + const image = this.lastPlotData.data[idx].image; + const maxw = 100, maxh = 100; + const imgw = Math.min(Math.ceil(image.right - image.left) * 2, maxw); + const imgh = Math.min(Math.ceil(image.bottom - image.top) * 2, maxh); + const regionUrl = `api/v1/item/${image.id}/tiles/region?width=${imgw}&height=${imgh}&left=${image.left}&top=${image.top}&right=${image.right}&bottom=${image.bottom}`; + let x = parseFloat($('svg g.hoverlayer g.hovertext text[x]').attr('x')); + let y = parseFloat($('svg g.hoverlayer g.hovertext text[y]').attr('y')); + x = x === 0 ? -maxw / 2 + (maxw - imgw) / 2 : x < 0 ? x - maxw + (maxw - imgw) : x; + const d = $('svg g.hoverlayer g.hovertext path[d]').attr('d'); + if (d && (d.split('L6,').length >= 2 || d.split('L-6,').length >= 2)) { + const y2 = parseFloat(d.split('v')[2]); + y += Math.abs(y2) - maxh - 13 + (maxh - imgh) / 2; + } else if (d && d.split('v').length === 2) { + const y2 = parseFloat(d.split('v')[1]); + y += Math.abs(y2) - maxh - 13 + (maxh - imgh) / 2; + } else { + y = -3; + } + $('svg g.hoverlayer g.hovertext image.hoverthumbnail').remove(); + $('svg g.hoverlayer g.hovertext').html($('svg g.hoverlayer g.hovertext').html() + ``); }, adjustHoverText: function (d, parts, plotData) { }, + onSelect: function (evt, plotData) { + if (plotData.colDict['_3_annotation.id'] === undefined || plotData.colDict['_5_annotationelement.id'] === undefined) { + return; + } + // evt is undefined when selection is cleared + if (evt === undefined) { + this.parentView._resetSelection(); + return; + } + // evt.points is an array with data, fullData, pointNumber, and pointIndex + const annots = {}; + const elements = []; + evt.points.forEach((pt) => { + const row = plotData.data[pt.pointIndex]; + const annotid = row[plotData.colDict['_3_annotation.id'].index]; + const elid = row[plotData.colDict['_5_annotationelement.id'].index]; + if (annotid === undefined || elid === undefined) { + return; + } + if (annots[annotid] === undefined) { + annots[annotid] = this.parentView.annotationSelector.collection.get(annotid) || null; + } + if (annots[annotid] === null || !annots[annotid]._elements || !annots[annotid]._elements._byId || !annots[annotid]._elements._byId[elid]) { + return; + } + elements.push(annots[annotid]._elements._byId[elid]); + }); + if (!elements.length) { + return; + } + this.parentView._resetSelection(); + elements.forEach((element, idx) => { + this.parentView._selectElement(element, {silent: idx !== elements.length - 1}); + }); + }, + + onElementSelect: function (elements) { + if (!this.lastPlotData || !this.lastPlotData.colDict['_5_annotationelement.id'] || !elements.models) { + return; + } + const ids = {}; + elements.models.forEach((el) => { + ids[el.id] = true; + }); + const colidx = this.lastPlotData.colDict['_5_annotationelement.id'].index; + const points = this.lastPlotData.data.map((row, idx) => ids[row[colidx]] ? idx : null).filter((idx) => idx !== null); + if (!points.length) { + return; + } + window.Plotly.restyle(this._plotlyNode[0], {selectedpoints: [points]}); + }, + + _formatNumber: function (val, significant) { + if (isNaN(parseFloat(val))) { + return val; + } + if (!significant || significant < 1) { + significant = 3; + } + const digits = Math.min(significant, Math.max(0, significant - Math.floor(Math.log10(Math.abs(val))))); + return val.toFixed(digits); + }, + + _hoverText: function (d, plotData) { + const used = {}; + let parts = []; + let key = '_0_item.name'; + if (plotData.adjacentItems && plotData.colDict[key] && d[plotData.colDict[key].index] !== undefined && plotData.colDict[key].distinctcount !== 1) { + used[key] = true; + parts.push(plotData.colDict[key]); + } + key = '_1_annotation.name'; + if (plotData.colDict[key] && d[plotData.colDict[key].index] !== undefined) { + used[key] = true; + parts.push(plotData.colDict[key]); + } + ['x', 'y', 'r', 'c', 's'].forEach((series) => { + if (plotData.series[series] && d[plotData.series[series].index] !== undefined && used[plotData.series[series].key] === undefined) { + used[plotData.series[series].key] = true; + parts.push(plotData.series[series]); + } + }); + parts = parts.map((col) => `${col.title}: ${col.type === 'number' ? this._formatNumber(d[col.index]) : d[col.index]}`); + + const imageDict = { + id: '_2_item.id', + left: '_bbox.x0', + top: '_bbox.y0', + right: '_bbox.x1', + bottom: '_bbox.y1' + }; + if (Object.values(imageDict).every((v) => plotData.colDict[v] && d[plotData.colDict[v].index] !== undefined)) { + d.image = {}; + Object.entries(imageDict).forEach(([k, v]) => { + d.image[k] = d[plotData.colDict[v].index]; + }); + /* Plotly adds hovertext rows in its own way, so add several rows + * that are blank and of some width that we can dynamically + * replace. */ + for (let i = 0; i < 8; i += 1) { + parts.push('                                        '); + } + } + this.adjustHoverText(d, parts, plotData); + return '' + parts.join('
') + '
'; + }, + plotDataToPlotly: function (plotData) { const colorBrewerPaired12 = ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', '#ffff99', '#b15928']; const viridis = ['#440154', '#482172', '#423d84', '#38578c', '#2d6f8e', '#24858d', '#1e9a89', '#2ab07e', '#51c468', '#86d449', '#c2df22', '#fde724']; @@ -214,17 +352,11 @@ var MetadataPlot = Panel.extend({ const plotlyData = { x: plotData.data.map((d) => d[plotData.series.x.index]), y: plotData.data.map((d) => d[plotData.series.y.index]), - hovertext: plotData.data.map((d) => { - const parts = []; - ['x', 'y', 'r', 'c', 's'].forEach((series) => { - if (plotData.series[series] && d[plotData.series[series].index] !== undefined) { - parts.push(`${plotData.series[series].title}: ${d[plotData.series[series].index]}`); - } - }); - this.adjustHoverText(d, parts, plotData); - return '' + parts.join('
') + '
'; - }), + hovertext: plotData.data.map((d) => this._hoverText(d, plotData)), hoverinfo: 'text', + hoverlabel: { + font: {size: 10} + }, marker: { symbol: plotData.series.s && plotData.series.s.distinct ? plotData.data.map((d) => plotData.series.s.distinct.indexOf(d[plotData.series.s.index])) : 0, size: plotData.series.r && (plotData.series.r.type === 'number' || plotData.series.r.distinctcount) @@ -312,8 +444,10 @@ var MetadataPlot = Panel.extend({ plotOptions.xaxis = {title: {text: plotData.format !== 'violin' ? `${plotData.series.x.title}` : `${plotData.series.s.title}`}}; plotOptions.yaxis = {title: {text: `${plotData.series.y.title}`}}; } + this._plotlyNode = elem; window.Plotly.newPlot(elem[0], this.plotDataToPlotly(plotData), plotOptions); elem[0].on('plotly_hover', (evt) => this.onHover(evt)); + elem[0].on('plotly_selected', (evt) => this.onSelect(evt, plotData)); }); } return this; diff --git a/histomicsui/web_client/templates/dialogs/metadataPlot.pug b/histomicsui/web_client/templates/dialogs/metadataPlot.pug index acc82c4a..a70a5052 100644 --- a/histomicsui/web_client/templates/dialogs/metadataPlot.pug +++ b/histomicsui/web_client/templates/dialogs/metadataPlot.pug @@ -22,7 +22,7 @@ {key: 'r', label: 'Radius'}, {key: 'c', label: 'Color'}, {key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin plots'}] - for series in seriesList + for series, seriesidx in seriesList .form-group label(for='h-plot-series-' + series.key) #{series.label} if series.comment @@ -30,9 +30,10 @@ select.form-control(id='h-plot-series-' + series.key) if !series.number option(value='_none_', selected=plotConfig[series.key] === undefined) None - each opt in plotOptions + each opt, optidx in plotOptions if (!series.number || opt.type === 'number') && (!series.string || opt.type === 'string' || opt.distinct) - var selected = plotConfig[series.key] === opt.key + - if (plotConfig[series.key] === undefined && series.number === true && optidx === seriesidx) { selected = true; } option(value=opt.key, selected=selected) #{opt.title} .form-group label(for='h-plot-folder') diff --git a/histomicsui/web_client/templates/panels/metadataPlot.pug b/histomicsui/web_client/templates/panels/metadataPlot.pug index 7d16328c..a764986c 100644 --- a/histomicsui/web_client/templates/panels/metadataPlot.pug +++ b/histomicsui/web_client/templates/panels/metadataPlot.pug @@ -10,4 +10,4 @@ block controls i.icon-resize-full(title="Maximize") block content - .h-metadata-plot-area + #h-metadata-plot.h-metadata-plot-area From d8eb9ce85f75d8a9592e7146acd5968801895002 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 23 Jul 2024 16:37:19 -0400 Subject: [PATCH 19/47] Update plotly and how selections are handled. Ugly because plotly.js v2 doesn't expose unselect in any meaningful way. --- histomicsui/web_client/package.json | 2 +- histomicsui/web_client/panels/MetadataPlot.js | 32 +++++++++++++++++-- histomicsui/web_client/webpack.helper.js | 4 ++- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/histomicsui/web_client/package.json b/histomicsui/web_client/package.json index 673e5763..98449820 100644 --- a/histomicsui/web_client/package.json +++ b/histomicsui/web_client/package.json @@ -38,7 +38,7 @@ "bootstrap-submenu": "^2.0.4", "copy-webpack-plugin": "^4.5.2", "petite-vue": "^0.4.1", - "plotly.js": "^1.58.5", + "plotly.js": "^2.34.0", "sinon": "^2.1.0", "tinycolor2": "~1.4.1", "url-search-params-polyfill": "^8.1.1", diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index c7effd15..ef06bae8 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -236,10 +236,18 @@ var MetadataPlot = Panel.extend({ }, onSelect: function (evt, plotData) { + if (this._elementSelect) { + this._elementSelect -= 1; + if (this._afterSelect && !this._elementSelect) { + this._afterSelect(); + this._afterSelect = null; + } + return; + } if (plotData.colDict['_3_annotation.id'] === undefined || plotData.colDict['_5_annotationelement.id'] === undefined) { return; } - // evt is undefined when selection is cleared + // evt is undefined when the selection is cleared if (evt === undefined) { this.parentView._resetSelection(); return; @@ -284,7 +292,27 @@ var MetadataPlot = Panel.extend({ if (!points.length) { return; } - window.Plotly.restyle(this._plotlyNode[0], {selectedpoints: [points]}); + /* Deselect any selection on plotly. There is no exposed function to + * do this, so we synthesize several actions: (a) switch to box select + * mode, (b) double click on the plot, (c) ignore the first selection + * event (first click), (d) on the second selection event, the plot no + * longer has a selection, so we can specify the selected points (in + * the _afterSelect callback), (e) switch back to whatever tool the + * user had selected on the plot. */ + this._elementSelect = 2; + const curactive = this._plotlyNode.find('.modebar-btn.active'); + this._afterSelect = () => { + window.Plotly.restyle(this._plotlyNode[0], {selectedpoints: [points]}); + if (curactive.length) { + curactive[0].dispatchEvent(new MouseEvent('click')); + } + }; + const plot = this._plotlyNode.find('.drag').first()[0]; + for (let i = 0; i <= 2; i += 1) { + this._plotlyNode.find('.modebar-btn[data-val="select"]')[0].dispatchEvent(new MouseEvent('click')); + plot.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, cancelable: true, view: window, clientX: 10, clientY: 10})); + plot.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, cancelable: true, view: window, clientX: 10, clientY: 10})); + } }, _formatNumber: function (val, significant) { diff --git a/histomicsui/web_client/webpack.helper.js b/histomicsui/web_client/webpack.helper.js index 0275578f..a3aac6cf 100644 --- a/histomicsui/web_client/webpack.helper.js +++ b/histomicsui/web_client/webpack.helper.js @@ -12,7 +12,9 @@ module.exports = function (config) { to: config.output.path, toType: 'dir' }, { - from: require.resolve('plotly.js/dist/plotly.min.js'), + // the minified version fails in our test environment because + // plotly.min.js uses some modern javascript (plotly.js doesn't) + from: require.resolve(process.env.TOX_ENV_NAME ? 'plotly.js/dist/plotly.js' : 'plotly.js/dist/plotly.min.js'), to: path.join(config.output.path, 'extra', 'plotly.js'), toType: 'file' }, { From 929b8ee7c8fa807f08f93d8469e6a8e44faf49dd Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 24 Jul 2024 13:01:56 -0400 Subject: [PATCH 20/47] Don't reload plot data if it hasn't changed --- histomicsui/web_client/panels/MetadataPlot.js | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index ef06bae8..68d96dbe 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -83,6 +83,7 @@ var MetadataPlot = Panel.extend({ this.plottableData = null; if (this.plottableDataPromise) { this.plottableDataPromise.abort(); + this.plottableDataPromise = null; } const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); @@ -162,17 +163,22 @@ var MetadataPlot = Panel.extend({ if (this._currentAnnotations && this._currentAnnotations.length >= 1) { keys = keys.concat(['_3_annotation.id', '_5_annotationelement.id']); } - this.plottableDataPromise = restRequest({ - url: `annotation/item/${this.item.id}/plot/data`, - method: 'POST', - error: null, - data: { - adjacentItems: !!this.plotConfig.folder, - keys: keys.join(','), - requiredKeys: requiredKeys.join(','), - annotations: JSON.stringify(this._currentAnnotations) - } - }).done((result) => { + const fetch = { + adjacentItems: !!this.plotConfig.folder, + keys: keys.join(','), + requiredKeys: requiredKeys.join(','), + annotations: JSON.stringify(this._currentAnnotations) + }; + if (!this.plottableDataPromise || !_.isEqual(this._lastPlottableDataFetch, fetch)) { + this.plottableDataPromise = restRequest({ + url: `annotation/item/${this.item.id}/plot/data`, + method: 'POST', + error: null, + data: fetch + }); + } + this._lastPlottableDataFetch = fetch; + this.plottableDataPromise.done((result) => { this.plottableData = result; }); }, From a20cc2a603caf9aa2722901bbd9fd281fe6d2db2 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 25 Jul 2024 14:38:09 -0400 Subject: [PATCH 21/47] Update plottable data to match changes in large_image. --- histomicsui/web_client/panels/MetadataPlot.js | 30 +++++++++---------- .../templates/dialogs/metadataPlot.pug | 6 +++- setup.py | 2 +- tests/web_client_specs/metadataPlotSpec.js | 14 ++++----- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 68d96dbe..e0c4ca43 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -156,12 +156,12 @@ var MetadataPlot = Panel.extend({ requiredKeys.push(this.plotConfig[k]); } }); - keys = keys.concat(['_0_item.name', '_2_item.id', '_bbox.x0', '_bbox.y0', '_bbox.x1', '_bbox.y1']); + keys = keys.concat(['item.name', 'item.id', 'bbox.x0', 'bbox.y0', 'bbox.x1', 'bbox.y1']); if (this._currentAnnotations && this._currentAnnotations.length > 1) { - keys = keys.concat(['_1_annotation.name']); + keys = keys.concat(['annotation.name']); } if (this._currentAnnotations && this._currentAnnotations.length >= 1) { - keys = keys.concat(['_3_annotation.id', '_5_annotationelement.id']); + keys = keys.concat(['annotation.id', 'annotationelement.id']); } const fetch = { adjacentItems: !!this.plotConfig.folder, @@ -250,7 +250,7 @@ var MetadataPlot = Panel.extend({ } return; } - if (plotData.colDict['_3_annotation.id'] === undefined || plotData.colDict['_5_annotationelement.id'] === undefined) { + if (plotData.colDict['annotation.id'] === undefined || plotData.colDict['annotationelement.id'] === undefined) { return; } // evt is undefined when the selection is cleared @@ -263,8 +263,8 @@ var MetadataPlot = Panel.extend({ const elements = []; evt.points.forEach((pt) => { const row = plotData.data[pt.pointIndex]; - const annotid = row[plotData.colDict['_3_annotation.id'].index]; - const elid = row[plotData.colDict['_5_annotationelement.id'].index]; + const annotid = row[plotData.colDict['annotation.id'].index]; + const elid = row[plotData.colDict['annotationelement.id'].index]; if (annotid === undefined || elid === undefined) { return; } @@ -286,14 +286,14 @@ var MetadataPlot = Panel.extend({ }, onElementSelect: function (elements) { - if (!this.lastPlotData || !this.lastPlotData.colDict['_5_annotationelement.id'] || !elements.models) { + if (!this.lastPlotData || !this.lastPlotData.colDict['annotationelement.id'] || !elements.models) { return; } const ids = {}; elements.models.forEach((el) => { ids[el.id] = true; }); - const colidx = this.lastPlotData.colDict['_5_annotationelement.id'].index; + const colidx = this.lastPlotData.colDict['annotationelement.id'].index; const points = this.lastPlotData.data.map((row, idx) => ids[row[colidx]] ? idx : null).filter((idx) => idx !== null); if (!points.length) { return; @@ -335,12 +335,12 @@ var MetadataPlot = Panel.extend({ _hoverText: function (d, plotData) { const used = {}; let parts = []; - let key = '_0_item.name'; + let key = 'item.name'; if (plotData.adjacentItems && plotData.colDict[key] && d[plotData.colDict[key].index] !== undefined && plotData.colDict[key].distinctcount !== 1) { used[key] = true; parts.push(plotData.colDict[key]); } - key = '_1_annotation.name'; + key = 'annotation.name'; if (plotData.colDict[key] && d[plotData.colDict[key].index] !== undefined) { used[key] = true; parts.push(plotData.colDict[key]); @@ -354,11 +354,11 @@ var MetadataPlot = Panel.extend({ parts = parts.map((col) => `${col.title}: ${col.type === 'number' ? this._formatNumber(d[col.index]) : d[col.index]}`); const imageDict = { - id: '_2_item.id', - left: '_bbox.x0', - top: '_bbox.y0', - right: '_bbox.x1', - bottom: '_bbox.y1' + id: 'item.id', + left: 'bbox.x0', + top: 'bbox.y0', + right: 'bbox.x1', + bottom: 'bbox.y1' }; if (Object.values(imageDict).every((v) => plotData.colDict[v] && d[plotData.colDict[v].index] !== undefined)) { d.image = {}; diff --git a/histomicsui/web_client/templates/dialogs/metadataPlot.pug b/histomicsui/web_client/templates/dialogs/metadataPlot.pug index a70a5052..3c698bc3 100644 --- a/histomicsui/web_client/templates/dialogs/metadataPlot.pug +++ b/histomicsui/web_client/templates/dialogs/metadataPlot.pug @@ -22,6 +22,10 @@ {key: 'r', label: 'Radius'}, {key: 'c', label: 'Color'}, {key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin plots'}] + - + var numNumbers = 0; + var numIndex = []; + plotOptions.forEach((po, idx) => { numIndex.push(numNumbers); if (po.type === 'number') { numNumbers += 1; }}); for series, seriesidx in seriesList .form-group label(for='h-plot-series-' + series.key) #{series.label} @@ -33,7 +37,7 @@ each opt, optidx in plotOptions if (!series.number || opt.type === 'number') && (!series.string || opt.type === 'string' || opt.distinct) - var selected = plotConfig[series.key] === opt.key - - if (plotConfig[series.key] === undefined && series.number === true && optidx === seriesidx) { selected = true; } + - if (plotConfig[series.key] === undefined && series.number === true && numIndex[optidx] === seriesidx) { selected = true; } option(value=opt.key, selected=selected) #{opt.title} .form-group label(for='h-plot-folder') diff --git a/setup.py b/setup.py index 4640e251..097a310e 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def prerelease_local_scheme(version): 'Programming Language :: Python :: 3.12', ], install_requires=[ - 'girder-large-image-annotation>=1.29.2', + 'girder-large-image-annotation>=1.29.4', 'girder-slicer-cli-web[girder]>=1.4.0', 'cachetools', 'orjson', diff --git a/tests/web_client_specs/metadataPlotSpec.js b/tests/web_client_specs/metadataPlotSpec.js index 7b0e9bb0..f338c94f 100644 --- a/tests/web_client_specs/metadataPlotSpec.js +++ b/tests/web_client_specs/metadataPlotSpec.js @@ -103,11 +103,11 @@ girderTest.promise.done(function () { return $('#h-plot-series-x').length; }, 'dialog controls to exist'); runs(function () { - $('#h-plot-series-x').val('gloms.pas.item'); - $('#h-plot-series-y').val('gloms.area.item'); - $('#h-plot-series-r').val('gloms.aspect.item'); - $('#h-plot-series-c').val('gloms.label.item'); - $('#h-plot-series-s').val('_0_item.name'); + $('#h-plot-series-x').val('data.gloms.0.pas'); + $('#h-plot-series-y').val('data.gloms.0.area'); + $('#h-plot-series-r').val('data.gloms.0.aspect'); + $('#h-plot-series-c').val('data.gloms.0.label'); + $('#h-plot-series-s').val('item.name'); $('.h-submit').click(); }); girderTest.waitForLoad(); @@ -130,8 +130,8 @@ girderTest.promise.done(function () { girderTest.waitForDialog(); runs(function () { // switch some other options, too. - $('#h-plot-series-r').val('gloms.label.item'); - $('#h-plot-series-c').val('gloms.average.item'); + $('#h-plot-series-r').val('data.gloms.0.label'); + $('#h-plot-series-c').val('data.gloms.0.average'); $('#h-plot-folder').prop('checked', false); $('.h-submit').click(); }); From ab744d2d5cb2034b48c5e001d9e00d7b90499f91 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 26 Jul 2024 09:12:07 -0400 Subject: [PATCH 22/47] Reduce console warning on plot hover --- histomicsui/web_client/panels/MetadataPlot.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index e0c4ca43..3d992ef1 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -217,6 +217,9 @@ var MetadataPlot = Panel.extend({ } const idx = evt.points[0].pointIndex; const image = this.lastPlotData.data[idx].image; + if (!image) { + return; + } const maxw = 100, maxh = 100; const imgw = Math.min(Math.ceil(image.right - image.left) * 2, maxw); const imgh = Math.min(Math.ceil(image.bottom - image.top) * 2, maxh); From 55de3fee82016d49d9dd71fec7a2808a8a6d518e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 1 Aug 2024 09:46:39 -0400 Subject: [PATCH 23/47] Don't show decimal places on integers --- histomicsui/web_client/panels/MetadataPlot.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 3d992ef1..99af2099 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -331,7 +331,10 @@ var MetadataPlot = Panel.extend({ if (!significant || significant < 1) { significant = 3; } - const digits = Math.min(significant, Math.max(0, significant - Math.floor(Math.log10(Math.abs(val))))); + let digits = Math.min(significant, Math.max(0, significant - Math.floor(Math.log10(Math.abs(val))))); + if (parseFloat(val) === parseInt(val, 10)) { + digits = 0; + } return val.toFixed(digits); }, From 6b041c2648ea7c962d2d1975f705fbea0e5bf18b Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 13 Aug 2024 11:54:09 -0400 Subject: [PATCH 24/47] Limit length of text in hover info on plots In many instances this will prevent overflow. Plotly doesn't support sensible overflow or wrapping, so this is an alternative. --- histomicsui/web_client/panels/MetadataPlot.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 99af2099..16421a53 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -357,7 +357,31 @@ var MetadataPlot = Panel.extend({ parts.push(plotData.series[series]); } }); - parts = parts.map((col) => `${col.title}: ${col.type === 'number' ? this._formatNumber(d[col.index]) : d[col.index]}`); + const maximized = this.$el.closest('.h-panel-maximized').length > 0; + const maxtotallen = 50; + const maxlen = 32; + parts = parts.map((col) => { + let title = '' + col.title; + let val = d[col.index]; + if (val === undefined || val === null) { + val = ''; + } else if (col.type === 'number') { + val = this._formatNumber(val); + } else { + val = '' + val; + } + const result = title + ': ' + val; + if (maximized || result.length <= maxtotallen) { + return result; + } + if (val.length > maxlen + 3) { + val = val.substring(0, maxlen).replace(/\.+$/, '') + '...'; + } + if (title.length + val.length + 2 > maxtotallen + 3) { + title = title.substring(0, maxtotallen - val.length - 2).replace(/\.+$/, '') + '...'; + } + return title + ': ' + val; + }); const imageDict = { id: 'item.id', From f62c39c7892c8bd7c409d3a3994d80fe477d8791 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 14 Aug 2024 14:34:52 -0400 Subject: [PATCH 25/47] Don't try to render hover images that don't exist --- histomicsui/web_client/panels/MetadataPlot.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 16421a53..4c99ea83 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -223,6 +223,9 @@ var MetadataPlot = Panel.extend({ const maxw = 100, maxh = 100; const imgw = Math.min(Math.ceil(image.right - image.left) * 2, maxw); const imgh = Math.min(Math.ceil(image.bottom - image.top) * 2, maxh); + if (!imgw || !imgh) { + return; + } const regionUrl = `api/v1/item/${image.id}/tiles/region?width=${imgw}&height=${imgh}&left=${image.left}&top=${image.top}&right=${image.right}&bottom=${image.bottom}`; let x = parseFloat($('svg g.hoverlayer g.hovertext text[x]').attr('x')); let y = parseFloat($('svg g.hoverlayer g.hovertext text[y]').attr('y')); From ebb0dc8b88d1ba036b5410db82328375e3a01c66 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 15 Aug 2024 09:47:57 -0400 Subject: [PATCH 26/47] Better handle long analyses menu This mostly resolves #409. There are still issues when there are lots of versions, lots of clis, or narrow windows. --- .pre-commit-config.yaml | 10 +- .../stylesheets/layout/headerAnalyses.styl | 5 + .../templates/layout/headerAnalyses.pug | 91 +++++++++++++++---- .../views/layout/HeaderAnalysesView.js | 7 +- tests/test_tcga.py | 2 +- 5 files changed, 89 insertions(+), 26 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71675ec4..7f4472cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-ast @@ -43,7 +43,7 @@ repos: files: README.rst name: rst-linter of README.rst - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell args: @@ -62,13 +62,13 @@ repos: - './histomicsui/web_client/package.json' - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.3 + rev: v0.6.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] types_or: [python, pyi, jupyter] - repo: https://github.com/asottile/pyupgrade - rev: v3.15.0 + rev: v3.17.0 hooks: - id: pyupgrade args: @@ -79,6 +79,6 @@ repos: hooks: - id: autopep8 - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.1.1 hooks: - id: flake8 diff --git a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl index 15fb3a14..c1cde948 100644 --- a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl +++ b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl @@ -1,3 +1,8 @@ ul.h-analyses-dropdown + min-width 120px + li > a padding 3px 20px + + .dropdown-menu + min-width 120px diff --git a/histomicsui/web_client/templates/layout/headerAnalyses.pug b/histomicsui/web_client/templates/layout/headerAnalyses.pug index 22f6d618..080f8ba3 100644 --- a/histomicsui/web_client/templates/layout/headerAnalyses.pug +++ b/histomicsui/web_client/templates/layout/headerAnalyses.pug @@ -1,21 +1,74 @@ +mixin analysisMenu(image, imageName) + li.dropdown-submenu + a(tabindex='0', href='#', data-name=imageName) #{imageName} + ul.dropdown-menu + each version, versionName in image + li.dropdown-submenu + a(tabindex='0', href='#') #{versionName} + ul.dropdown-menu + each cli, cliName in version + - var api = cli.run.replace(/\/run$/, ''); + li + a.h-analysis-item( + tabindex='0', + href='#', + data-api=api, + data-image=imageName, + data-version=versionName, + data-cli=cliName) #{cliName} + a.h-analyses-dropdown-link(data-toggle='dropdown') | #[span.icon-tasks] Analyses #[span.icon-down-open] -ul.h-analyses-dropdown.dropdown-menu(role='menu') - each image, imageName in analyses - li.dropdown-submenu - a(tabindex='0', href='#', data-name=imageName) #{imageName} - ul.dropdown-menu - each version, versionName in image - li.dropdown-submenu - a(tabindex='0', href='#') #{versionName} - ul.dropdown-menu - each cli, cliName in version - - var api = cli.run.replace(/\/run$/, ''); - li - a.h-analysis-item( - tabindex='0', - href='#', - data-api=api, - data-image=imageName, - data-version=versionName, - data-cli=cliName) #{cliName} +- + let depth = 0; + let rows = maxEntries; + let keyList = Object.keys(analyses).sort(); + let entries = keyList.length; + if (entries > maxEntries * maxEntries * maxEntries) { + rows = Math.ceil(Math.pow(entries, 1./3)); + } + if (entries > maxEntries * maxEntries) { + depth = 2; + } else if (entries > maxEntries) { + depth = 1; + } +if depth === 2 + ul.h-analyses-dropdown.dropdown-menu(role='menu') + for d2v, d2i in Array(Math.ceil(entries / rows / rows)) + - + let start2 = d2i * rows * rows; + let end2 = Math.min((d2i + 1) * rows * rows, entries); + li.dropdown-submenu + a(tabindex='0', href='#', title=keyList[start2] + ' to ' + keyList[end2 - 1]) #{start2 + 1} to #{end2} + ul.dropdown-menu + for d1v, d1i in Array(Math.ceil((end2 - start2) / rows)) + - + let start1 = start2 + d1i * rows; + let end1 = Math.min(start2 + (d1i + 1) * rows, entries); + li.dropdown-submenu + a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1} + ul.dropdown-menu + for d0v, d0i in Array(end1 - start1) + - + let imageName = keyList[start1 + d0i]; + let image = analyses[imageName]; + +analysisMenu(image, imageName) +else if depth === 1 + ul.h-analyses-dropdown.dropdown-menu(role='menu') + for d1v, d1i in Array(Math.ceil(entries / rows)) + - + let start1 = d1i * rows; + let end1 = Math.min((d1i + 1) * rows, entries); + li.dropdown-submenu + a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1} + ul.dropdown-menu + for d0v, d0i in Array(end1 - start1) + - + let imageName = keyList[start1 + d0i]; + let image = analyses[imageName]; + +analysisMenu(image, imageName) +else + ul.h-analyses-dropdown.dropdown-menu(role='menu') + each imageName in keyList + - let image = analyses[imageName]; + +analysisMenu(image, imageName) diff --git a/histomicsui/web_client/views/layout/HeaderAnalysesView.js b/histomicsui/web_client/views/layout/HeaderAnalysesView.js index bc673efa..6b05bdc1 100644 --- a/histomicsui/web_client/views/layout/HeaderAnalysesView.js +++ b/histomicsui/web_client/views/layout/HeaderAnalysesView.js @@ -27,10 +27,15 @@ var HeaderUserView = View.extend({ restRequest({ url: 'slicer_cli_web/docker_image' }).then((analyses) => { + let maxEntries = Math.max(10, Math.floor(($('.h-image-view-body').height() || 0) / 26) - 10); + if (Object.keys(analyses).length > maxEntries * maxEntries) { + maxEntries = Math.max(10, Math.floor((($('.h-image-view-body').height() || 0) / 26 - 10) / 2)); + } if (_.keys(analyses || {}).length > 0) { this.$el.removeClass('hidden'); this.$el.html(headerAnalysesTemplate({ - analyses: analyses || {} + analyses: analyses || {}, + maxEntries: maxEntries })); this.$('.h-analyses-dropdown-link').submenupicker(); } else { diff --git a/tests/test_tcga.py b/tests/test_tcga.py index 7dba14d4..2c9349fa 100644 --- a/tests/test_tcga.py +++ b/tests/test_tcga.py @@ -20,7 +20,7 @@ from . import girder_utilities as utilities -@pytest.fixture() +@pytest.fixture def singular(tmp_path_factory): root_tmp_dir = tmp_path_factory.getbasetemp().parent with filelock.FileLock(root_tmp_dir / '.test_tcga.lock'): From d5c92b6c35009b124407972a78f3ad8dd01783fb Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 16 Aug 2024 09:03:27 -0400 Subject: [PATCH 27/47] Better handle long analyses menu This should now handle any analysis menu that doesn't have more versions than the screen height or names longer than some maximum. --- .../stylesheets/layout/headerAnalyses.styl | 90 +++++++++++++++++++ .../templates/layout/headerAnalyses.pug | 59 ++++++++---- .../views/layout/HeaderAnalysesView.js | 7 +- 3 files changed, 132 insertions(+), 24 deletions(-) diff --git a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl index c1cde948..96513daf 100644 --- a/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl +++ b/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl @@ -6,3 +6,93 @@ ul.h-analyses-dropdown .dropdown-menu min-width 120px + +ul.dropdown-menu[row_offset="1"] + top -26px + +ul.dropdown-menu[row_offset="2"] + top -52px + +ul.dropdown-menu[row_offset="3"] + top -78px + +ul.dropdown-menu[row_offset="4"] + top -104px + +ul.dropdown-menu[row_offset="5"] + top -130px + +ul.dropdown-menu[row_offset="6"] + top -156px + +ul.dropdown-menu[row_offset="7"] + top -182px + +ul.dropdown-menu[row_offset="8"] + top -208px + +ul.dropdown-menu[row_offset="9"] + top -234px + +ul.dropdown-menu[row_offset="10"] + top -260px + +ul.dropdown-menu[row_offset="11"] + top -286px + +ul.dropdown-menu[row_offset="12"] + top -312px + +ul.dropdown-menu[row_offset="13"] + top -338px + +ul.dropdown-menu[row_offset="14"] + top -364px + +ul.dropdown-menu[row_offset="15"] + top -390px + +ul.dropdown-menu[row_offset="16"] + top -416px + +ul.dropdown-menu[row_offset="17"] + top -442px + +ul.dropdown-menu[row_offset="18"] + top -468px + +ul.dropdown-menu[row_offset="19"] + top -494px + +ul.dropdown-menu[row_offset="20"] + top -520px + +ul.dropdown-menu[row_offset="21"] + top -546px + +ul.dropdown-menu[row_offset="22"] + top -572px + +ul.dropdown-menu[row_offset="23"] + top -598px + +ul.dropdown-menu[row_offset="24"] + top -624px + +ul.dropdown-menu[row_offset="25"] + top -650px + +ul.dropdown-menu[row_offset="26"] + top -676px + +ul.dropdown-menu[row_offset="27"] + top -702px + +ul.dropdown-menu[row_offset="28"] + top -728px + +ul.dropdown-menu[row_offset="29"] + top -754px + +ul.dropdown-menu[row_offset="30"] + top -780px diff --git a/histomicsui/web_client/templates/layout/headerAnalyses.pug b/histomicsui/web_client/templates/layout/headerAnalyses.pug index 080f8ba3..39e52237 100644 --- a/histomicsui/web_client/templates/layout/headerAnalyses.pug +++ b/histomicsui/web_client/templates/layout/headerAnalyses.pug @@ -1,11 +1,15 @@ -mixin analysisMenu(image, imageName) +mixin analysisMenu(image, imageName, pos, maxRows) li.dropdown-submenu a(tabindex='0', href='#', data-name=imageName) #{imageName} - ul.dropdown-menu + - + let finalv = pos + Object.keys(image).length + let posv = pos - (finalv > maxRows ? finalv - maxRows : 0) + ul.dropdown-menu(row_offset=finalv > maxRows ? finalv - maxRows : 0) each version, versionName in image li.dropdown-submenu a(tabindex='0', href='#') #{versionName} - ul.dropdown-menu + - let finalc = posv + Object.keys(version).length + ul.dropdown-menu(row_offset=finalc > maxRows ? finalc - maxRows : 0) each cli, cliName in version - var api = cli.run.replace(/\/run$/, ''); li @@ -16,59 +20,76 @@ mixin analysisMenu(image, imageName) data-image=imageName, data-version=versionName, data-cli=cliName) #{cliName} + - posv += 1 a.h-analyses-dropdown-link(data-toggle='dropdown') | #[span.icon-tasks] Analyses #[span.icon-down-open] - let depth = 0; - let rows = maxEntries; + let rows = maxRows; let keyList = Object.keys(analyses).sort(); let entries = keyList.length; - if (entries > maxEntries * maxEntries * maxEntries) { + if (entries > maxRows * maxRows * maxRows) { rows = Math.ceil(Math.pow(entries, 1./3)); } - if (entries > maxEntries * maxEntries) { + if (entries > maxRows * maxRows) { depth = 2; - } else if (entries > maxEntries) { + } else if (entries > maxRows) { depth = 1; } -if depth === 2 - ul.h-analyses-dropdown.dropdown-menu(role='menu') +ul.h-analyses-dropdown.dropdown-menu(role='menu') + if depth === 2 + - let pos2 = 0 for d2v, d2i in Array(Math.ceil(entries / rows / rows)) - let start2 = d2i * rows * rows; let end2 = Math.min((d2i + 1) * rows * rows, entries); li.dropdown-submenu a(tabindex='0', href='#', title=keyList[start2] + ' to ' + keyList[end2 - 1]) #{start2 + 1} to #{end2} - ul.dropdown-menu + - + let final1 = pos1 + (end2 - start2) + let pos1 = pos2 - (final1 > maxRows ? final1 - maxRows : 0) + ul.dropdown-menu(row_offset=final1 > maxRows ? final1 - maxRows : 0) for d1v, d1i in Array(Math.ceil((end2 - start2) / rows)) - let start1 = start2 + d1i * rows; let end1 = Math.min(start2 + (d1i + 1) * rows, entries); li.dropdown-submenu a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1} - ul.dropdown-menu + - + let final0 = pos1 + (end1 - start1) + let pos0 = pos1 - (final0 > maxRows ? final0 - maxRows : 0) + ul.dropdown-menu(row_offset=final0 > maxRows ? final0 - maxRows : 0) for d0v, d0i in Array(end1 - start1) - let imageName = keyList[start1 + d0i]; let image = analyses[imageName]; - +analysisMenu(image, imageName) -else if depth === 1 - ul.h-analyses-dropdown.dropdown-menu(role='menu') + +analysisMenu(image, imageName, pos0, maxRows) + - pos0 += 1 + - pos1 += 1 + - pos2 += 1 + else if depth === 1 + - let pos1 = 0 for d1v, d1i in Array(Math.ceil(entries / rows)) - let start1 = d1i * rows; let end1 = Math.min((d1i + 1) * rows, entries); li.dropdown-submenu a(tabindex='0', href='#', title=keyList[start1] + ' to ' + keyList[end1 - 1]) #{start1 + 1} to #{end1} - ul.dropdown-menu + - + let final0 = pos1 + (end1 - start1) + let pos0 = pos1 - (final0 > maxRows ? final0 - maxRows : 0) + ul.dropdown-menu(row_offset=final0 > maxRows ? final0 - maxRows : 0) for d0v, d0i in Array(end1 - start1) - let imageName = keyList[start1 + d0i]; let image = analyses[imageName]; - +analysisMenu(image, imageName) -else - ul.h-analyses-dropdown.dropdown-menu(role='menu') + +analysisMenu(image, imageName, pos0, maxRows) + - pos0 += 1 + - pos1 += 1 + else + - let pos0 = 0 each imageName in keyList - let image = analyses[imageName]; - +analysisMenu(image, imageName) + +analysisMenu(image, imageName, pos0, maxRows) + - pos0 += 1 diff --git a/histomicsui/web_client/views/layout/HeaderAnalysesView.js b/histomicsui/web_client/views/layout/HeaderAnalysesView.js index 6b05bdc1..6e0f6732 100644 --- a/histomicsui/web_client/views/layout/HeaderAnalysesView.js +++ b/histomicsui/web_client/views/layout/HeaderAnalysesView.js @@ -27,15 +27,12 @@ var HeaderUserView = View.extend({ restRequest({ url: 'slicer_cli_web/docker_image' }).then((analyses) => { - let maxEntries = Math.max(10, Math.floor(($('.h-image-view-body').height() || 0) / 26) - 10); - if (Object.keys(analyses).length > maxEntries * maxEntries) { - maxEntries = Math.max(10, Math.floor((($('.h-image-view-body').height() || 0) / 26 - 10) / 2)); - } + const maxRows = Math.max(5, Math.floor((($('.h-image-view-body').height() || 0) - 8) / 26)); if (_.keys(analyses || {}).length > 0) { this.$el.removeClass('hidden'); this.$el.html(headerAnalysesTemplate({ analyses: analyses || {}, - maxEntries: maxEntries + maxRows: maxRows })); this.$('.h-analyses-dropdown-link').submenupicker(); } else { From f162be8c4fec02330d5acd419c4c058571128bb2 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 26 Aug 2024 13:22:04 -0400 Subject: [PATCH 28/47] Fix a typo in the README --- README.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 0a54eb70..3fa0e0b5 100644 --- a/README.rst +++ b/README.rst @@ -94,7 +94,7 @@ If you are making changes to the HistomicsUI frontend, you can make Girder watch Annotations and Metadata from Jobs ---------------------------------- -This handles ingesting annotations and metadata that are uploaded and associating them with existing large image items in the Girder database. These annotations and metadata re commonly generated through jobs, such as HistomicTK tasks, but can also be added manually. +This handles ingesting annotations and metadata that are uploaded and associating them with existing large image items in the Girder database. These annotations and metadata are commonly generated through jobs, such as HistomicTK tasks, but can also be added manually. If a file is uploaded to the Girder system that includes a ``reference`` record, and that ``reference`` record contains an ``identifier`` field and at least one of a ``fileId`` and an ``itemId`` field, specific identifiers can be used to ingest the results. If a ``userId`` is specified in the ``reference`` record, permissions for adding the annotation or metadata are associated with that user. @@ -124,7 +124,6 @@ This work was funded in part by the NIH grant U24-CA194362-01_. .. _Girder Worker: https://girder-worker.readthedocs.io/en/latest/ .. _large_image: https://github.com/girder/large_image .. _slicer_cli_web: https://github.com/girder/slicer_cli_web -.. _slicer execution model: https://www.slicer.org/slicerWiki/index.php/Slicer3:Execution_Model_Documentation .. _celery: http://www.celeryproject.org/ .. _HistomicsTK: https://github.com/DigitalSlideArchive/HistomicsTK .. _Digital Slide Archive: https://github.com/DigitalSlideArchive/digital_slide_archive From 7521e0560bf03d258be18f2495e27c3010459c06 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 3 Sep 2024 13:14:36 -0400 Subject: [PATCH 29/47] Adjust dialog spacing --- histomicsui/web_client/package.json | 2 +- histomicsui/web_client/stylesheets/layout/layout.styl | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/histomicsui/web_client/package.json b/histomicsui/web_client/package.json index 98449820..c9b1ee49 100644 --- a/histomicsui/web_client/package.json +++ b/histomicsui/web_client/package.json @@ -38,7 +38,7 @@ "bootstrap-submenu": "^2.0.4", "copy-webpack-plugin": "^4.5.2", "petite-vue": "^0.4.1", - "plotly.js": "^2.34.0", + "plotly.js": "2.34.0", "sinon": "^2.1.0", "tinycolor2": "~1.4.1", "url-search-params-polyfill": "^8.1.1", diff --git a/histomicsui/web_client/stylesheets/layout/layout.styl b/histomicsui/web_client/stylesheets/layout/layout.styl index 8ac71633..1b1cc2ca 100644 --- a/histomicsui/web_client/stylesheets/layout/layout.styl +++ b/histomicsui/web_client/stylesheets/layout/layout.styl @@ -108,3 +108,13 @@ body.hui-body[view-mode="dark"] .region-dropdown position relative z-index auto + +.modal-dialog .modal-content + .modal-header, .modal-footer, .modal-body + padding 8px 15px + + label + margin-bottom 3px + + .form-group + margin-bottom 8px From bb2154f12174d9fc3b081f1973e5d730f2adacbe Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 9 Sep 2024 16:14:57 -0400 Subject: [PATCH 30/47] Add dimension reduction options to the plot --- histomicsui/web_client/dialogs/metadataPlot.js | 7 +++++++ histomicsui/web_client/panels/MetadataPlot.js | 17 ++++++++++++++++- .../templates/dialogs/metadataPlot.pug | 8 +++++--- .../web_client/views/layout/LoginView.js | 1 - 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/histomicsui/web_client/dialogs/metadataPlot.js b/histomicsui/web_client/dialogs/metadataPlot.js index 911503c4..08968f14 100644 --- a/histomicsui/web_client/dialogs/metadataPlot.js +++ b/histomicsui/web_client/dialogs/metadataPlot.js @@ -1,3 +1,5 @@ +import $ from 'jquery'; + import View from '@girder/core/views/View'; import metadataPlotDialog from '../templates/dialogs/metadataPlot.pug'; @@ -35,6 +37,11 @@ const MetadataPlotDialog = View.extend({ configOptions[series] = val; } }); + ['u'].forEach((series) => { + const opts = this.$('#h-plot-series-' + series + ' option'); + const val = opts.filter((idx, o) => o.selected).map((idx, o) => $(o).val()).get(); + configOptions[series] = val.length ? val : undefined; + }); this.result = configOptions; this.$el.modal('hide'); } diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 4c99ea83..04d09fb6 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -150,7 +150,7 @@ var MetadataPlot = Panel.extend({ if (!keys.length) { return; } - const requiredKeys = []; + let requiredKeys = []; ['x', 'y'].forEach((k) => { if (this.plotConfig[k] !== undefined) { requiredKeys.push(this.plotConfig[k]); @@ -163,12 +163,27 @@ var MetadataPlot = Panel.extend({ if (this._currentAnnotations && this._currentAnnotations.length >= 1) { keys = keys.concat(['annotation.id', 'annotationelement.id']); } + if (this.plotConfig.u) { + ['x', 'y', 'r', 'c', 's'].forEach((k) => { + if (this.plotConfig[k] !== undefined && this.plotConfig[k].startsWith('compute.') && !keys.includes(this.plotConfig[k])) { + keys.push(this.plotConfig[k]); + } + if (this.plotConfig[k] !== undefined && this.plotConfig[k].startsWith('compute.') && !requiredKeys.includes(this.plotConfig[k])) { + requiredKeys.push(this.plotConfig[k]); + } + }); + keys = keys.concat(this.plotConfig.u); + requiredKeys = requiredKeys.concat(this.plotConfig.u); + } const fetch = { adjacentItems: !!this.plotConfig.folder, keys: keys.join(','), requiredKeys: requiredKeys.join(','), annotations: JSON.stringify(this._currentAnnotations) }; + if (this.plotConfig.u && this.plotConfig.u.length >= 3) { + fetch.compute = JSON.stringify({columns: this.plotConfig.u}); + } if (!this.plottableDataPromise || !_.isEqual(this._lastPlottableDataFetch, fetch)) { this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, diff --git a/histomicsui/web_client/templates/dialogs/metadataPlot.pug b/histomicsui/web_client/templates/dialogs/metadataPlot.pug index 3c698bc3..64c1e5ff 100644 --- a/histomicsui/web_client/templates/dialogs/metadataPlot.pug +++ b/histomicsui/web_client/templates/dialogs/metadataPlot.pug @@ -21,7 +21,8 @@ {key: 'y', label: 'y-axis', number: true}, {key: 'r', label: 'Radius'}, {key: 'c', label: 'Color'}, - {key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin plots'}] + {key: 's', label: 'Symbol', string: true, comment: 'Grouping for violin plots'}, + {key: 'u', label: 'Dimension Reducation', number: true, multiple: true}] - var numNumbers = 0; var numIndex = []; @@ -31,14 +32,15 @@ label(for='h-plot-series-' + series.key) #{series.label} if series.comment p.g-hui-description #{series.comment} - select.form-control(id='h-plot-series-' + series.key) + select.form-control(id='h-plot-series-' + series.key, multiple=series.multiple) if !series.number option(value='_none_', selected=plotConfig[series.key] === undefined) None each opt, optidx in plotOptions if (!series.number || opt.type === 'number') && (!series.string || opt.type === 'string' || opt.distinct) - var selected = plotConfig[series.key] === opt.key - if (plotConfig[series.key] === undefined && series.number === true && numIndex[optidx] === seriesidx) { selected = true; } - option(value=opt.key, selected=selected) #{opt.title} + - if (series.multiple) { selected = plotConfig[series.key] ? plotConfig[series.key].includes(opt.key) : false; } + option(value=opt.key, selected=selected) #{opt.title}#{opt.count > 1 && opt.distinctcount !== 1 ? ' *' : ''} .form-group label(for='h-plot-folder') input#h-plot-folder(type='checkbox', checked=plotConfig.folder) diff --git a/histomicsui/web_client/views/layout/LoginView.js b/histomicsui/web_client/views/layout/LoginView.js index a50d30b3..268b5821 100644 --- a/histomicsui/web_client/views/layout/LoginView.js +++ b/histomicsui/web_client/views/layout/LoginView.js @@ -9,7 +9,6 @@ wrap(LoginView, 'render', function (render) { const loginText = (settings['histomicsui.login_text'] || ''); if (loginText) { this.$('#g-login-form label[for="g-login"]').text(loginText); - console.log('HERE', loginText); // DWM:: } return null; }); From 664d2b1eb13f584ed1989d0a9a985713a5685243 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 11 Sep 2024 06:55:25 -0400 Subject: [PATCH 31/47] Adjust CI --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index c8b1be8c..31cfbb24 100644 --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ deps = setuptools_scm urllib3<1.26 -rrequirements-dev.txt + girder-large-image-annotation!=1.29.8,!=1.29.9 extras = analysis allowlist_externals = From 5843c377b8db59008bb15880eae65aaf72a1bde9 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 18 Sep 2024 13:42:05 -0400 Subject: [PATCH 32/47] Improve plot dialog and options Allow adjusting multi-select size. Show loading spinner. Don't send needless compute requests. --- histomicsui/web_client/dialogs/metadataPlot.js | 2 ++ histomicsui/web_client/panels/MetadataPlot.js | 17 ++++++++++++++--- .../stylesheets/dialogs/metadataPlot.styl | 7 +++++++ .../stylesheets/panels/metadataPlot.styl | 12 ++++++++++++ .../templates/dialogs/metadataPlot.pug | 2 +- .../templates/panels/metadataPlot.pug | 2 +- 6 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl diff --git a/histomicsui/web_client/dialogs/metadataPlot.js b/histomicsui/web_client/dialogs/metadataPlot.js index 08968f14..b891627f 100644 --- a/histomicsui/web_client/dialogs/metadataPlot.js +++ b/histomicsui/web_client/dialogs/metadataPlot.js @@ -4,6 +4,7 @@ import View from '@girder/core/views/View'; import metadataPlotDialog from '../templates/dialogs/metadataPlot.pug'; import '@girder/core/utilities/jquery/girderModal'; +import '../stylesheets/dialogs/metadataPlot.styl'; const MetadataPlotDialog = View.extend({ events: { @@ -22,6 +23,7 @@ const MetadataPlotDialog = View.extend({ plotOptions: this.plotOptions }) ).girderModal(this); + return this; }, diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 04d09fb6..5bfee9bd 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -88,6 +88,7 @@ var MetadataPlot = Panel.extend({ const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); // redo this when annotations are turned on or off + this.$el.addClass('loading'); this.plottableListPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/list`, method: 'POST', @@ -97,6 +98,8 @@ var MetadataPlot = Panel.extend({ } }).done((result) => { this.plottableList = result; + this.plottableListPromise = null; + this.$el.toggleClass('loading', !!(this.plottableListPromise || this.plottableDataPromise)); const plotOptions = this.getPlotOptions(); if (plotOptions.filter((v) => v.type === 'number' && v.count).length >= 2) { if (!hasPlot) { @@ -163,8 +166,10 @@ var MetadataPlot = Panel.extend({ if (this._currentAnnotations && this._currentAnnotations.length >= 1) { keys = keys.concat(['annotation.id', 'annotationelement.id']); } + let anyCompute = false; if (this.plotConfig.u) { ['x', 'y', 'r', 'c', 's'].forEach((k) => { + anyCompute = anyCompute || !!(this.plotConfig[k] !== undefined && this.plotConfig[k].startsWith('compute.')); if (this.plotConfig[k] !== undefined && this.plotConfig[k].startsWith('compute.') && !keys.includes(this.plotConfig[k])) { keys.push(this.plotConfig[k]); } @@ -172,8 +177,10 @@ var MetadataPlot = Panel.extend({ requiredKeys.push(this.plotConfig[k]); } }); - keys = keys.concat(this.plotConfig.u); - requiredKeys = requiredKeys.concat(this.plotConfig.u); + if (anyCompute) { + keys = keys.concat(this.plotConfig.u); + requiredKeys = requiredKeys.concat(this.plotConfig.u); + } } const fetch = { adjacentItems: !!this.plotConfig.folder, @@ -181,10 +188,11 @@ var MetadataPlot = Panel.extend({ requiredKeys: requiredKeys.join(','), annotations: JSON.stringify(this._currentAnnotations) }; - if (this.plotConfig.u && this.plotConfig.u.length >= 3) { + if (this.plotConfig.u && this.plotConfig.u.length >= 3 && anyCompute) { fetch.compute = JSON.stringify({columns: this.plotConfig.u}); } if (!this.plottableDataPromise || !_.isEqual(this._lastPlottableDataFetch, fetch)) { + this.$el.addClass('loading'); this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, method: 'POST', @@ -195,6 +203,8 @@ var MetadataPlot = Panel.extend({ this._lastPlottableDataFetch = fetch; this.plottableDataPromise.done((result) => { this.plottableData = result; + this.plottableDataPromise = null; + this.$el.toggleClass('loading', !!(this.plottableListPromise || this.plottableDataPromise)); }); }, @@ -467,6 +477,7 @@ var MetadataPlot = Panel.extend({ plotlyData.meanline = {visible: true}; plotlyData.yaxis = {zeroline: false}; plotlyData.scalemode = 'width'; + plotlyData.spanmode = 'hard'; plotlyData.width = 0.9; // plotlyData.points = 'outliers'; plotlyData.points = 'all'; diff --git a/histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl b/histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl new file mode 100644 index 00000000..d815e67e --- /dev/null +++ b/histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl @@ -0,0 +1,7 @@ +.modal-body + div.h-plot-resizable + resize vertical + overflow auto + + select + height calc(100% - 24px) diff --git a/histomicsui/web_client/stylesheets/panels/metadataPlot.styl b/histomicsui/web_client/stylesheets/panels/metadataPlot.styl index 194ed55d..d25e282b 100644 --- a/histomicsui/web_client/stylesheets/panels/metadataPlot.styl +++ b/histomicsui/web_client/stylesheets/panels/metadataPlot.styl @@ -17,6 +17,18 @@ min-height 266px min-width 266px + .s-panel-title + .load-mark + display none + +.h-metadata-plot.loading + .s-panel-title + .icon-chart-line + display none + + .load-mark + display inherit + .h-panel-maximized .h-metadata-plot-area.js-plotly-plot height 100% diff --git a/histomicsui/web_client/templates/dialogs/metadataPlot.pug b/histomicsui/web_client/templates/dialogs/metadataPlot.pug index 64c1e5ff..197049a0 100644 --- a/histomicsui/web_client/templates/dialogs/metadataPlot.pug +++ b/histomicsui/web_client/templates/dialogs/metadataPlot.pug @@ -28,7 +28,7 @@ var numIndex = []; plotOptions.forEach((po, idx) => { numIndex.push(numNumbers); if (po.type === 'number') { numNumbers += 1; }}); for series, seriesidx in seriesList - .form-group + .form-group(class=series.multiple ? 'h-plot-resizable' : '') label(for='h-plot-series-' + series.key) #{series.label} if series.comment p.g-hui-description #{series.comment} diff --git a/histomicsui/web_client/templates/panels/metadataPlot.pug b/histomicsui/web_client/templates/panels/metadataPlot.pug index a764986c..7c4373a5 100644 --- a/histomicsui/web_client/templates/panels/metadataPlot.pug +++ b/histomicsui/web_client/templates/panels/metadataPlot.pug @@ -1,7 +1,7 @@ extends ./panel.pug block title - | #[span.icon-chart-line] Metadata Plot + | #[span.icon-chart-line] #[span.icon-spin1.animate-spin.load-mark(title="Getting Plot Information")] Metadata Plot block controls span.s-no-panel-toggle button.g-widget-metadata-plot-settings.btn.btn-sm.btn-default(title="Plot Settings") From 1e71a6d27300794e66072f2baed5108dec8564ab Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 19 Sep 2024 13:56:28 -0400 Subject: [PATCH 33/47] Call plot endpoint less --- histomicsui/web_client/panels/MetadataPlot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 5bfee9bd..cd0cdca8 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -191,7 +191,7 @@ var MetadataPlot = Panel.extend({ if (this.plotConfig.u && this.plotConfig.u.length >= 3 && anyCompute) { fetch.compute = JSON.stringify({columns: this.plotConfig.u}); } - if (!this.plottableDataPromise || !_.isEqual(this._lastPlottableDataFetch, fetch)) { + if (!_.isEqual(this._lastPlottableDataFetch, fetch)) { this.$el.addClass('loading'); this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, From 14c4ead3a5655b2640e582c0e5d0e818fb2389a4 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 20 Sep 2024 08:55:12 -0400 Subject: [PATCH 34/47] Update ruff config file. Fix a typo. --- histomicsui/__init__.py | 2 +- ruff.toml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/histomicsui/__init__.py b/histomicsui/__init__.py index f90cf3cf..cef687f0 100644 --- a/histomicsui/__init__.py +++ b/histomicsui/__init__.py @@ -465,7 +465,7 @@ def lookUpToken(token, parentType, parent): # Auto-ingest annotations into database when a file with an identifier # ending in 'AnnotationFile' is uploaded (usually .anot files). events.bind('data.process', 'histomicsui.annotations', handlers.process_annotations) - # Auto-ingest metadta into parent when a file with an identifier + # Auto-ingest metadata into parent when a file with an identifier # ending in 'ItemMetadata' is uploaded (usually .meta files). events.bind('data.process', 'histomicsui.metadata', handlers.process_metadata) diff --git a/ruff.toml b/ruff.toml index db17e47d..46400c07 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,7 +4,7 @@ exclude = [ "*/web_client/*", "*/*egg*/*", ] -ignore = [ +lint.ignore = [ "B017", "B026", "B904", @@ -33,7 +33,7 @@ ignore = [ "PT017", ] line-length = 100 -select = [ +lint.select = [ "B", # bugbear "C90", # mccabe "D", # pydocstyle @@ -56,8 +56,8 @@ select = [ "RSE", ] -[flake8-quotes] +[lint.flake8-quotes] inline-quotes = "single" -[mccabe] +[lint.mccabe] max-complexity = 14 From f5705d7290afa01fc38fe75f4994387752529968 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 20 Sep 2024 10:26:19 -0400 Subject: [PATCH 35/47] Use session information to be more response to plottable data --- histomicsui/web_client/package.json | 1 + histomicsui/web_client/panels/MetadataPlot.js | 19 ++++++++++++------- setup.py | 2 +- tox.ini | 1 - 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/histomicsui/web_client/package.json b/histomicsui/web_client/package.json index c9b1ee49..87ab915f 100644 --- a/histomicsui/web_client/package.json +++ b/histomicsui/web_client/package.json @@ -42,6 +42,7 @@ "sinon": "^2.1.0", "tinycolor2": "~1.4.1", "url-search-params-polyfill": "^8.1.1", + "uuid": "^8.3.2", "vue": "~2.6.14", "vue-color": "^2.8.1", "vue-loader": "~15.9.8", diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index cd0cdca8..b0d764e9 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import _ from 'underscore'; +import {v4 as uuidv4} from 'uuid'; import {restRequest} from '@girder/core/rest'; @@ -12,6 +13,8 @@ import MetadataPlotDialog from '../dialogs/metadataPlot'; import metadataPlotTemplate from '../templates/panels/metadataPlot.pug'; import '../stylesheets/panels/metadataPlot.styl'; +const sessionId = uuidv4(); + var MetadataPlot = Panel.extend({ events: _.extend(Panel.prototype.events, { 'click .g-widget-metadata-plot-settings': function (event) { @@ -94,7 +97,8 @@ var MetadataPlot = Panel.extend({ method: 'POST', error: null, data: { - annotations: JSON.stringify(this._currentAnnotations) + annotations: JSON.stringify(this._currentAnnotations), + uuid: sessionId } }).done((result) => { this.plottableList = result; @@ -182,25 +186,26 @@ var MetadataPlot = Panel.extend({ requiredKeys = requiredKeys.concat(this.plotConfig.u); } } - const fetch = { + const params = { adjacentItems: !!this.plotConfig.folder, keys: keys.join(','), requiredKeys: requiredKeys.join(','), - annotations: JSON.stringify(this._currentAnnotations) + annotations: JSON.stringify(this._currentAnnotations), + uuid: sessionId }; if (this.plotConfig.u && this.plotConfig.u.length >= 3 && anyCompute) { - fetch.compute = JSON.stringify({columns: this.plotConfig.u}); + params.compute = JSON.stringify({columns: this.plotConfig.u}); } - if (!_.isEqual(this._lastPlottableDataFetch, fetch)) { + if (!_.isEqual(this._lastPlottableDataParams, params)) { this.$el.addClass('loading'); this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, method: 'POST', error: null, - data: fetch + data: params }); } - this._lastPlottableDataFetch = fetch; + this._lastPlottableDataParams = params; this.plottableDataPromise.done((result) => { this.plottableData = result; this.plottableDataPromise = null; diff --git a/setup.py b/setup.py index 097a310e..258b0335 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def prerelease_local_scheme(version): 'Programming Language :: Python :: 3.12', ], install_requires=[ - 'girder-large-image-annotation>=1.29.4', + 'girder-large-image-annotation>=1.29.10', 'girder-slicer-cli-web[girder]>=1.4.0', 'cachetools', 'orjson', diff --git a/tox.ini b/tox.ini index 31cfbb24..c8b1be8c 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ deps = setuptools_scm urllib3<1.26 -rrequirements-dev.txt - girder-large-image-annotation!=1.29.8,!=1.29.9 extras = analysis allowlist_externals = From a63ac84fb5736d21a15332fe391139a1af2de738 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 23 Sep 2024 11:13:28 -0400 Subject: [PATCH 36/47] Fix maximizing the plot. Fix colors on multi-value violin plots. --- histomicsui/web_client/panels/MetadataPlot.js | 35 +++++++++++++++---- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index b0d764e9..154ce66b 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -82,16 +82,19 @@ var MetadataPlot = Panel.extend({ this.plottableList = null; if (this.plottableListPromise) { this.plottableListPromise.abort(); + this.plottableListLoading = false; } this.plottableData = null; if (this.plottableDataPromise) { this.plottableDataPromise.abort(); this.plottableDataPromise = null; + this.plottableDataLoading = false; } const hasPlot = (this.getPlotOptions().filter((v) => v.type === 'number' && v.count).length >= 2); // redo this when annotations are turned on or off this.$el.addClass('loading'); + this.plottableListLoading = true; this.plottableListPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/list`, method: 'POST', @@ -102,14 +105,17 @@ var MetadataPlot = Panel.extend({ } }).done((result) => { this.plottableList = result; - this.plottableListPromise = null; - this.$el.toggleClass('loading', !!(this.plottableListPromise || this.plottableDataPromise)); + this.plottableListLoading = false; + this.$el.toggleClass('loading', !!(this.plottableListLoading || this.plottableDataLoading)); const plotOptions = this.getPlotOptions(); if (plotOptions.filter((v) => v.type === 'number' && v.count).length >= 2) { if (!hasPlot) { this.render(); } } + }).fail((result) => { + this.plottableListLoading = false; + this.$el.toggleClass('loading', !!(this.plottableListLoading || this.plottableDataLoading)); }); }, @@ -198,18 +204,22 @@ var MetadataPlot = Panel.extend({ } if (!_.isEqual(this._lastPlottableDataParams, params)) { this.$el.addClass('loading'); + this.plottableDataLoading = true; this.plottableDataPromise = restRequest({ url: `annotation/item/${this.item.id}/plot/data`, method: 'POST', error: null, data: params }); + this._lastPlottableDataParams = params; } - this._lastPlottableDataParams = params; this.plottableDataPromise.done((result) => { this.plottableData = result; - this.plottableDataPromise = null; - this.$el.toggleClass('loading', !!(this.plottableListPromise || this.plottableDataPromise)); + this.plottableDataLoading = false; + this.$el.toggleClass('loading', !!(this.plottableListLoading || this.plottableDataLoading)); + }).fail(() => { + this.plottableDataLoading = false; + this.$el.toggleClass('loading', !!(this.plottableListLoading || this.plottableDataLoading)); }); }, @@ -483,17 +493,28 @@ var MetadataPlot = Panel.extend({ plotlyData.yaxis = {zeroline: false}; plotlyData.scalemode = 'width'; plotlyData.spanmode = 'hard'; + plotlyData.showlegend = false; plotlyData.width = 0.9; // plotlyData.points = 'outliers'; plotlyData.points = 'all'; plotlyData.pointpos = 0; plotlyData.jitter = 0; // plotlyData.side = 'positive'; - if (plotData.series.c && plotData.series.c.distinct) { + if (plotData.series.c && plotData.series.c.distinct && plotData.series.s.distinct) { plotlyData.transforms = [{ type: 'groupby', groups: plotlyData.x, - styles: Object.keys(plotData.series.c.distinct).map((k, kidx) => ({target: kidx, value: {line: {color: colorBrewerPaired12[kidx]}}})) + styles: Object.keys(plotData.series.s.distinct).map((k, kidx) => { + k = plotData.series.s.distinct[kidx]; + for (let didx = 0; didx < plotData.data.length; didx += 1) { + if (plotData.data[didx][plotData.series.s.index] === k) { + const cval = plotData.data[didx][plotData.series.c.index]; + const cidx = plotData.series.c.distinct.indexOf(cval); + return {target: kidx, value: {line: {color: colorBrewerPaired12[cidx]}}}; + } + } + return {target: kidx, value: {line: {color: '#000000'}}}; + }) }]; } } From c0c27b794d945c1e72e65fd8ed2c997078fa5fd6 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 4 Oct 2024 14:37:51 -0400 Subject: [PATCH 37/47] Fix a check if we can group by two different values for violin plots --- histomicsui/web_client/panels/MetadataPlot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 154ce66b..2e29f1e3 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -500,7 +500,7 @@ var MetadataPlot = Panel.extend({ plotlyData.pointpos = 0; plotlyData.jitter = 0; // plotlyData.side = 'positive'; - if (plotData.series.c && plotData.series.c.distinct && plotData.series.s.distinct) { + if (plotData.series.c && plotData.series.c.distinct && plotData.series.s && plotData.series.s.distinct) { plotlyData.transforms = [{ type: 'groupby', groups: plotlyData.x, From f0d2a5103ecb94cafd0941989ebf5c762b73b052 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 4 Oct 2024 14:50:31 -0400 Subject: [PATCH 38/47] Fix a condition where the plottable data promise could be null --- histomicsui/web_client/panels/MetadataPlot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/histomicsui/web_client/panels/MetadataPlot.js b/histomicsui/web_client/panels/MetadataPlot.js index 2e29f1e3..fa5b8f0c 100644 --- a/histomicsui/web_client/panels/MetadataPlot.js +++ b/histomicsui/web_client/panels/MetadataPlot.js @@ -202,7 +202,7 @@ var MetadataPlot = Panel.extend({ if (this.plotConfig.u && this.plotConfig.u.length >= 3 && anyCompute) { params.compute = JSON.stringify({columns: this.plotConfig.u}); } - if (!_.isEqual(this._lastPlottableDataParams, params)) { + if (!_.isEqual(this._lastPlottableDataParams, params) || !this.plottableDataPromise) { this.$el.addClass('loading'); this.plottableDataLoading = true; this.plottableDataPromise = restRequest({ From ee7013af39da4da6e93c551915d119f4806a6ace Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 11 Oct 2024 17:29:30 -0400 Subject: [PATCH 39/47] Fix a test based on a change in the large_image client --- tests/web_client_specs/huiTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/web_client_specs/huiTest.js b/tests/web_client_specs/huiTest.js index 773e3fc5..870f27ef 100644 --- a/tests/web_client_specs/huiTest.js +++ b/tests/web_client_specs/huiTest.js @@ -85,8 +85,8 @@ }, 'item list to load'); runs(function () { - var $item = $('.g-item-list-link:contains("' + name + '")'); - imageId = $item.next().attr('href').match(/\/item\/([a-f0-9]+)\/download/)[1]; + var $item = $('.g-item-list-link.li-column-record-name:contains("' + name + '")'); + imageId = $item.next().attr('href').match(/item\/([a-f0-9]{24})/)[1]; expect($item.length).toBe(1); $item.click(); }); From a1abee9bfa14ab9ada74ad23d80e67162c5fc9e8 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 17 Oct 2024 08:59:30 -0400 Subject: [PATCH 40/47] Guard resource path lookups --- histomicsui/rest/system.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/histomicsui/rest/system.py b/histomicsui/rest/system.py index b216557a..a378d072 100644 --- a/histomicsui/rest/system.py +++ b/histomicsui/rest/system.py @@ -17,6 +17,7 @@ import datetime import os +from girder import logger from girder.api import access from girder.api.describe import Description, autoDescribeRoute, describeRoute from girder.api.rest import RestException, boundHandler, filtermodel @@ -458,5 +459,12 @@ def getMultipleResourcePaths(self, resources): model = ModelImporter.model(kind) for id in resources[kind]: doc = model.load(id=id, user=user, level=AccessType.READ) - results[kind][id] = path_util.getResourcePath(kind, doc, user=user) + if doc is None: + logger.info(f'Failed to load {kind} {id}') + continue + try: + results[kind][id] = path_util.getResourcePath(kind, doc, user=user) + except Exception: + logger.info(f'Failed to resolve path for {kind} {id} {doc}') + continue return results From 0b08a89de0b8f50aeffe2d4608c891643c30da50 Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 17 Oct 2024 13:11:39 -0400 Subject: [PATCH 41/47] Don't import `girder.logger` --- histomicsui/rest/system.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/histomicsui/rest/system.py b/histomicsui/rest/system.py index 884c854c..c9c10ba2 100644 --- a/histomicsui/rest/system.py +++ b/histomicsui/rest/system.py @@ -15,9 +15,9 @@ ############################################################################# import datetime +import logging import os -from girder import logger from girder.api import access from girder.api.describe import Description, autoDescribeRoute, describeRoute from girder.api.rest import RestException, boundHandler, filtermodel @@ -34,6 +34,7 @@ from histomicsui.constants import PluginSettings +logger = logging.getLogger(__name__) def addSystemEndpoints(apiRoot): """ From 68f81bc1c7e6af0f3a4e88a21625187dbae69add Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 11 Oct 2024 16:42:28 -0400 Subject: [PATCH 42/47] Add and support app buttons. This requires an appropriately new version in large_image. Future work is to support app icons. Those should probably be configurable before any are merged in. --- histomicsui/web_client/views/itemList.js | 35 +++++++++++++++++------- setup.py | 2 +- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/histomicsui/web_client/views/itemList.js b/histomicsui/web_client/views/itemList.js index d3258601..577e9ba1 100644 --- a/histomicsui/web_client/views/itemList.js +++ b/histomicsui/web_client/views/itemList.js @@ -4,7 +4,7 @@ import {wrap} from '@girder/core/utilities/PluginUtils'; import {AccessType} from '@girder/core/constants'; import {restRequest} from '@girder/core/rest'; import events from '@girder/core/events'; -import ItemListWidget from '@girder/core/views/widgets/ItemListWidget'; +import ItemListWidget from '@girder/large_image/views/itemList'; import {HuiSettings} from './utils'; @@ -63,17 +63,32 @@ wrap(ItemListWidget, 'render', function (render) { } HuiSettings.getSettings().then((settings) => { - const brandName = (settings['histomicsui.brand_name'] || ''); - const webrootPath = (settings['histomicsui.webroot_path'] || ''); - if (!this.$el.closest('.modal-dialog').length) { - for (let ix = 0; ix < this.collection.length; ix++) { - if (!this.$el.find('.g-item-list li.g-item-list-entry:eq(' + ix + ') .g-hui-open-link').length && this.collection.models[ix].attributes.largeImage) { - this.$el.find('.g-item-list li.g-item-list-entry:eq(' + ix + ') a[class^=g-]:last').after( - `` - ); + const brandName = (settings['histomicsui.brand_name'] || 'HistomicsUI'); + const webrootPath = (settings['histomicsui.webroot_path'] || 'histomics'); + + ItemListWidget.registeredApplications.histomicsui = { + name: brandName, + // icon: + check: (modelType, model) => { + if (modelType !== 'item' || !model.get('largeImage')) { + return false; + } + const li = model.get('largeImage'); + if (!li.fileId || li.expected === true) { + return false; } + let priority = 0; + try { + if (model.get('meta') && model.get('meta').dicom && model.get('meta').dicom.Modality && model.get('meta').dicom.Modality !== 'SM') { + priority = 1; + } + } catch (e) {} + return { + url: `${webrootPath}#?image=${model.id}`, + priority: priority + }; } - } + }; if (this.accessLevel >= AccessType.WRITE) { adjustView.call(this, settings); } diff --git a/setup.py b/setup.py index 258b0335..be5f4e0c 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def prerelease_local_scheme(version): 'Programming Language :: Python :: 3.12', ], install_requires=[ - 'girder-large-image-annotation>=1.29.10', + 'girder-large-image-annotation>=1.30.1', 'girder-slicer-cli-web[girder]>=1.4.0', 'cachetools', 'orjson', From 757a95fffa72ef529672ab3d4d26228768c0fb4d Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 17 Oct 2024 15:41:20 -0400 Subject: [PATCH 43/47] Load settings earlier. --- histomicsui/web_client/views/itemList.js | 56 +++++++++++++----------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/histomicsui/web_client/views/itemList.js b/histomicsui/web_client/views/itemList.js index 577e9ba1..e4c2ac7d 100644 --- a/histomicsui/web_client/views/itemList.js +++ b/histomicsui/web_client/views/itemList.js @@ -63,32 +63,6 @@ wrap(ItemListWidget, 'render', function (render) { } HuiSettings.getSettings().then((settings) => { - const brandName = (settings['histomicsui.brand_name'] || 'HistomicsUI'); - const webrootPath = (settings['histomicsui.webroot_path'] || 'histomics'); - - ItemListWidget.registeredApplications.histomicsui = { - name: brandName, - // icon: - check: (modelType, model) => { - if (modelType !== 'item' || !model.get('largeImage')) { - return false; - } - const li = model.get('largeImage'); - if (!li.fileId || li.expected === true) { - return false; - } - let priority = 0; - try { - if (model.get('meta') && model.get('meta').dicom && model.get('meta').dicom.Modality && model.get('meta').dicom.Modality !== 'SM') { - priority = 1; - } - } catch (e) {} - return { - url: `${webrootPath}#?image=${model.id}`, - priority: priority - }; - } - }; if (this.accessLevel >= AccessType.WRITE) { adjustView.call(this, settings); } @@ -100,3 +74,33 @@ wrap(ItemListWidget, 'render', function (render) { this.delegateEvents(); } }); + +HuiSettings.getSettings().then((settings) => { + const brandName = (settings['histomicsui.brand_name'] || 'HistomicsUI'); + const webrootPath = (settings['histomicsui.webroot_path'] || 'histomics'); + + ItemListWidget.registeredApplications.histomicsui = { + name: brandName, + // icon: + check: (modelType, model) => { + if (modelType !== 'item' || !model.get('largeImage')) { + return false; + } + const li = model.get('largeImage'); + if (!li.fileId || li.expected === true) { + return false; + } + let priority = 0; + try { + if (model.get('meta') && model.get('meta').dicom && model.get('meta').dicom.Modality && model.get('meta').dicom.Modality !== 'SM') { + priority = 1; + } + } catch (e) {} + return { + url: `${webrootPath}#?image=${model.id}`, + priority: priority + }; + } + }; + return settings; +}); From 5f1f4f1e369dec400e720df44ad35e89d58f9070 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 22 Oct 2024 15:27:30 -0400 Subject: [PATCH 44/47] Update precommit packages --- .pre-commit-config.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f4472cb..57b43002 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-ast @@ -54,21 +54,22 @@ repos: hooks: - id: circleci-config-validate - repo: https://github.com/ThisIsManta/stylus-supremacy - rev: v2.17.5 + rev: v4.0.0 hooks: - id: stylus-supremacy + language_version: "22.10.0" args: - '--options' - './histomicsui/web_client/package.json' - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.6.0 + rev: v0.7.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] types_or: [python, pyi, jupyter] - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.0 hooks: - id: pyupgrade args: From 13197f0127535601a30e0c4be9e2f3966e7272fe Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 29 Oct 2024 10:37:30 -0400 Subject: [PATCH 45/47] Update CI to handle a large-image mocking change --- tests/web_client_specs/annotationSpec.js | 6 +++++- tests/web_client_specs/girderUISpec.js | 6 +++++- tests/web_client_specs/huiTest.js | 6 +++++- tests/web_client_specs/itemSpec.js | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/web_client_specs/annotationSpec.js b/tests/web_client_specs/annotationSpec.js index 2ab88b38..fbf94516 100644 --- a/tests/web_client_specs/annotationSpec.js +++ b/tests/web_client_specs/annotationSpec.js @@ -1506,7 +1506,11 @@ girderTest.promise.done(function () { // remock Webgl huiTest.app.bodyView.once('h:viewerWidgetCreated', function (viewerWidget) { viewerWidget.once('g:beforeFirstRender', function () { - window.geo.util.mockWebglRenderer(); + try { + window.geo.util.mockWebglRenderer(); + } catch (err) { + // if this is already mocked, do nothing. + } }); }); $el.click(); diff --git a/tests/web_client_specs/girderUISpec.js b/tests/web_client_specs/girderUISpec.js index 29863c22..d450abc9 100644 --- a/tests/web_client_specs/girderUISpec.js +++ b/tests/web_client_specs/girderUISpec.js @@ -9,7 +9,11 @@ describe('itemList', function () { var GeojsViewer = window.girder.plugins.large_image.views.imageViewerWidget.geojs; window.girder.utilities.PluginUtils.wrap(GeojsViewer, 'initialize', function (initialize) { this.once('g:beforeFirstRender', function () { - window.geo.util.mockWebglRenderer(); + try { + window.geo.util.mockWebglRenderer(); + } catch (err) { + // if this is already mocked, do nothing. + } }); initialize.apply(this, _.rest(arguments)); }); diff --git a/tests/web_client_specs/huiTest.js b/tests/web_client_specs/huiTest.js index 870f27ef..a255fd87 100644 --- a/tests/web_client_specs/huiTest.js +++ b/tests/web_client_specs/huiTest.js @@ -49,7 +49,11 @@ runs(function () { app.bodyView.once('h:viewerWidgetCreated', function (viewerWidget) { viewerWidget.once('g:beforeFirstRender', function () { - window.geo.util.mockWebglRenderer(); + try { + window.geo.util.mockWebglRenderer(); + } catch (err) { + // if this is already mocked, do nothing. + } }); }); $('.h-open-image').click(); diff --git a/tests/web_client_specs/itemSpec.js b/tests/web_client_specs/itemSpec.js index d73e2aeb..78dbbff3 100644 --- a/tests/web_client_specs/itemSpec.js +++ b/tests/web_client_specs/itemSpec.js @@ -58,7 +58,11 @@ describe('Test the HistomicsUI itemUI screen', function () { var GeojsViewer = window.girder.plugins.large_image.views.imageViewerWidget.geojs; window.girder.utilities.PluginUtils.wrap(GeojsViewer, 'initialize', function (initialize) { this.once('g:beforeFirstRender', function () { - window.geo.util.mockWebglRenderer(); + try { + window.geo.util.mockWebglRenderer(); + } catch (err) { + // if this is already mocked, do nothing. + } }); initialize.apply(this, _.rest(arguments)); }); From 9fb3cdde5a770972b0020c3ca9f6d0389dcef465 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 25 Oct 2024 10:13:08 -0400 Subject: [PATCH 46/47] Add a pyproject.toml base to reduce pip 24.2 warnings --- pyproject.toml | 3 +++ setup.py | 1 + 2 files changed, 4 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..8fd8d67e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index be5f4e0c..ade66d69 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def prerelease_local_scheme(version): 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', ], install_requires=[ 'girder-large-image-annotation>=1.30.1', From dc43dfc61ec70b4c0b554ee70e24174b4cd8efff Mon Sep 17 00:00:00 2001 From: naglepuff Date: Thu, 31 Oct 2024 12:14:50 -0400 Subject: [PATCH 47/47] Use max annotations size config Ported over from the master branch since merging didn't bring this change from @manthey in. --- histomicsui/handlers.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/histomicsui/handlers.py b/histomicsui/handlers.py index ca951c8b..2579022a 100644 --- a/histomicsui/handlers.py +++ b/histomicsui/handlers.py @@ -7,6 +7,7 @@ import cherrypy import girder.utility import girder_large_image_annotation +import large_image.config import orjson from girder.constants import AccessType from girder.exceptions import RestException @@ -91,9 +92,17 @@ def read_entire_file(): while chunk := fptr.read(1048576): yield chunk - contents = b''.join(chunk for chunk in read_entire_file()) - data = orjson.loads(contents.decode()) - del contents + try: + if file['size'] > int(large_image.config.getConfig( + 'max_annotation_input_file_length', 1024 ** 3)): + msg = 'File is larger thatn will be read into memory' + raise Exception(msg) + contents = b''.join(chunk for chunk in read_entire_file()) + data = orjson.loads(contents.decode()) + del contents + except Exception: + logger.error('Could not parse annotation file') + raise if time.time() - startTime > 10: logger.info('Decoded json in %5.3fs', time.time() - startTime)