diff --git a/.env b/.env index d616f95f..dedacd3d 100644 --- a/.env +++ b/.env @@ -6,3 +6,4 @@ //VITE_PENNSIEVE_API_LOCATION=https://api.pennsieve.io #VITE_API_LOCATION=https://sparc-api.herokuapp.com/ VITE_SPARC_API=https://alan-wu-portal-api.herokuapp.com/ +VITE_APP_CELL_CARDS_API=http://localhost:8787/cellCardsData.json diff --git a/src/App.vue b/src/App.vue index 3977e04e..0fa4c9ab 100644 --- a/src/App.vue +++ b/src/App.vue @@ -161,6 +161,7 @@ export default { ElIconSetting: shallowRef(ElIconSetting), routerIsReady: false, showLongLabel: true, + isMapLoaded: false, } }, computed: { @@ -177,6 +178,7 @@ export default { algoliaId: import.meta.env.VITE_ALGOLIA_ID, pennsieveApi: import.meta.env.VITE_PENNSIEVE_API_LOCATION, flatmapAPI: this.$route.query.flatmapserver ? this.$route.query.flatmapserver : import.meta.env.VITE_FLATMAPAPI_LOCATION, + cellCardsApi: import.meta.env.VITE_APP_CELL_CARDS_API, rootUrl: import.meta.env.VITE_ROOT_URL, } } @@ -251,7 +253,6 @@ export default { } getShareLink(1) }); - }, setFlatmap: function() { this.$refs.map.setCurrentEntry( @@ -317,6 +318,7 @@ export default { }, mapIsLoaded: function(map) { console.log("map is loaded", map) + this.isMapLoaded = true; // map.changeViewingMode('Annotation') }, viewerIsReady: function() { diff --git a/src/components.d.ts b/src/components.d.ts index 95232a3c..0ebd6d22 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -19,6 +19,8 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCol: typeof import('element-plus/es')['ElCol'] + ElCollapse: typeof import('element-plus/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] @@ -28,8 +30,11 @@ declare module 'vue' { ElIconArrowDown: typeof import('@element-plus/icons-vue')['ArrowDown'] ElIconArrowUp: typeof import('@element-plus/icons-vue')['ArrowUp'] ElIconCompass: typeof import('@element-plus/icons-vue')['Compass'] + ElIconCpu: typeof import('@element-plus/icons-vue')['Cpu'] + ElIconDocument: typeof import('@element-plus/icons-vue')['Document'] ElIconEditPen: typeof import('@element-plus/icons-vue')['EditPen'] ElIconMoreFilled: typeof import('@element-plus/icons-vue')['MoreFilled'] + ElIconSGrid: typeof import('@element-plus/icons-vue')['SGrid'] ElIconShare: typeof import('@element-plus/icons-vue')['Share'] ElInput: typeof import('element-plus/es')['ElInput'] ElMain: typeof import('element-plus/es')['ElMain'] diff --git a/src/components/ContentVuer.vue b/src/components/ContentVuer.vue index 7b44eb3f..28e595d5 100644 --- a/src/components/ContentVuer.vue +++ b/src/components/ContentVuer.vue @@ -146,6 +146,9 @@ export default { onShowConnectivityTooltips: function(payload) { this.$refs.viewer?.showConnectivityTooltips(payload); }, + onShowFeatureInFlatmap: function(payload) { + this.$refs.viewer?.showFeatureInFlatmap(payload); + }, onShowReferenceConnectivity: function(payload) { this.$refs.viewer?.showConnectivitiesByReference(payload); }, diff --git a/src/components/DialogToolbarContent.vue b/src/components/DialogToolbarContent.vue index 6d386815..ce9ed18a 100644 --- a/src/components/DialogToolbarContent.vue +++ b/src/components/DialogToolbarContent.vue @@ -29,6 +29,9 @@ + + + {{ globalSettings.viewingMode }} - @@ -435,6 +441,7 @@ import { ElButton as Button, ElCheckbox as Checkbox, ElCol as Col, + ElIcon, ElIcon as Icon, ElInput as Input, ElPopover as Popover, @@ -520,7 +527,8 @@ export default { viewingModes: { 'Exploration': 'Find relevant research and view detail of neural pathways by selecting a pathway to view its connections and data sources', 'Neuron Connection': 'Discover neuron connections by selecting a feature and viewing its associated network connections', - 'Annotation': ['View feature annotations', 'Add, comment on and view feature annotations'] + 'Annotation': ['View feature annotations', 'Add, comment on and view feature annotations'], + 'Cell Type': 'Explore cell cards for different cell types and their associated data', }, authorisedUser: false, mapLoaded: false, @@ -546,12 +554,19 @@ export default { } else if (value === 'Annotation') { this.globalSettings.displayMarkers = false; this.globalSettings.interactiveMode = 'dataset'; + } else if (value === 'Cell Type') { + this.globalSettings.displayMarkers = true; + this.globalSettings.interactiveMode = 'connectivity'; } else { this.globalSettings.displayMarkers = false; this.globalSettings.interactiveMode = 'connectivity'; } this.updateGlobalSettings('viewingMode'); + + if (value === 'Cell Type') { + this.$emit('open-cell-card-explorer'); + } } }, updateGlobalSettings: function(changedKey) { @@ -559,7 +574,7 @@ export default { this.settingsStore.updateGlobalSettings(this.globalSettings); // display marker update - if (updatedSettings.includes('displayMarkers')) { + if (updatedSettings.includes('displayMarkers') || updatedSettings.includes('interactiveMode')) { EventBus.emit('markerUpdate'); } if (updatedSettings.includes('interactiveMode')) { diff --git a/src/components/MapContent.vue b/src/components/MapContent.vue index 79b078e5..f326a55b 100644 --- a/src/components/MapContent.vue +++ b/src/components/MapContent.vue @@ -411,6 +411,7 @@ export default { this.options.algoliaId ? this.settingsStore.updateAlgoliaId(this.options.algoliaId) : null; this.options.pennsieveApi ? this.settingsStore.updatePennsieveApi(this.options.pennsieveApi) : null; this.options.flatmapAPI ? this.settingsStore.updateFlatmapAPI(this.options.flatmapAPI) : null; + this.options.cellCardsApi ? this.settingsStore.updateCellCardsApi(this.options.cellCardsApi) : null; this.options.rootUrl ? this.settingsStore.updateRootUrl(this.options.rootUrl) : null; } this.settingsStore.updateAllClosable(this.allClosable); diff --git a/src/components/SplitDialog.vue b/src/components/SplitDialog.vue index 3d8cf817..5ddfecd4 100644 --- a/src/components/SplitDialog.vue +++ b/src/components/SplitDialog.vue @@ -173,6 +173,10 @@ export default { const wholeBodyScaffoldIDs = [307, '307']; const sckanVersion = Object.keys(this.connectivitiesStore.globalConnectivities) .find(key => key.includes('sckan')); + // Filter active entries and emit their resources (species) + const activeEntries = this.entries.filter((entry) => activePaneIDs.includes(entry.id)); + const activeSpecies = activeEntries.map((entry) => entry.resource); + this.$emit('update-active-species', activeSpecies); const uuids = Array.from( new Set( this.entries @@ -639,6 +643,12 @@ export default { content.onShowConnectivityTooltips(payload); }); }); + EventBus.on('soma-location-hovered', (payload) => { + const contents = this.getActiveContents(); + contents.forEach((content) => { + content.onShowFeatureInFlatmap(payload); + }); + }); EventBus.on('connectivity-source-change', (payload) => { const contents = this.getActiveContents(); //Use ongoingSource array to make sure, the knowledge diff --git a/src/components/SplitFlow.vue b/src/components/SplitFlow.vue index 72ae6c1e..50ed6375 100644 --- a/src/components/SplitFlow.vue +++ b/src/components/SplitFlow.vue @@ -11,6 +11,7 @@ @onFullscreen="onFullscreen" @local-search="onDisplaySearch" @fetch-suggestions="fetchSuggestions" + @open-cell-card-explorer="openCellCardExplorerFromToolbar" ref="dialogToolbar" /> @@ -31,6 +32,7 @@ :filterOptions="filterOptions" :showVisibilityFilter="showVisibilityFilter" :showLongLabel="showLongLabel" + :showCellCards="showCellCards" @tabClicked="onSidebarTabClicked" @tabClosed="onSidebarTabClosed" @actionClick="actionClick" @@ -52,6 +54,8 @@ @show-connectivity-graph="onShowConnectivityGraph" @filter-visibility="onFilterVisibility" @connectivity-item-close="onConnectivityItemClose" + @soma-location-hovered="showSomaLocation" + @soma-locations-ready="onSomaLocationsReady" @trackEvent="trackEvent" /> @@ -186,6 +191,8 @@ export default { filterVisibility: true, filterOptions: [], annotationHighlight: [], + showCellCards: false, + cellCardSomaLocations: [], } }, watch: { @@ -212,6 +219,20 @@ export default { }, }, methods: { + openCellCardExplorerFromToolbar: function () { + this.openCellCardExplorer(); + }, + openCellCardExplorer: function (payload = {}) { + this.showCellCards = true; + + if (this.$refs.sideBar) { + this.$refs.sideBar.tabClicked({ id: 4, type: 'cellCardExplorer' }); + this.$refs.sideBar.setDrawerOpen(true); + if (payload && (payload.filters?.length || payload.query)) { + this.$refs.sideBar.openCellCardExplorerSearch(payload.filters || [], payload.query || ''); + } + } + }, onFilterVisibility: function (state) { this.filterVisibility = state; const filterExpression = { @@ -321,6 +342,8 @@ export default { }); this.filterTriggered = true; } + } else if (action.type === "OpenCellCardExplorer") { + this.openCellCardExplorer(action); } else if (action.type == "URL") { window.open(action.resource, "_blank"); } else if (action.type == "Facet") { @@ -595,6 +618,25 @@ export default { onConnectivityHovered: function (data) { EventBus.emit('connectivity-hovered', data); }, + showSomaLocation: function (name) { + EventBus.emit('soma-location-hovered', name); + }, + onSomaLocationsReady: function (somaLocations) { + const normalizedSomaLocations = (Array.isArray(somaLocations) ? somaLocations : []) + .map((item) => { + return { + label: String(item?.label || '').trim(), + curie: String(item?.curie || '').trim(), + count: Number(item?.count || 0), + }; + }) + .filter((item) => item.label); + + this.cellCardSomaLocations = [...new Map( + normalizedSomaLocations.map((item) => [item.label.toLowerCase(), item]) + ).values()]; + this.updateSomaLocationMarkers(this.cellCardSomaLocations); + }, onConnectivitySourceChange: function (data) { this.connectivityExplorerClicked.push(true); EventBus.emit('connectivity-source-change', data); @@ -727,8 +769,35 @@ export default { }); } } + } else if (data.tabType === 'cellType') { + const filterValues = data.filter.filter(f => (f.facet && f.facet.toLowerCase() !== 'show all')) + .map((f) => f.tagLabel) + .join(', '); + const searchValue = data.query; + + if (filterValues) { + Tagging.sendEvent({ + 'event': 'interaction_event', + 'event_name': 'portal_maps_action_filter', + 'category': filterValues, + 'location': 'map_sidebar_cell_card_filter' + }); + } + + if (searchValue) { + Tagging.sendEvent({ + 'event': 'interaction_event', + 'event_name': 'portal_maps_action_search', + 'category': searchValue, + 'location': 'map_sidebar_cell_card_search' + }); + } } }, + updateSomaLocationMarkers: function (data) { + this.settingsStore.updateCellCardSomaLocations(data); + EventBus.emit("markerUpdate"); + }, updateMarkers: function (data) { this.settingsStore.updateMarkers(data); EventBus.emit("markerUpdate"); @@ -818,6 +887,10 @@ export default { // - connectivity knowledge is loaded // - if sidebar state is not restored yet const sidebarState = state?.sidebar; + // Restore Cell Card Explorer + if (sidebarState?.activeTabId === 4) { + this.showCellCards = true; + } if (!this.sidebarStateRestored && sidebarState && this.$refs.sideBar && this.connectivityKnowledge?.length) { if (sidebarState.connectivityEntries?.length) { this.restoreConnectivityEntries(sidebarState.connectivityEntries); @@ -890,6 +963,11 @@ export default { }) } }, + updateActiveSpeciesForEntries: function (activeSpecies) { + if (this.$refs.sideBar) { + this.$refs.sideBar.updateActiveSpeciesForEntries(activeSpecies); + } + }, contextUpdate: function (payload) { EventBus.emit("contextUpdate", payload); }, @@ -1089,6 +1167,7 @@ export default { PENNSIEVE_API_LOCATION: this.settingsStore.pennsieveApi, ROOT_URL: this.settingsStore.rootUrl, FLATMAPAPI_LOCATION: this.settingsStore.flatmapAPI, + CELL_CARDS_API: this.settingsStore.cellCardsApi, }; }, entries: function() { diff --git a/src/components/viewers/Flatmap.vue b/src/components/viewers/Flatmap.vue index 909e1879..746ad8f1 100644 --- a/src/components/viewers/Flatmap.vue +++ b/src/components/viewers/Flatmap.vue @@ -166,6 +166,28 @@ export default { flatmap.showConnectivityTooltips(payload); } }, + showFeatureInFlatmap: function (payload) { + if (this?.alive && this.flatmapReady) { + const flatmap = this.$refs.flatmap; + + if (payload) { + const searchResults = flatmap.searchSuggestions(payload); + let geoJSONID = undefined; + + if (searchResults?.results.length) { + const featureId = searchResults?.results[0].featureId; + geoJSONID = featureId; + } + + if (geoJSONID) { + flatmap.showPopup(geoJSONID, payload); + } + } else { + flatmap.closeTooltip(); + this.showConnectivityTooltips({connectivityInfo: null, data: []}); + } + } + }, showConnectivitiesByReference: function (payload) { if (this?.alive) { const currentFlatmap = this.$refs.flatmap; diff --git a/src/components/viewers/MultiFlatmap.vue b/src/components/viewers/MultiFlatmap.vue index 3ddacd8c..f6e5a783 100644 --- a/src/components/viewers/MultiFlatmap.vue +++ b/src/components/viewers/MultiFlatmap.vue @@ -200,7 +200,7 @@ export default { updateProvCard: function() { const imp = this.getFlatmapImp(); if (imp) { - let provClone = {id: this.entry.id, prov: imp.mapMetadata}; + let provClone = {id: this.entry.id, prov: imp.mapMetadata, species: this.activeSpecies}; EventBus.emit("mapImpProv", provClone); this.$emit("flatmap-provenance-ready", provClone); } @@ -355,6 +355,28 @@ export default { flatmap.showConnectivityTooltips(payload); } }, + showFeatureInFlatmap: function (payload) { + if (this?.alive && this.flatmapReady) { + const flatmap = this.$refs.multiflatmap.getCurrentFlatmap(); + + if (payload) { + const searchResults = flatmap.searchSuggestions(payload); + let geoJSONID = undefined; + + if (searchResults?.results.length) { + const featureId = searchResults?.results[0].featureId; + geoJSONID = featureId; + } + + if (geoJSONID) { + flatmap.showPopup(geoJSONID, payload); + } + } else { + flatmap.closeTooltip(); + this.showConnectivityTooltips({connectivityInfo: null, data: []}); + } + } + }, showConnectivitiesByReference: function (payload) { if (this?.alive && this.flatmapReady && this.$refs.multiflatmap) { const currentFlatmap = this.$refs.multiflatmap.getCurrentFlatmap(); diff --git a/src/mixins/ContentMixin.js b/src/mixins/ContentMixin.js index b08052d0..a654ffe6 100644 --- a/src/mixins/ContentMixin.js +++ b/src/mixins/ContentMixin.js @@ -184,6 +184,20 @@ export default { if (resource.eventType == "click") { result.eventType = "selected"; if (resource.feature.type == "marker") { + // Cell card explorer - cell type markers + if (!resource.feature['dataset-terms']) { + returnedAction = { + type: "OpenCellCardExplorer", + label: resource.feature.label, + models: resource.feature.models, + query: '', + filters: [{ + facet: resource.feature.label, + term: 'Soma location', + }], + }; + fireResourceSelected = true; + } else { let label = result.internalName; // `resource.feature.id` is the marker identifier (not featureId or models) if (this.settingsStore.isFeaturedMarkerIdentifier(resource.feature.id)) { @@ -236,6 +250,7 @@ export default { const flatmap = this.$refs.multiflatmap.getCurrentFlatmap().mapImp; flatmap.clearSearchResults(); } + } } } else if (resource.eventType == "mouseenter") { result.eventType = "highlighted"; @@ -574,6 +589,9 @@ export default { showConnectivityTooltips: function () { return; }, + showFeatureInFlatmap: function () { + return; + }, setVisibilityFilter: function() { return; }, diff --git a/src/mixins/DynamicMarkerMixin.js b/src/mixins/DynamicMarkerMixin.js index 59a487da..880f7caf 100644 --- a/src/mixins/DynamicMarkerMixin.js +++ b/src/mixins/DynamicMarkerMixin.js @@ -9,7 +9,17 @@ const removeDuplicates = function (arrayOfAnything) { JSON.parse(e) ) } - + +const getCellTypeSomaLocations = function (somaLocations) { + return (Array.isArray(somaLocations) ? somaLocations : []) + .map((location) => ({ + label: String(location?.label || '').trim(), + curie: String(location?.curie || '').trim(), + count: Number(location?.count || 0), + })) + .filter((location) => location.curie && location.count > 0) +} + /* eslint-disable no-alert, no-console */ export default { computed: { @@ -34,17 +44,30 @@ export default { flatmapMarkerUpdate(flatmap) { if (!this.flatmapReady) return; - let flatmapImp = flatmap; - if (!flatmapImp) - flatmapImp = this.getFlatmapImp(); + const flatmapImp = flatmap ?? this.getFlatmapImp(); if (flatmapImp) { - // Set the dataset markers - let markers = this.settingsStore.globalSettings.displayMarkers ? this.settingsStore.markers : []; - markers = removeDuplicates(markers); + const displayMarkers = this.settingsStore.globalSettings.displayMarkers; + const isCellTypeViewingMode = this.settingsStore.globalSettings.viewingMode === "Cell Type"; + const markers = !displayMarkers || isCellTypeViewingMode + ? [] + : removeDuplicates(this.settingsStore.markers); + const somaLocations = !displayMarkers || !isCellTypeViewingMode + ? [] + : getCellTypeSomaLocations(this.settingsStore.cellCardSomaLocations); + flatmapImp.clearMarkers(); flatmapImp.clearDatasetMarkers(); - flatmapImp.addDatasetMarkers(markers); + + if (typeof flatmapImp.clearSomaLocationMarkers === "function") { + flatmapImp.clearSomaLocationMarkers(); + } + + if (isCellTypeViewingMode && typeof flatmapImp.addSomaLocationMarkers === "function") { + flatmapImp.addSomaLocationMarkers(somaLocations); + } else { + flatmapImp.addDatasetMarkers(markers); + } // Set the featured markers if (this.entry.type === "MultiFlatmap") { diff --git a/src/stores/entries.js b/src/stores/entries.js index 0cddf703..e401b68e 100644 --- a/src/stores/entries.js +++ b/src/stores/entries.js @@ -41,6 +41,9 @@ export const useEntriesStore = defineStore('entries', { const sckanVersion = getKnowledgeSourceFromProvenance(prov.prov); entry['uuid'] = prov.prov.uuid; entry['sckanVersion'] = sckanVersion; + if (entry.type === 'MultiFlatmap' && prov.species) { + entry['resource'] = prov.species; + } } }, updateLabelForEntry(targetEntry, label) { diff --git a/src/stores/settings.js b/src/stores/settings.js index 510ee4b7..bdfec547 100644 --- a/src/stores/settings.js +++ b/src/stores/settings.js @@ -12,12 +12,14 @@ export const useSettingsStore = defineStore('settings', { algoliaId: undefined, pennsieveApi: undefined, flatmapAPI: undefined, + cellCardsApi: undefined, mapManager: undefined, rootUrl: undefined, facets: { species: [], gender: [], organ: [] }, appliedFacets: [], numberOfDatasetsForFacets: [], markers: [], + cellCardSomaLocations: [], hoverAnatomies: [], hoverOrgans: [], hoverDOI: '', @@ -110,6 +112,9 @@ export const useSettingsStore = defineStore('settings', { updateFlatmapAPI(flatmapAPI) { this.flatmapAPI = flatmapAPI; }, + updateCellCardsApi(cellCardsApi) { + this.cellCardsApi = cellCardsApi; + }, updateMapManager(mapManager) { this.mapManager = mapManager; }, @@ -119,6 +124,9 @@ export const useSettingsStore = defineStore('settings', { updateMarkers(markers) { this.markers = markers; }, + updateCellCardSomaLocations(somaLocations) { + this.cellCardSomaLocations = somaLocations; + }, updateHoverFeatures(anatomies, organs, doi, connectivity) { this.hoverAnatomies = anatomies; this.hoverOrgans = organs;