UNPKG

@xulab-research/vue-anatomogram

Version:

Interactive anatomical diagrams for Vue.js applications - A Vue-compatible rewrite of EBI anatomogram

433 lines (381 loc) 13.3 kB
// src/Main.js import AnatomogramSvg from './AnatomogramSvg.js' import Assets, { getDefaultView, supportedSpecies } from './Assets.js' export default class Main { constructor(options = {}) { this.options = { species: 'homo_sapiens', // Default species is Homo sapiens selectedView: null, // Selected view (optional) showIds: [], // IDs of organs to show highlightIds: [], // IDs of organs to highlight selectIds: [], // IDs of selected organs showTitle: true, // Option to show title showControls: true, // Option to show controls showColour: '#808080', // Default colour for showing organs highlightColour: '#ff0000', // Colour for highlighted organs selectColour: '#800080', // Colour for selected organs showOpacity: 0.4, // Opacity for showing organs highlightOpacity: 0.6, // Opacity for highlighted organs selectOpacity: 0.8, // Opacity for selected organs onClick: () => {}, // Click event handler onMouseOver: () => {}, // Mouse over event handler onMouseOut: () => {}, // Mouse out event handler onViewChanged: () => {}, // View changed event handler atlasUrl: 'https://www.ebi.ac.uk/gxa/', // Base URL for atlas ...options } if (!this.options.selectedView) { this.options.selectedView = getDefaultView(this.options.species) // Set default view if not provided } this.container = null // Container to mount the anatomogram this.anatomogramSvg = null // SVG element of the anatomogram this.assets = null // Assets related to the anatomogram this.mounted = false // Flag indicating if the anatomogram is mounted this.destroyed = false // Flag indicating if the anatomogram is destroyed } generateMarkup() { const idsWithMarkup = [] // Process organs to show this.options.showIds.forEach(id => { idsWithMarkup.push({ id: id, markupNormal: { fill: this.options.showColour, opacity: this.options.showOpacity, stroke: '#333333', strokeWidth: 1 }, markupUnderFocus: { fill: this.options.showColour, opacity: Math.min(this.options.showOpacity + 0.2, 1.0), stroke: '#000000', strokeWidth: 2 } }) }) // Process highlighted organs this.options.highlightIds.forEach(id => { idsWithMarkup.push({ id: id, markupNormal: { fill: this.options.highlightColour, opacity: this.options.highlightOpacity, stroke: '#333333', strokeWidth: 1 }, markupUnderFocus: { fill: this.options.highlightColour, opacity: Math.min(this.options.highlightOpacity + 0.2, 1.0), stroke: '#000000', strokeWidth: 2 } }) }) // Process selected organs this.options.selectIds.forEach(id => { idsWithMarkup.push({ id: id, markupNormal: { fill: this.options.selectColour, opacity: this.options.selectOpacity, stroke: '#000000', strokeWidth: 2 }, markupUnderFocus: { fill: this.options.selectColour, opacity: Math.min(this.options.selectOpacity + 0.1, 1.0), stroke: '#000000', strokeWidth: 3 } }) }) return idsWithMarkup // Return all the generated markup for the organs } async mount(container) { if (this.destroyed) { throw new Error('Cannot mount destroyed anatomogram instance') // Error if the instance is destroyed } if (!container) { throw new Error('Container element is required') // Error if no container is provided } try { this.container = container container.innerHTML = '' // Initialize assets this.assets = new Assets() await this.assets.init() // Check if species is supported if (!supportedSpecies.includes(this.options.species)) { throw new Error(`Species "${this.options.species}" is not supported. Supported species: ${supportedSpecies.join(', ')}`) } // Create wrapper const wrapper = document.createElement('div') wrapper.className = 'anatomogram-wrapper' wrapper.style.cssText = ` width: 100%; height: 100%; display: flex; flex-direction: column; font-family: Arial, sans-serif; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; background: #ffffff; ` // Add title if (this.options.showTitle) { this.createTitle(wrapper) } // Add controls if (this.options.showControls) { this.createControls(wrapper) } // Add SVG container const svgContainer = document.createElement('div') svgContainer.className = 'anatomogram-svg-container' svgContainer.style.cssText = ` flex: 1; display: flex; justify-content: center; align-items: center; min-height: 300px; overflow: hidden; padding: 20px; background: #fafafa; ` // Initialize SVG this.anatomogramSvg = new AnatomogramSvg({ species: this.options.species, selectedView: this.options.selectedView, idsWithMarkup: this.generateMarkup(), onMouseOver: this.options.onMouseOver, onMouseOut: this.options.onMouseOut, onClick: this.options.onClick, atlasUrl: this.options.atlasUrl }) // Mount SVG await this.anatomogramSvg.mount(svgContainer) wrapper.appendChild(svgContainer) // Add to container container.appendChild(wrapper) this.mounted = true } catch (error) { console.error('Failed to mount anatomogram:', error) this.showError(container, error.message) // Show error message if mounting fails throw error } } createTitle(wrapper) { const titleElement = document.createElement('div') titleElement.className = 'anatomogram-title' titleElement.style.cssText = ` padding: 15px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; font-weight: bold; font-size: 16px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); ` const speciesName = this.options.species.replace('_', ' ') .replace(/\b\w/g, l => l.toUpperCase()) const viewName = this.options.selectedView ? ` - ${this.options.selectedView.charAt(0).toUpperCase() + this.options.selectedView.slice(1)}` : '' titleElement.textContent = `🔬 Anatomogram: ${speciesName}${viewName}` wrapper.appendChild(titleElement) } createControls(wrapper) { const controlsElement = document.createElement('div') controlsElement.className = 'anatomogram-controls' controlsElement.style.cssText = ` padding: 15px 20px; background: #f8f9fa; border-bottom: 1px solid #dee2e6; display: flex; gap: 15px; align-items: center; flex-wrap: wrap; ` // View selector const views = this.assets.getAnatomogramViews(this.options.species) if (views && views.length > 1) { const viewLabel = document.createElement('label') viewLabel.textContent = 'View: ' viewLabel.style.cssText = 'font-weight: 500; color: #495057;' const viewSelect = document.createElement('select') viewSelect.style.cssText = ` padding: 6px 12px; border: 1px solid #ced4da; border-radius: 4px; background: white; font-size: 14px; cursor: pointer; ` views.forEach(view => { const option = document.createElement('option') option.value = view option.textContent = view.charAt(0).toUpperCase() + view.slice(1) option.selected = view === this.options.selectedView viewSelect.appendChild(option) }) viewSelect.addEventListener('change', (e) => { this.switchView(e.target.value) // Switch view on selection change }) controlsElement.appendChild(viewLabel) controlsElement.appendChild(viewSelect) } // Status indicator const statusElement = document.createElement('div') statusElement.style.cssText = ` margin-left: auto; display: flex; gap: 10px; align-items: center; font-size: 13px; color: #6c757d; ` const counts = { shown: this.options.showIds.length, highlighted: this.options.highlightIds.length, selected: this.options.selectIds.length } statusElement.innerHTML = ` <span>📊 Organs: <span style="color: ${this.options.showColour};">Shown: ${counts.shown}</span> | <span style="color: ${this.options.highlightColour};">Highlighted: ${counts.highlighted}</span> | <span style="color: ${this.options.selectColour};">Selected: ${counts.selected}</span> </span> ` // Clear button if (counts.shown + counts.highlighted + counts.selected > 0) { const clearButton = document.createElement('button') clearButton.textContent = '🗑️ Clear All' clearButton.style.cssText = ` padding: 6px 12px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; ` clearButton.addEventListener('click', () => { this.clearAll() // Clear all organs }) statusElement.appendChild(clearButton) } controlsElement.appendChild(statusElement) wrapper.appendChild(controlsElement) } switchView(view) { if (this.anatomogramSvg) { this.options.selectedView = view this.anatomogramSvg.update({ selectedView: view // Update the selected view }) this.options.onViewChanged(view) } } update(newOptions = {}) { if (!this.mounted || this.destroyed) return // Merge new options const oldSpecies = this.options.species Object.assign(this.options, newOptions) // If species changes, remount the anatomogram if (newOptions.species && newOptions.species !== oldSpecies) { if (this.container) { this.mount(this.container) } return } // Update SVG markup if (this.anatomogramSvg) { this.anatomogramSvg.update({ selectedView: this.options.selectedView, idsWithMarkup: this.generateMarkup(), onMouseOver: this.options.onMouseOver, onMouseOut: this.options.onMouseOut, onClick: this.options.onClick }) } } clearAll() { this.update({ showIds: [], highlightIds: [], selectIds: [] }) // Clear all organ selections } showError(container, message) { container.innerHTML = ` <div style=" display: flex; flex-direction: column; justify-content: center; align-items: center; height: 300px; color: #dc3545; text-align: center; padding: 40px; background: #fff5f5; border: 2px dashed #f5c6cb; border-radius: 10px; margin: 20px; "> <div style="font-size: 48px; margin-bottom: 20px;">❌</div> <h3 style="margin: 0 0 15px 0; color: #721c24;">Error Loading Anatomogram</h3> <p style="margin: 0; color: #856464; max-width: 400px; line-height: 1.5;">${message}</p> <button onclick="location.reload()" style=" margin-top: 20px; padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; ">🔄 Reload Page</button> </div> ` // Display error message if there is an issue loading the anatomogram } destroy() { if (this.anatomogramSvg) { this.anatomogramSvg.destroy?.() this.anatomogramSvg = null } if (this.container) { this.container.innerHTML = '' this.container = null } this.assets = null this.mounted = false this.destroyed = true // Set destroyed flag to true } // Helper methods show(ids) { if (!Array.isArray(ids)) ids = [ids] this.update({ showIds: [...new Set([...this.options.showIds, ...ids])] // Add new IDs to show }) } highlight(ids) { if (!Array.isArray(ids)) ids = [ids] this.update({ highlightIds: [...new Set([...this.options.highlightIds, ...ids])] // Add new IDs to highlight }) } select(ids) { if (!Array.isArray(ids)) ids = [ids] this.update({ selectIds: [...new Set([...this.options.selectIds, ...ids])] // Add new IDs to select }) } hide(ids) { if (!Array.isArray(ids)) ids = [ids] this.update({ showIds: this.options.showIds.filter(id => !ids.includes(id)), highlightIds: this.options.highlightIds.filter(id => !ids.includes(id)), selectIds: this.options.selectIds.filter(id => !ids.includes(id)) // Remove specified IDs from show, highlight, and select lists }) } }