/** * Class representing a minimap for a large SVG chart. */ class Minimap { private mainView: HTMLElement; private container: HTMLElement; private visibleSection: SVGRectElement; private minimapPane: HTMLDivElement; private miniMapSVG: SVGSVGElement; private mainViewHeight: number = 0; private mainViewWidth: number = 0; private mainViewScrollWidth: number = 0; private mainViewScrollHeight: number = 0; private scaleUnitY: number = 1; private scaleUnitX: number = 1; /** * Create a Minimap. * @param {SVGSVGElement} chartElement - The SVG element of the chart. * @param {HTMLElement} containerElement - The minimap-container element for the minimap. * @param {HTMLElement} mainViewElement - The main viewport element. * @example
const chartElement = document.getElementById('sankey-chart-svg') as SVGSVGElement; const containerElement = document.getElementById('container') as HTMLElement; const mainViewElement = document.getElementById('mainViewport') as HTMLElement; const minimap = new Minimap(chartElement, minimap-containerElement, mainViewElement); */ constructor(chartElement: SVGSVGElement, containerElement: HTMLElement, mainViewElement: HTMLElement) { this.mainView = mainViewElement; this.container = containerElement; this.visibleSection = this.createVisibleSection(); this.miniMapSVG = this.createMiniMapSVG(chartElement.id); this.miniMapSVG.appendChild(this.visibleSection); this.minimapPane = this.createMinimapPane(); this.minimapPane.appendChild(this.miniMapSVG); this.container.appendChild(this.minimapPane); this.mainView.addEventListener('scroll', this.syncScroll.bind(this)); this.visibleSection.addEventListener('mousedown', this.startDrag.bind(this)); // Create a ResizeObserver that triggers the minimap update const resizeObserver = new ResizeObserver(() => { this.initialize(); }); resizeObserver.observe(this.container); resizeObserver.observe(this.mainView); } /** * Reinitialize the Minimap. */ private initialize() { this.mainViewHeight = this.mainView.clientHeight; this.mainViewWidth = this.mainView.clientWidth; this.mainViewScrollWidth = this.mainView.scrollWidth; this.mainViewScrollHeight = this.mainView.scrollHeight; this.visibleSection.setAttribute('width', this.mainViewWidth.toString()); this.visibleSection.setAttribute('height', this.mainViewHeight.toString()); this.miniMapSVG.setAttribute('viewBox', `0 0 ${this.mainViewScrollWidth} ${this.mainViewScrollHeight}`); this.mainView.scrollTop = 0 this.mainView.scrollLeft = 0; this.visibleSection.setAttribute('width', this.mainViewWidth.toString()); this.visibleSection.setAttribute('height', this.mainViewHeight.toString()); this.scaleUnitY = this.mainViewHeight === this.mainViewScrollHeight ? 1 : -1 / (this.mainViewHeight - this.mainViewScrollHeight); this.scaleUnitX = this.mainViewWidth === this.mainViewScrollWidth ? 1 : -1 / (this.mainViewWidth - this.mainViewScrollWidth); this.minimapPane.style.minHeight = `${this.mainViewHeight}px`; this.minimapPane.style.display = 'block'; const minimapHeight = Math.min(this.minimapPane.clientHeight, this.mainViewHeight); this.minimapPane.style.minHeight = `${minimapHeight}px`; if (this.mainViewHeight === this.mainViewScrollHeight && this.mainViewWidth === this.mainViewScrollWidth) { this.minimapPane.style.display = 'none'; } this.syncScroll(); } /** * Create the SVG element for the minimap. * @param {string} svgHref - The href of the SVG element. * @returns {SVGSVGElement} The created SVG element. */ private createMiniMapSVG(svgHref: string): SVGSVGElement { const previewSVG = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); previewSVG.setAttribute('class', 'preview-svg'); const useElement = document.createElementNS('http://www.w3.org/2000/svg', 'use'); useElement.setAttribute('href', `#${svgHref}`); useElement.setAttribute('pointer-events', 'none'); previewSVG.appendChild(useElement); previewSVG.style.position = 'absolute'; previewSVG.style.top = '0'; previewSVG.style.left = '0'; previewSVG.style.width = '100%'; previewSVG.style.height = 'auto'; return previewSVG; } /** * Create the minimap pane element. * @returns {HTMLDivElement} The created minimap pane element. */ private createMinimapPane(): HTMLDivElement { const minimapPane = document.createElement('div'); minimapPane.className = 'minimap-pane'; minimapPane.style.overflow = 'hidden'; minimapPane.style.position = 'absolute'; minimapPane.style.right = '0'; minimapPane.style.top = '0'; return minimapPane; } /** * Create the lense element. * @returns {SVGRectElement} The created lense element. */ private createVisibleSection(): SVGRectElement { const visibleSection = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); visibleSection.setAttribute('class', 'minimap-visible-section'); visibleSection.setAttribute('x', '0'); visibleSection.setAttribute('y', '0'); return visibleSection; } /** * Synchronize the scroll position of the main view and the minimap. */ private syncScroll() { const scrollPosYInPercentage = this.scaleUnitY * this.mainView.scrollTop; const scrollPosXInPercentage = this.scaleUnitX * this.mainView.scrollLeft; this.minimapPane.scrollTop = (this.minimapPane.scrollHeight - this.minimapPane.clientHeight) * scrollPosYInPercentage; this.minimapPane.scrollLeft = (this.minimapPane.scrollWidth - this.minimapPane.clientWidth) * scrollPosXInPercentage; const overlayY = (this.mainViewScrollHeight - this.mainViewHeight) * scrollPosYInPercentage; const overlayX = (this.mainViewScrollWidth - this.mainViewWidth) * scrollPosXInPercentage; this.visibleSection.setAttribute('y', overlayY.toString()); this.visibleSection.setAttribute('x', overlayX.toString()); } /** * Start dragging the lense element. * @param {MouseEvent} event - The mousedown event. */ private startDrag(event: MouseEvent) { event.preventDefault(); document.addEventListener('mousemove', this.drag); document.addEventListener('mouseup', this.endDrag); } /** * Drag the lense element. * @param {MouseEvent} event - The mousemove event. */ private drag = (event: MouseEvent) => { const minimapRect = this.minimapPane.getBoundingClientRect(); const lenseRect = this.visibleSection.getBoundingClientRect(); let newX = event.clientX - minimapRect.left - lenseRect.width / 2; let newY = event.clientY - minimapRect.top - lenseRect.height / 2; newX = Math.max(0, Math.min(newX, minimapRect.width - lenseRect.width)); newY = Math.max(0, Math.min(newY, minimapRect.height - lenseRect.height)); const minimapHeight = this.minimapPane.scrollHeight > this.minimapPane.clientHeight ? minimapRect.height : this.miniMapSVG.getBoundingClientRect().height; const scrollPosYInPercentage = newY / (minimapHeight - lenseRect.height); const scrollPosXInPercentage = newX / (minimapRect.width - lenseRect.width); this.mainView.scrollTop = scrollPosYInPercentage * (this.mainViewScrollHeight - this.mainViewHeight); this.mainView.scrollLeft = scrollPosXInPercentage * (this.mainViewScrollWidth - this.mainViewWidth); } /** * End dragging the lense element. */ private endDrag = () => { document.removeEventListener('mousemove', this.drag); document.removeEventListener('mouseup', this.endDrag); } } export { Minimap };