import { Component, Input, ElementRef, ViewChild, inject, Renderer2, EventEmitter, Output, } from '@angular/core'; import { CommonModule } from '@angular/common'; // modules import { NgbModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; // interfaces import { IMapState, IMapStates, ITopListByStateItem } from './interfaces'; // constants import { DashboardStateConstants } from './constants'; // enums import { ePlacement } from '../ca-icon-dropdown/enums/placement.enum'; @Component({ selector: 'app-ca-heatmap-usa', templateUrl: './ca-heatmap-usa.component.html', styleUrls: ['./ca-heatmap-usa.component.scss'], imports: [CommonModule, NgbModule, NgbPopoverModule], }) export class CaHeatmapUsaComponent { @ViewChild('svgElement') svgElement!: ElementRef; @Input() set stateItems(value: ITopListByStateItem[]) { this._stateItems = value; this.mapUsaStates(); } /** Input to receive hovered state code from external source (e.g., table) */ @Input() set hoveredStateCode(stateCode: string | null) { this._hoveredStateCode = stateCode; this.handleExternalHover(stateCode); } /** Emits the currently hovered state when hovering on the map */ @Output() stateHover = new EventEmitter<{ item: ITopListByStateItem; isHovered: boolean; }>(); private renderer = inject(Renderer2); private _hoveredStateCode: string | null = null; public usaStates: IMapStates = structuredClone( DashboardStateConstants.usaStates ); public _stateItems!: ITopListByStateItem[]; public currentlyHoveredElement: SVGPathElement | null = null; public currentlyHoveredState: IMapState | null = null; // Enums public ePlacement = ePlacement; get hoveredStateCode(): string | null { return this._hoveredStateCode; } private mapUsaStates(): void { this.usaStates = structuredClone(DashboardStateConstants.usaStates); this._stateItems.forEach((selectedMapItem: ITopListByStateItem) => { const selectedState = this.usaStates[selectedMapItem.stateCode]; if (selectedState) { selectedState.color = selectedMapItem.color; selectedState.value = selectedMapItem.value; selectedState.percent = selectedMapItem.percent; } }); } private applyHover( stateCode: string, element: SVGPathElement, emitEvent: boolean ): void { // Skip if same element is already hovered if (this.currentlyHoveredElement === element) { return; } // Clear previous hover this.clearHoverStyling(); // Set new state if (this.usaStates[stateCode]?.value) { this.currentlyHoveredState = this.usaStates[stateCode]; } this.currentlyHoveredElement = element; // Apply styling requestAnimationFrame(() => { this.renderer.addClass(element, '__hover'); }); // Bring element to front this.renderer.appendChild(this.svgElement.nativeElement, element); // Emit event for map-initiated hovers if (emitEvent && this.currentlyHoveredState) { const item = this._stateItems.find( (i) => i.stateCode === stateCode ); if (item) { this.stateHover.emit({ item, isHovered: true }); } } } private clearHover(stateCode?: string): void { if (stateCode) { const item = this._stateItems.find( (i) => i.stateCode === stateCode ); if (item) { this.stateHover.emit({ item, isHovered: false }); } } this.clearHoverStyling(); } private clearHoverStyling(): void { if (this.currentlyHoveredElement) { this.renderer.removeClass(this.currentlyHoveredElement, '__hover'); this.currentlyHoveredElement = null; } // Fallback cleanup for any stray hover classes this.svgElement?.nativeElement .querySelectorAll('.__hover') .forEach((el) => this.renderer.removeClass(el, '__hover')); } private handleExternalHover(stateCode: string | null): void { if (!this.svgElement?.nativeElement) { return; } if (stateCode) { const stateElement = this.svgElement.nativeElement.querySelector( `.state-${stateCode}` ) as SVGPathElement | null; if (stateElement) { this.applyHover(stateCode, stateElement, false); return; } } this.clearHover(); } public onStateHover( stateCode: string, isRemovingHover: boolean, event?: MouseEvent, stateElement?: Element ): void { const target = (event?.target ?? stateElement) as SVGPathElement; if (isRemovingHover) { this.clearHover(stateCode); return; } if (target) { this.applyHover(stateCode, target, true); } } }